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 4f423492..202ca6dc 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 @@ -217,6 +217,7 @@ public final class API { public static final String EXAM_PROCTORING_ENDPOINT = EXAM_MONITORING_ENDPOINT + "/proctoring"; public static final String EXAM_PROCTORING_COLLECTING_ROOMS_SEGMENT = "/collecting-rooms"; + public static final String EXAM_SCREEN_PROCTORING_GROUPS_SEGMENT = "/screenproctoring-groups"; public static final String EXAM_PROCTORING_OPEN_BREAK_OUT_ROOM_SEGMENT = "/open"; public static final String EXAM_PROCTORING_CLOSE_ROOM_SEGMENT = "/close"; public static final String EXAM_PROCTORING_NOTIFY_OPEN_ROOM_SEGMENT = "/notify-open-room"; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/monitoring/MonitoringFullPageData.java b/src/main/java/ch/ethz/seb/sebserver/gbl/monitoring/MonitoringFullPageData.java index 0cec3599..e0d55526 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/monitoring/MonitoringFullPageData.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/monitoring/MonitoringFullPageData.java @@ -15,12 +15,14 @@ import com.fasterxml.jackson.annotation.JsonProperty; import ch.ethz.seb.sebserver.gbl.model.Domain; import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom; +import ch.ethz.seb.sebserver.gbl.model.session.ScreenProctoringGroup; @JsonIgnoreProperties(ignoreUnknown = true) public class MonitoringFullPageData { public static final String ATTR_CONNECTIONS_DATA = "monitoringConnectionData"; public static final String ATTR_PROCTORING_DATA = "proctoringData"; + public static final String ATTR_SCREEN_PROCTORING_DATA = "screenProctoringData"; @JsonProperty(Domain.CLIENT_CONNECTION.ATTR_EXAM_ID) public final Long examId; @@ -28,15 +30,19 @@ public class MonitoringFullPageData { public final MonitoringSEBConnectionData monitoringConnectionData; @JsonProperty(ATTR_PROCTORING_DATA) public final Collection proctoringData; + @JsonProperty(ATTR_SCREEN_PROCTORING_DATA) + final Collection screenProctoringData; public MonitoringFullPageData( @JsonProperty(Domain.CLIENT_CONNECTION.ATTR_EXAM_ID) final Long examId, @JsonProperty(ATTR_CONNECTIONS_DATA) final MonitoringSEBConnectionData monitoringConnectionData, - @JsonProperty(ATTR_PROCTORING_DATA) final Collection proctoringData) { + @JsonProperty(ATTR_PROCTORING_DATA) final Collection proctoringData, + @JsonProperty(ATTR_SCREEN_PROCTORING_DATA) final Collection screenProctoringData) { this.examId = examId; this.monitoringConnectionData = monitoringConnectionData; this.proctoringData = proctoringData; + this.screenProctoringData = screenProctoringData; } public Long getExamId() { @@ -51,6 +57,10 @@ public class MonitoringFullPageData { return this.proctoringData; } + public Collection getScreenProctoringData() { + return this.screenProctoringData; + } + @Override public int hashCode() { final int prime = 31; @@ -79,12 +89,14 @@ public class MonitoringFullPageData { @Override public String toString() { final StringBuilder builder = new StringBuilder(); - builder.append("OverallMonitroingData [examId="); + builder.append("MonitoringFullPageData [examId="); builder.append(this.examId); builder.append(", monitoringConnectionData="); builder.append(this.monitoringConnectionData); builder.append(", proctoringData="); builder.append(this.proctoringData); + builder.append(", screenProctoringData="); + builder.append(this.screenProctoringData); builder.append("]"); return builder.toString(); } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/RAPSpringConfig.java b/src/main/java/ch/ethz/seb/sebserver/gui/RAPSpringConfig.java index b4c2df1d..978c53ea 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/RAPSpringConfig.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/RAPSpringConfig.java @@ -45,6 +45,9 @@ public class RAPSpringConfig { @Value("${sebserver.gui.remote.proctoring.api-servler.endpoint:/remote-view-servlet}") private String remoteProctoringViewServletEndpoint; + @Value("${sebserver.gui.screen.proctoring.api-servler.endpoint:/screen-proctoring}") + private String screenProctoringViewServletEndpoint; + @Bean public StaticApplicationPropertyResolver staticApplicationPropertyResolver() { return new StaticApplicationPropertyResolver(); @@ -83,6 +86,17 @@ public class RAPSpringConfig { this.remoteProctoringEndpoint + this.remoteProctoringViewServletEndpoint + "/*"); } + @Bean + public ServletRegistrationBean servletScreenProctoringRegistrationBean( + final ApplicationContext applicationContext) { + + final ScreenProctoringServlet proctoringServlet = applicationContext + .getBean(ScreenProctoringServlet.class); + return new ServletRegistrationBean<>( + proctoringServlet, + this.remoteProctoringEndpoint + this.screenProctoringViewServletEndpoint + "/*"); + } + @Bean public MessageSource messageSource() { final ReloadableResourceBundleMessageSource reloadableResourceBundleMessageSource = diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/ScreenProctoringServlet.java b/src/main/java/ch/ethz/seb/sebserver/gui/ScreenProctoringServlet.java new file mode 100644 index 00000000..1f97d74c --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/ScreenProctoringServlet.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2023 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; + +import java.io.IOException; + +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; + +import org.springframework.stereotype.Component; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; + +import ch.ethz.seb.sebserver.gbl.model.user.UserInfo; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.AuthorizationContextHolder; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.SEBServerAuthorizationContext; + +@Component +@GuiProfile +public class ScreenProctoringServlet extends HttpServlet { + + private static final long serialVersionUID = 4147410676185956971L; + + @Override + protected void doGet( + final HttpServletRequest req, + final HttpServletResponse resp) throws ServletException, IOException { + + final HttpSession httpSession = req.getSession(); + final ServletContext servletContext = httpSession.getServletContext(); + final WebApplicationContext webApplicationContext = WebApplicationContextUtils + .getRequiredWebApplicationContext(servletContext); + + final UserInfo user = isAuthenticated(httpSession, webApplicationContext); + + // TODO https://stackoverflow.com/questions/46582/response-redirect-with-post-instead-of-get + + final String hello = "Hello"; + +// StringBuilder sb = new StringBuilder(); +// sb.Append(""); +// sb.AppendFormat(@""); +// sb.AppendFormat("
",postbackUrl); +// sb.AppendFormat("", id); +// // Other params go here +// sb.Append("
"); +// sb.Append(""); +// sb.Append(""); + + resp.getOutputStream().println(hello); + + } + + private UserInfo isAuthenticated( + final HttpSession httpSession, + final WebApplicationContext webApplicationContext) { + + final AuthorizationContextHolder authorizationContextHolder = webApplicationContext + .getBean(AuthorizationContextHolder.class); + final SEBServerAuthorizationContext authorizationContext = authorizationContextHolder + .getAuthorizationContext(httpSession); + if (!authorizationContext.isValid() || !authorizationContext.isLoggedIn()) { + throw new RuntimeException("No authentication found"); + } + + return authorizationContext.getLoggedInUser().getOrThrow(); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionCategory.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionCategory.java index 6dfbc15c..a4a1ffce 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionCategory.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionCategory.java @@ -48,6 +48,7 @@ public enum ActionCategory { STATE_FILTER(new LocTextKey("sebserver.exam.monitoring.action.category.statefilter"), 40), GROUP_FILTER(new LocTextKey("sebserver.exam.monitoring.action.category.groupfilter"), 50), PROCTORING(new LocTextKey("sebserver.exam.overall.action.category.proctoring"), 60), + SCREEN_PROCTORING(new LocTextKey("sebserver.exam.overall.action.category.screenproctoring"), 65), FINISHED_EXAM_LIST(new LocTextKey("sebserver.finished.exam.list.actions"), 1); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java index 52b83fd7..3a7f80ac 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/action/ActionDefinition.java @@ -1035,11 +1035,11 @@ public enum ActionDefinition { PageStateDefinitionImpl.MONITORING_RUNNING_EXAM, ActionCategory.CLIENT_EVENT_LIST), - MONITOR_EXAM_NEW_PROCTOR_ROOM( - new LocTextKey("sebserver.monitoring.exam.action.newroom"), - ImageIcon.VISIBILITY, - PageStateDefinitionImpl.MONITORING_RUNNING_EXAM, - ActionCategory.PROCTORING), +// MONITOR_EXAM_NEW_PROCTOR_ROOM( +// new LocTextKey("sebserver.monitoring.exam.action.newroom"), +// ImageIcon.VISIBILITY, +// PageStateDefinitionImpl.MONITORING_RUNNING_EXAM, +// ActionCategory.PROCTORING), MONITOR_EXAM_VIEW_PROCTOR_ROOM( new LocTextKey("sebserver.monitoring.exam.action.viewroom"), ImageIcon.PROCTOR_ROOM, @@ -1056,6 +1056,12 @@ public enum ActionDefinition { PageStateDefinitionImpl.MONITORING_RUNNING_EXAM, ActionCategory.PROCTORING), + MONITOR_EXAM_VIEW_SCREEN_PROCTOR_GROUP( + new LocTextKey("sebserver.monitoring.exam.action.viewgroup"), + ImageIcon.SCREEN_PROC_ON, + PageStateDefinitionImpl.MONITORING_RUNNING_EXAM, + ActionCategory.SCREEN_PROCTORING), + FINISHED_EXAM_VIEW_LIST( new LocTextKey("sebserver.finished.action.list"), PageStateDefinitionImpl.FINISHED_EXAM_LIST), diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ScreenProctoringSettingsPopup.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ScreenProctoringSettingsPopup.java index 8b12658f..5cf42840 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ScreenProctoringSettingsPopup.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/exam/ScreenProctoringSettingsPopup.java @@ -76,8 +76,6 @@ public class ScreenProctoringSettingsPopup { new LocTextKey("sebserver.exam.sps.form.accountId"); private final static LocTextKey FORM_ACCOUNT_SECRET_SPS = new LocTextKey("sebserver.exam.sps.form.accountSecret"); - private final static LocTextKey FORM_COLLECT_STRATEGY = - new LocTextKey("sebserver.exam.sps.form.collect.strategy"); private final static LocTextKey SAVE_TEXT_KEY = new LocTextKey("sebserver.exam.sps.form.saveSettings"); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringRunningExam.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringRunningExam.java index c33c5e50..84ed77dc 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringRunningExam.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/monitoring/MonitoringRunningExam.java @@ -18,12 +18,15 @@ import java.util.function.BooleanSupplier; import java.util.function.Consumer; import java.util.function.Function; +import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.text.StringEscapeUtils; import org.eclipse.swt.SWT; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.TreeItem; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; @@ -38,7 +41,10 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.Indicator; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringFeature; +import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus; +import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom; +import ch.ethz.seb.sebserver.gbl.model.session.ScreenProctoringGroup; import ch.ethz.seb.sebserver.gbl.model.user.UserInfo; import ch.ethz.seb.sebserver.gbl.model.user.UserRole; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; @@ -60,8 +66,11 @@ import ch.ethz.seb.sebserver.gui.service.push.ServerPushService; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExam; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamProctoringSettings; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetScreenProctoringSettings; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.clientgroup.GetClientGroups; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.indicator.GetIndicators; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetCollectingRooms; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetScreenProctoringGroups; 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.FullPageMonitoringGUIUpdate; @@ -76,6 +85,8 @@ import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService @GuiProfile public class MonitoringRunningExam implements TemplateComposer { + private static final Logger log = LoggerFactory.getLogger(MonitoringRunningExam.class); + private static final LocTextKey EMPTY_SELECTION_TEXT_KEY = new LocTextKey("sebserver.monitoring.exam.connection.emptySelection"); private static final LocTextKey EMPTY_ACTIVE_SELECTION_TEXT_KEY = @@ -194,8 +205,7 @@ public class MonitoringRunningExam implements TemplateComposer { ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION, ActionDefinition.MONITOR_EXAM_QUIT_SELECTED, ActionDefinition.MONITOR_EXAM_LOCK_SELECTED, - ActionDefinition.MONITOR_EXAM_DISABLE_SELECTED_CONNECTION, - ActionDefinition.MONITOR_EXAM_NEW_PROCTOR_ROOM)); + ActionDefinition.MONITOR_EXAM_DISABLE_SELECTED_CONNECTION)); actionBuilder @@ -275,14 +285,19 @@ public class MonitoringRunningExam implements TemplateComposer { .call() .getOr(null); - if (proctoringSettings != null && proctoringSettings.enableProctoring) { - guiUpdates.add(createProctoringActions( - proctoringSettings, - currentUser.getProctoringGUIService(), - pageContext, - content, - actionBuilder)); - } + final ScreenProctoringSettings screenProctoringSettings = this.restService + .getBuilder(GetScreenProctoringSettings.class) + .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) + .call() + .getOr(null); + + guiUpdates.add(createProctoringActions( + proctoringSettings, + screenProctoringSettings, + currentUser.getProctoringGUIService(), + pageContext, + content)); + } // finally start the page update (server push) @@ -304,12 +319,20 @@ public class MonitoringRunningExam implements TemplateComposer { private FullPageMonitoringGUIUpdate createProctoringActions( final ProctoringServiceSettings proctoringSettings, + final ScreenProctoringSettings screenProctoringSettings, final ProctoringGUIService proctoringGUIService, final PageContext pageContext, - final Composite parent, - final PageActionBuilder actionBuilder) { + final Composite parent) { - if (proctoringSettings.enabledFeatures.contains(ProctoringFeature.TOWN_HALL)) { + final PageActionBuilder actionBuilder = this.pageService + .pageActionBuilder(pageContext.clearEntityKeys()); + + final boolean proctoringEnabled = proctoringSettings != null && + BooleanUtils.toBoolean(proctoringSettings.enableProctoring); + final boolean screenProctoringEnabled = screenProctoringSettings != null && + BooleanUtils.toBoolean(proctoringSettings.enableProctoring); + + if (proctoringEnabled && proctoringSettings.enabledFeatures.contains(ProctoringFeature.TOWN_HALL)) { final EntityKey entityKey = pageContext.getEntityKey(); actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_OPEN_TOWNHALL_PROCTOR_ROOM) .withEntityKey(entityKey) @@ -338,18 +361,44 @@ public class MonitoringRunningExam implements TemplateComposer { } } - this.monitoringProctoringService.initCollectingRoomActions( - pageContext, - actionBuilder, - proctoringSettings, - proctoringGUIService); + proctoringGUIService.clearCollectingRoomActionState(); + final EntityKey entityKey = pageContext.getEntityKey(); + final Collection collectingRooms = (proctoringEnabled) + ? this.pageService + .getRestService() + .getBuilder(GetCollectingRooms.class) + .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) + .call() + .onError(error -> log.error("Failed to get collecting room data:", error)) + .getOr(Collections.emptyList()) + : Collections.emptyList(); - return monitoringStatus -> this.monitoringProctoringService.updateCollectingRoomActions( - monitoringStatus.proctoringData(), + final Collection screenProctoringGroups = (screenProctoringEnabled) + ? this.pageService + .getRestService() + .getBuilder(GetScreenProctoringGroups.class) + .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) + .call() + .onError(error -> log.error("Failed to get collecting room data:", error)) + .getOr(Collections.emptyList()) + : Collections.emptyList(); + + this.monitoringProctoringService.updateCollectingRoomActions( + collectingRooms, + screenProctoringGroups, pageContext, - actionBuilder, proctoringSettings, - proctoringGUIService); + proctoringGUIService, + screenProctoringSettings); + + return monitoringStatus -> this.monitoringProctoringService + .updateCollectingRoomActions( + monitoringStatus.proctoringData(), + monitoringStatus.screenProctoringData(), + pageContext, + proctoringSettings, + proctoringGUIService, + screenProctoringSettings); } private FullPageMonitoringGUIUpdate createFilterActions( @@ -430,8 +479,7 @@ public class MonitoringRunningExam implements TemplateComposer { .noEventPropagation() .withSwitchAction( actionBuilder.newAction(hideActionDef) - .withExec( - hideStateViewAction(filter, clientTable, status)) + .withExec(hideStateViewAction(filter, clientTable, status)) .noEventPropagation() .withNameAttributes(numOfConnections) .create()) @@ -443,8 +491,7 @@ public class MonitoringRunningExam implements TemplateComposer { .noEventPropagation() .withSwitchAction( actionBuilder.newAction(showActionDef) - .withExec( - showStateViewAction(filter, clientTable, status)) + .withExec(showStateViewAction(filter, clientTable, status)) .noEventPropagation() .withNameAttributes(numOfConnections) .create()) diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetScreenProctoringGroups.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetScreenProctoringGroups.java new file mode 100644 index 00000000..f560807f --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/session/GetScreenProctoringGroups.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023 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 java.util.Collection; + +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.model.session.ScreenProctoringGroup; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; + +@Lazy +@Component +@GuiProfile +public class GetScreenProctoringGroups extends RestCall> { + + public GetScreenProctoringGroups() { + super(new TypeKey<>( + CallType.GET_LIST, + EntityType.SCREEN_PROCTORING_GROUP, + new TypeReference>() { + }), + HttpMethod.GET, + MediaType.APPLICATION_FORM_URLENCODED, + API.EXAM_PROCTORING_ENDPOINT + + API.MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_SCREEN_PROCTORING_GROUPS_SEGMENT); + } +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/MonitoringFilter.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/MonitoringFilter.java index bb29cb35..f9d75015 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/MonitoringFilter.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/MonitoringFilter.java @@ -16,6 +16,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.ClientGroup; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus; import ch.ethz.seb.sebserver.gbl.model.session.ClientMonitoringData; import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom; +import ch.ethz.seb.sebserver.gbl.model.session.ScreenProctoringGroup; import ch.ethz.seb.sebserver.gbl.monitoring.MonitoringFullPageData; import ch.ethz.seb.sebserver.gbl.monitoring.MonitoringSEBConnectionData; @@ -95,4 +96,13 @@ public interface MonitoringFilter { } } + default Collection screenProctoringData() { + final MonitoringFullPageData monitoringFullPageData = getMonitoringFullPageData(); + if (monitoringFullPageData != null) { + return monitoringFullPageData.getScreenProctoringData(); + } else { + return null; + } + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/MonitoringProctoringService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/MonitoringProctoringService.java index a6b70723..08aa0047 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/MonitoringProctoringService.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/MonitoringProctoringService.java @@ -8,9 +8,9 @@ package ch.ethz.seb.sebserver.gui.service.session.proctoring; +import java.net.URI; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.Map; import org.apache.commons.io.IOUtils; @@ -18,6 +18,7 @@ import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.eclipse.rap.rwt.RWT; import org.eclipse.rap.rwt.client.service.JavaScriptExecutor; +import org.eclipse.rap.rwt.client.service.UrlLauncher; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.widgets.Display; @@ -27,7 +28,10 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.core.io.Resource; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; import com.fasterxml.jackson.core.type.TypeReference; @@ -39,8 +43,10 @@ import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringFeature; +import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom; +import ch.ethz.seb.sebserver.gbl.model.session.ScreenProctoringGroup; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; import ch.ethz.seb.sebserver.gbl.util.Tuple; import ch.ethz.seb.sebserver.gbl.util.Utils; @@ -55,7 +61,6 @@ import ch.ethz.seb.sebserver.gui.service.page.PageService; import ch.ethz.seb.sebserver.gui.service.page.PageService.PageActionBuilder; import ch.ethz.seb.sebserver.gui.service.page.event.ActionActivationEvent; import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction; -import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetCollectingRooms; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetProctorRoomConnection; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.IsTownhallRoomAvailable; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.NotifyProctoringRoomOpened; @@ -93,6 +98,9 @@ public class MonitoringProctoringService { private final Resource openRoomScriptRes; private final String remoteProctoringEndpoint; + @Value("${sebserver.gui.screen.proctoring.api-servler.endpoint:/screen-proctoring}") + private String screenProctoringViewServletEndpoint; + public MonitoringProctoringService( final PageService pageService, final GuiServiceInfo guiServiceInfo, @@ -152,87 +160,124 @@ public class MonitoringProctoringService { return action; } - public void initCollectingRoomActions( - final PageContext pageContext, - final PageActionBuilder actionBuilder, - final ProctoringServiceSettings proctoringSettings, - final ProctoringGUIService proctoringGUIService) { - - proctoringGUIService.clearCollectingRoomActionState(); - final EntityKey entityKey = pageContext.getEntityKey(); - final Collection collectingRooms = this.pageService - .getRestService() - .getBuilder(GetCollectingRooms.class) - .withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId) - .call() - .onError(error -> log.error("Failed to get collecting room data:", error)) - .getOr(Collections.emptyList()); - - updateCollectingRoomActions( - collectingRooms, - pageContext, - actionBuilder, - proctoringSettings, - proctoringGUIService); - } - public void updateCollectingRoomActions( final Collection collectingRooms, + final Collection screenProctoringGroups, final PageContext pageContext, - final PageActionBuilder actionBuilder, final ProctoringServiceSettings proctoringSettings, - final ProctoringGUIService proctoringGUIService) { - - final EntityKey entityKey = pageContext.getEntityKey(); - final I18nSupport i18nSupport = this.pageService.getI18nSupport(); + final ProctoringGUIService proctoringGUIService, + final ScreenProctoringSettings screenProctoringSettings) { collectingRooms .stream() - .forEach(room -> { - if (proctoringGUIService.collectingRoomActionActive(room.name)) { - // update action - final TreeItem treeItem = proctoringGUIService.getCollectingRoomActionItem(room.name); - proctoringGUIService.registerCollectingRoomAction(room, treeItem); - treeItem.setText(i18nSupport.getText(new LocTextKey( - ActionDefinition.MONITOR_EXAM_VIEW_PROCTOR_ROOM.title.name, - room.subject, - room.roomSize, - proctoringSettings.collectingRoomSize))); - processProctorRoomActionActivation(treeItem, room, pageContext); - } else { - // create new action - final PageAction action = - actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_VIEW_PROCTOR_ROOM) - .withEntityKey(entityKey) - .withExec(_action -> openExamProctoringRoom( - proctoringGUIService, - proctoringSettings, - room, - _action)) - .withNameAttributes( - room.subject, - room.roomSize, - proctoringSettings.collectingRoomSize) - .noEventPropagation() - .create(); - - this.pageService.publishAction( - action, - _treeItem -> proctoringGUIService.registerCollectingRoomAction( - room, - _treeItem, - collectingRoom -> showCollectingRoomPopup(pageContext, entityKey, - collectingRoom))); - - processProctorRoomActionActivation( - proctoringGUIService.getCollectingRoomActionItem(room.name), - room, pageContext); - } - }); + .forEach(room -> updateProctoringAction( + pageContext, + proctoringSettings, + proctoringGUIService, + room)); if (proctoringSettings.enabledFeatures.contains(ProctoringFeature.TOWN_HALL)) { updateTownhallButton(proctoringGUIService, pageContext); } + + if (screenProctoringGroups != null) { + screenProctoringGroups + .stream() + .forEach(group -> updateScreenProctoringAction( + pageContext, + screenProctoringSettings, + proctoringGUIService, + group)); + } + } + + private void updateScreenProctoringAction( + final PageContext pageContext, + final ScreenProctoringSettings settings, + final ProctoringGUIService proctoringGUIService, + final ScreenProctoringGroup group) { + + final PageActionBuilder actionBuilder = this.pageService + .pageActionBuilder(pageContext.clearEntityKeys()); + final EntityKey entityKey = pageContext.getEntityKey(); + final I18nSupport i18nSupport = this.pageService.getI18nSupport(); + + final TreeItem screeProcotringGroupAction = proctoringGUIService.getScreeProcotringGroupAction(group); + if (screeProcotringGroupAction != null) { + // update action + screeProcotringGroupAction.setText(i18nSupport.getText(new LocTextKey( + ActionDefinition.MONITOR_EXAM_VIEW_SCREEN_PROCTOR_GROUP.title.name, + group.name, + group.size))); + + } else { + // create action + this.pageService.publishAction( + actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_VIEW_SCREEN_PROCTOR_GROUP) + .withEntityKey(entityKey) + .withExec(_action -> openScreenProctoringTab( + settings, + group, + _action)) + .withNameAttributes( + group.name, + group.size) + .noEventPropagation() + .create(), + _treeItem -> proctoringGUIService.registerScreeProcotringGroupAction(group, _treeItem)); + } + } + + private void updateProctoringAction( + final PageContext pageContext, + final ProctoringServiceSettings proctoringSettings, + final ProctoringGUIService proctoringGUIService, + final RemoteProctoringRoom room) { + + final PageActionBuilder actionBuilder = this.pageService + .pageActionBuilder(pageContext.clearEntityKeys()); + final EntityKey entityKey = pageContext.getEntityKey(); + final I18nSupport i18nSupport = this.pageService.getI18nSupport(); + + if (proctoringGUIService.collectingRoomActionActive(room.name)) { + // update action + final TreeItem treeItem = proctoringGUIService.getCollectingRoomActionItem(room.name); + proctoringGUIService.registerCollectingRoomAction(room, treeItem); + treeItem.setText(i18nSupport.getText(new LocTextKey( + ActionDefinition.MONITOR_EXAM_VIEW_PROCTOR_ROOM.title.name, + room.subject, + room.roomSize, + proctoringSettings.collectingRoomSize))); + processProctorRoomActionActivation(treeItem, room, pageContext); + } else { + // create new action + final PageAction action = + actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_VIEW_PROCTOR_ROOM) + .withEntityKey(entityKey) + .withExec(_action -> openExamProctoringRoom( + proctoringGUIService, + proctoringSettings, + room, + _action)) + .withNameAttributes( + room.subject, + room.roomSize, + proctoringSettings.collectingRoomSize) + .noEventPropagation() + .create(); + + this.pageService.publishAction( + action, + _treeItem -> proctoringGUIService.registerCollectingRoomAction( + room, + _treeItem, + collectingRoom -> showCollectingRoomPopup(pageContext, entityKey, + collectingRoom))); + + processProctorRoomActionActivation( + proctoringGUIService.getCollectingRoomActionItem(room.name), + room, pageContext); + } } private void showCollectingRoomPopup( @@ -263,6 +308,28 @@ public class MonitoringProctoringService { this.proctorRoomConnectionsPopup.show(pc, collectingRoom.subject); } + private PageAction openScreenProctoringTab( + final ScreenProctoringSettings settings, + final ScreenProctoringGroup group, + final PageAction _action) { + + final String serviceRedirect = settings.spsServiceURL + "/guilogin"; + final ResponseEntity redirect = new RestTemplate().exchange( + serviceRedirect, + HttpMethod.GET, + null, + Void.class); + + final URI redirectLocation = redirect.getHeaders().getLocation(); + final UrlLauncher launcher = RWT.getClient().getService(UrlLauncher.class); + final String url = this.remoteProctoringEndpoint + + this.screenProctoringViewServletEndpoint + + "?group=" + group.uuid + + "&loc=" + redirectLocation; + launcher.openURL(url); + return _action; + } + private PageAction openExamProctoringRoom( final ProctoringGUIService proctoringGUIService, final ProctoringServiceSettings proctoringSettings, diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ProctoringGUIService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ProctoringGUIService.java index efc0d333..9ee7d793 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ProctoringGUIService.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/session/proctoring/ProctoringGUIService.java @@ -28,6 +28,7 @@ import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection; import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom; +import ch.ethz.seb.sebserver.gbl.model.session.ScreenProctoringGroup; import ch.ethz.seb.sebserver.gbl.util.Pair; import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; @@ -40,6 +41,7 @@ public class ProctoringGUIService { private static final Logger log = LoggerFactory.getLogger(ProctoringGUIService.class); public static final String SESSION_ATTR_PROCTORING_DATA = "SESSION_ATTR_PROCTORING_DATA"; + public static final String SESSION_ATTR_SCREEN_PROCTORING_DATA = "SESSION_ATTR_SCREEN_PROCTORING_DATA"; private static final String SHOW_CONNECTION_ACTION_APPLIED = "SHOW_CONNECTION_ACTION_APPLIED"; private static final String CLOSE_ROOM_SCRIPT = "var existingWin = window.open('', '%s'); existingWin.close()"; @@ -47,10 +49,23 @@ public class ProctoringGUIService { final Map openWindows = new HashMap<>(); final Map> collectingRoomsActionState; + final Map screenProctoringGroupState; public ProctoringGUIService(final RestService restService) { this.restService = restService; this.collectingRoomsActionState = new HashMap<>(); + this.screenProctoringGroupState = new HashMap<>(); + } + + public void registerScreeProcotringGroupAction( + final ScreenProctoringGroup screenProctoringGroup, + final TreeItem actionItem) { + + this.screenProctoringGroupState.put(screenProctoringGroup.uuid, actionItem); + } + + public TreeItem getScreeProcotringGroupAction(final ScreenProctoringGroup screenProctoringGroup) { + return this.screenProctoringGroupState.get(screenProctoringGroup.uuid); } public boolean collectingRoomActionActive(final String name) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java index 1daf89a1..3070fec4 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java @@ -90,20 +90,35 @@ public interface ExamAdminService { /** This indicates if proctoring is set and enabled for a certain exam. * * @param examId the exam instance - * @return Result refer to proctoring is enabled flag or to an error when happened. */ - default Result isProctoringEnabled(final Exam exam) { + * @return proctoring is enabled flag */ + default boolean isProctoringEnabled(final Exam exam) { if (exam == null || exam.id == null) { - return Result.ofRuntimeError("Invalid Exam model"); + return false; } if (exam.additionalAttributesIncluded()) { - return Result.tryCatch(() -> { - return BooleanUtils.toBooleanObject( - exam.getAdditionalAttribute(ProctoringServiceSettings.ATTR_ENABLE_PROCTORING)); - }); + return BooleanUtils.toBoolean( + exam.getAdditionalAttribute(ProctoringServiceSettings.ATTR_ENABLE_PROCTORING)); } - return isProctoringEnabled(exam.id); + return isProctoringEnabled(exam.id).getOr(false); + } + + /** This indicates if screen proctoring is set and enabled for a certain exam. + * + * @param examId the exam instance + * @return screen proctoring is enabled flag */ + default boolean isScreenProctoringEnabled(final Exam exam) { + if (exam == null || exam.id == null) { + return false; + } + + if (exam.additionalAttributesIncluded()) { + return BooleanUtils.toBoolean( + exam.getAdditionalAttribute(ProctoringServiceSettings.ATTR_ENABLE_PROCTORING)); + } + + return isProctoringEnabled(exam.id).getOr(false); } /** Updates needed additional attributes from assigned exam configuration for the exam @@ -117,6 +132,12 @@ public interface ExamAdminService { * @return Result refer to proctoring is enabled flag or to an error when happened. */ Result isProctoringEnabled(final Long examId); + /** This indicates if screen proctoring is set and enabled for a certain exam. + * + * @param examId the exam identifier + * @return Result refer to screen proctoring is enabled flag or to an error when happened. */ + Result isScreenProctoringEnabled(final Long examId); + /** Get the exam proctoring service implementation for specified exam. * * @param examId the exam identifier diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java index 721ac440..0e1d79f6 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java @@ -33,6 +33,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamStatus; import ch.ethz.seb.sebserver.gbl.model.exam.OpenEdxSEBRestriction; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings; import ch.ethz.seb.sebserver.gbl.model.exam.QuizData; +import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationNode; @@ -269,6 +270,23 @@ public class ExamAdminServiceImpl implements ExamAdminService { }); } + @Override + public Result isScreenProctoringEnabled(final Long examId) { + return this.additionalAttributesDAO.getAdditionalAttribute( + EntityType.EXAM, + examId, + ScreenProctoringSettings.ATTR_ENABLE_SCREEN_PROCTORING) + .map(rec -> BooleanUtils.toBoolean(rec.getValue())) + .onErrorDo(error -> { + if (log.isDebugEnabled()) { + log.warn("Failed to verify screen proctoring enabled for exam: {}, {}", + examId, + error.getMessage()); + } + return false; + }); + } + @Override public Result getExamProctoringService(final Long examId) { return getProctoringServiceSettings(examId) diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ScreenProctoringService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ScreenProctoringService.java index 5cf5f3f7..f86d642f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ScreenProctoringService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ScreenProctoringService.java @@ -8,10 +8,13 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session; +import java.util.Collection; + import org.springframework.context.event.EventListener; import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.ScreenProctoringSettings; +import ch.ethz.seb.sebserver.gbl.model.session.ScreenProctoringGroup; import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl.ExamDeletionEvent; @@ -44,6 +47,12 @@ public interface ScreenProctoringService extends SessionUpdateTask { * @return Result refer to the given Exam or to an error when happened */ Result applyScreenProctoingForExam(Long examId); + /** Get list of all screen proctoring collecting groups for a particular exam. + * + * @param examId The exam identifier (PK) + * @return Result refer to the list of ScreenProctoringGroup or to an error when happened */ + Result> getCollectingGroups(Long examId); + /** Gets invoked after an exam has been changed and saved. * * @param exam the exam that has been changed and saved */ diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionControlTask.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionControlTask.java index 7ab39540..9563119b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionControlTask.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionControlTask.java @@ -78,7 +78,7 @@ public class ExamSessionControlTask implements DisposableBean { this.examTimePrefix, this.examTimeSuffix); - this.updateMaster(); + this.webserviceInfo.updateMaster(); SEBServerInit.INIT_LOGGER.info("------>"); SEBServerInit.INIT_LOGGER.info( @@ -105,8 +105,7 @@ public class ExamSessionControlTask implements DisposableBean { initialDelay = 5000) private void examSessionUpdateTask() { - updateMaster(); - + this.webserviceInfo.updateMaster(); if (!this.webserviceInfo.isMaster()) { return; } @@ -120,10 +119,6 @@ public class ExamSessionControlTask implements DisposableBean { .forEach(SessionUpdateTask::processSessionUpdateTask); } - private void updateMaster() { - this.webserviceInfo.updateMaster(); - } - @Override public void destroy() { // TODO try to reset master diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ScreenProctoringServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ScreenProctoringServiceImpl.java index 64b16db0..1ecd33c7 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ScreenProctoringServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ScreenProctoringServiceImpl.java @@ -199,6 +199,11 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService { }); } + @Override + public Result> getCollectingGroups(final Long examId) { + return this.screenProctoringGroupDAO.getCollectingGroups(examId); + } + @Override public Result updateExamOnScreenProctoingService(final Long examId) { return this.examDAO.byPK(examId) diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomProctoringService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomProctoringService.java index 9a6fc59f..1c233bbd 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomProctoringService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/proctoring/ZoomProctoringService.java @@ -60,7 +60,6 @@ import org.springframework.web.client.RestClientResponseException; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; -import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService; import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.api.APIMessage; @@ -108,10 +107,6 @@ public class ZoomProctoringService implements RemoteProctoringService { private static final String ZOOM_ACCESS_TOKEN_HEADER = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}"; - @Deprecated - private static final String ZOOM_API_ACCESS_TOKEN_PAYLOAD = - "{\"iss\":\"%s\",\"exp\":%s}"; - private static final Map SEB_API_NAME_INSTRUCTION_NAME_MAPPING = Utils.immutableMapOf(Arrays.asList( new Tuple<>( API.EXAM_PROCTORING_ATTR_RECEIVE_AUDIO, @@ -137,7 +132,6 @@ public class ZoomProctoringService implements RemoteProctoringService { .stream().collect(Collectors.toMap(Tuple::get_1, Tuple::get_2))); private final ExamSessionService examSessionService; - private final ClientHttpRequestFactoryService clientHttpRequestFactoryService; private final Cryptor cryptor; private final AsyncService asyncService; private final JSONMapper jsonMapper; @@ -150,7 +144,6 @@ public class ZoomProctoringService implements RemoteProctoringService { public ZoomProctoringService( final ExamSessionService examSessionService, - final ClientHttpRequestFactoryService clientHttpRequestFactoryService, final Cryptor cryptor, final AsyncService asyncService, final JSONMapper jsonMapper, @@ -162,7 +155,6 @@ public class ZoomProctoringService implements RemoteProctoringService { @Value("${sebserver.webservice.proctoring.zoom.tokenexpiry.seconds:86400}") final int tokenExpirySeconds) { this.examSessionService = examSessionService; - this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; this.cryptor = cryptor; this.asyncService = asyncService; this.jsonMapper = jsonMapper; @@ -706,13 +698,7 @@ public class ZoomProctoringService implements RemoteProctoringService { } private ZoomRestTemplate createNewRestTemplate(final ProctoringServiceSettings proctoringSettings) { - if (StringUtils.isNoneBlank(proctoringSettings.accountId)) { - log.info("Create new OAuthZoomRestTemplate for settings: {}", proctoringSettings); - return new OAuthZoomRestTemplate(this, proctoringSettings); - } else { - log.warn("Create new JWTZoomRestTemplate for settings: {}", proctoringSettings); - return new JWTZoomRestTemplate(this, proctoringSettings); - } + return new OAuthZoomRestTemplate(this, proctoringSettings); } private static abstract class ZoomRestTemplate { @@ -1054,94 +1040,6 @@ public class ZoomProctoringService implements RemoteProctoringService { } } - @Deprecated - private final static class JWTZoomRestTemplate extends ZoomRestTemplate { - - public JWTZoomRestTemplate( - final ZoomProctoringService zoomProctoringService, - final ProctoringServiceSettings proctoringSettings) { - - super(zoomProctoringService, proctoringSettings); - } - - @Override - public void initConnection() { - if (this.restTemplate == null) { - - this.credentials = new ClientCredentials( - this.proctoringSettings.appKey, - this.proctoringSettings.appSecret); - - this.restTemplate = new RestTemplate(this.zoomProctoringService.clientHttpRequestFactoryService - .getClientHttpRequestFactory() - .getOrThrow()); - } - } - - @Override - public HttpHeaders getHeaders() { - final String jwt = this.createJWTForAPIAccess( - this.credentials, - System.currentTimeMillis() + Constants.MINUTE_IN_MILLIS); - - final HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.set(HttpHeaders.AUTHORIZATION, "Bearer " + jwt); - httpHeaders.set(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); - return httpHeaders; - } - - private String createJWTForAPIAccess( - final ClientCredentials credentials, - final Long expTime) { - - try { - - final CharSequence decryptedSecret = this.zoomProctoringService.cryptor - .decrypt(credentials.secret) - .getOrThrow(); - - final StringBuilder builder = new StringBuilder(); - final Encoder urlEncoder = Base64.getUrlEncoder().withoutPadding(); - - final String jwtHeaderPart = urlEncoder - .encodeToString(ZOOM_ACCESS_TOKEN_HEADER.getBytes(StandardCharsets.UTF_8)); - - final String jwtPayload = String.format( - ZOOM_API_ACCESS_TOKEN_PAYLOAD - .replaceAll(" ", "") - .replaceAll("\n", ""), - credentials.clientIdAsString(), - expTime); - - if (log.isTraceEnabled()) { - log.trace("Zoom API Token payload: {}", jwtPayload); - } - - final String jwtPayloadPart = urlEncoder - .encodeToString(jwtPayload.getBytes(StandardCharsets.UTF_8)); - - final String message = jwtHeaderPart + "." + jwtPayloadPart; - - final Mac sha256_HMAC = Mac.getInstance(TOKEN_ENCODE_ALG); - final SecretKeySpec secret_key = new SecretKeySpec( - Utils.toByteArray(decryptedSecret), - TOKEN_ENCODE_ALG); - - sha256_HMAC.init(secret_key); - final String hash = urlEncoder - .encodeToString(sha256_HMAC.doFinal(Utils.toByteArray(message))); - - builder.append(message) - .append(".") - .append(hash); - - return builder.toString(); - } catch (final Exception e) { - throw new RuntimeException("Failed to create JWT for Zoom API access: ", e); - } - } - } - private static final class ZoomCredentialsAccessTokenProvider extends OAuth2AccessTokenSupport implements AccessTokenProvider { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java index 81f499a5..4bf7e984 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamMonitoringController.java @@ -52,6 +52,7 @@ import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction; import ch.ethz.seb.sebserver.gbl.model.session.ClientNotification; import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom; +import ch.ethz.seb.sebserver.gbl.model.session.ScreenProctoringGroup; import ch.ethz.seb.sebserver.gbl.model.user.UserInfo; import ch.ethz.seb.sebserver.gbl.model.user.UserRole; import ch.ethz.seb.sebserver.gbl.monitoring.MonitoringFullPageData; @@ -66,11 +67,12 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap; import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService; import ch.ethz.seb.sebserver.webservice.servicelayer.institution.SecurityKeyService; -import ch.ethz.seb.sebserver.webservice.servicelayer.session.RemoteProctoringRoomService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.RemoteProctoringRoomService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientConnectionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientNotificationService; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.ScreenProctoringService; import io.swagger.v3.oas.annotations.security.SecurityRequirement; @WebServiceProfile @@ -90,6 +92,7 @@ public class ExamMonitoringController { private final RemoteProctoringRoomService examProcotringRoomService; private final ExamAdminService examAdminService; private final SecurityKeyService securityKeyService; + private final ScreenProctoringService screenProctoringService; private final Executor executor; public ExamMonitoringController( @@ -101,6 +104,7 @@ public class ExamMonitoringController { final RemoteProctoringRoomService examProcotringRoomService, final SecurityKeyService securityKeyService, final ExamAdminService examAdminService, + final ScreenProctoringService screenProctoringService, @Qualifier(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME) final Executor executor) { this.sebClientConnectionService = sebClientConnectionService; @@ -112,6 +116,7 @@ public class ExamMonitoringController { this.examProcotringRoomService = examProcotringRoomService; this.examAdminService = examAdminService; this.securityKeyService = securityKeyService; + this.screenProctoringService = screenProctoringService; this.executor = executor; } @@ -314,6 +319,9 @@ public class ExamMonitoringController { name = API.EXAM_MONITORING_CLIENT_GROUP_FILTER, required = false) final String hiddenClientGroups) { + // TODO respond this within another Thread-pool (Executor) + // TODO try to cache some monitoring data throughout multiple requests (for about 2 sec.) + final Exam runningExam = checkPrivileges(institutionId, examId); final MonitoringSEBConnectionData monitoringSEBConnectionData = this.examSessionService @@ -322,25 +330,28 @@ public class ExamMonitoringController { createMonitoringFilter(hiddenStates, hiddenClientGroups)) .getOrThrow(); - MonitoringFullPageData monitoringFullPageData; - if (this.examAdminService.isProctoringEnabled(runningExam).getOr(false)) { - final Collection proctoringData = this.examProcotringRoomService - .getProctoringCollectingRooms(examId) - .getOrThrow(); + final boolean proctoringEnabled = this.examAdminService.isProctoringEnabled(runningExam); + final boolean screenProctoringEnabled = this.examAdminService.isScreenProctoringEnabled(runningExam); - monitoringFullPageData = new MonitoringFullPageData( - examId, - monitoringSEBConnectionData, - proctoringData); + final Collection proctoringData = (proctoringEnabled) + ? this.examProcotringRoomService + .getProctoringCollectingRooms(examId) + .onError(error -> log.error("Failed to get RemoteProctoringRoom for exam: {}", examId, error)) + .getOr(Collections.emptyList()) + : Collections.emptyList(); - } else { - monitoringFullPageData = new MonitoringFullPageData( - examId, - monitoringSEBConnectionData, - Collections.emptyList()); - } + final Collection screenProctoringData = (screenProctoringEnabled) + ? this.screenProctoringService + .getCollectingGroups(examId) + .onError(error -> log.error("Failed to get ScreenProctoringGroup for exam: {}", examId, error)) + .getOr(Collections.emptyList()) + : Collections.emptyList(); - return monitoringFullPageData; + return new MonitoringFullPageData( + examId, + monitoringSEBConnectionData, + proctoringData, + screenProctoringData); } @RequestMapping( 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 cc03c5ab..a9740692 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 @@ -31,12 +31,14 @@ import ch.ethz.seb.sebserver.gbl.model.Domain; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom; +import ch.ethz.seb.sebserver.gbl.model.session.ScreenProctoringGroup; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService; import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService; -import ch.ethz.seb.sebserver.webservice.servicelayer.session.RemoteProctoringRoomService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.RemoteProctoringRoomService; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.ScreenProctoringService; import io.swagger.v3.oas.annotations.security.SecurityRequirement; @WebServiceProfile @@ -51,17 +53,20 @@ public class ExamProctoringController { private final ExamAdminService examAdminService; private final AuthorizationService authorizationService; private final ExamSessionService examSessionService; + private final ScreenProctoringService screenProctoringService; public ExamProctoringController( final RemoteProctoringRoomService examProcotringRoomService, final ExamAdminService examAdminService, final AuthorizationService authorizationService, - final ExamSessionService examSessionService) { + final ExamSessionService examSessionService, + final ScreenProctoringService screenProctoringService) { this.examProcotringRoomService = examProcotringRoomService; this.examAdminService = examAdminService; this.authorizationService = authorizationService; this.examSessionService = examSessionService; + this.screenProctoringService = screenProctoringService; } /** This is called by Spring to initialize the WebDataBinder and is used here to @@ -95,6 +100,25 @@ public class ExamProctoringController { .getOrThrow(); } + @RequestMapping( + path = API.MODEL_ID_VAR_PATH_SEGMENT + + API.EXAM_SCREEN_PROCTORING_GROUPS_SEGMENT, + method = RequestMethod.GET, + consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, + produces = MediaType.APPLICATION_JSON_VALUE) + public Collection getScreenProctoringGroupsOfExam( + @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) { + + checkAccess(institutionId, examId); + return this.screenProctoringService + .getCollectingGroups(examId) + .getOrThrow(); + } + @RequestMapping( path = API.MODEL_ID_VAR_PATH_SEGMENT, method = RequestMethod.GET, diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index b538491b..dbc1f8ce 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -2173,14 +2173,17 @@ sebserver.monitoring.exam.list.title=Running Exams sebserver.monitoring.exam.list.actions= sebserver.monitoring.exam.action.detail.view=Back To Monitoring sebserver.monitoring.exam.action.list.view=Monitoring -sebserver.monitoring.exam.action.viewroom=View {0} ( {1} / {2} ) +sebserver.monitoring.exam.action.viewroom=View {0} ( {1} / {2} ) +sebserver.monitoring.exam.action.viewgroup=View {0} ( {1} ) sebserver.exam.monitoring.action.category.statefilter=State Filter sebserver.exam.monitoring.action.category.groupfilter=Client Group Filter -sebserver.exam.overall.action.category.proctoring=Proctoring +sebserver.exam.overall.action.category.proctoring=Live Proctoring sebserver.monitoring.exam.action.proctoring.openTownhall=Open Townhall sebserver.monitoring.exam.action.proctoring.showTownhall=Show Townhall sebserver.monitoring.exam.action.proctoring.closeTownhall=Close Townhall +sebserver.exam.overall.action.category.screenproctoring=Screen Proctoring + sebserver.monitoring.exam.proctoring.room.all.name=Townhall Room sebserver.monitoring.exam.proctoring.action.close=Close Window sebserver.monitoring.exam.proctoring.action.broadcaston.audio=Start Audio Broadcast