SEBSERV-139 implementing join-in room in overall monitoring view

This commit is contained in:
anhefti 2020-08-27 10:37:56 +02:00
parent a91192ecbb
commit 3c0c8a4c41
22 changed files with 740 additions and 187 deletions

View file

@ -13,8 +13,9 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
@JsonIgnoreProperties(ignoreUnknown = true)
public class SEBClientProctoringConnectionData {
public class SEBProctoringConnectionData {
public static final String ATTR_CONNECTION_TOKEN = "connectionToken";
public static final String ATTR_SERVER_HOST = "serverHost";
public static final String ATTR_SERVER_URL = "serverURL";
public static final String ATTR_ROOM_NAME = "roomName";
@ -22,6 +23,9 @@ public class SEBClientProctoringConnectionData {
public static final String ATTR_ACCESS_TOKEN = "accessToken";
public static final String ATTR_CONNECTION_URL = "connectionURL";
@JsonProperty(ATTR_CONNECTION_TOKEN)
public final String connectionToken;
@JsonProperty(ATTR_SERVER_HOST)
public final String serverHost;
@ -37,24 +41,25 @@ public class SEBClientProctoringConnectionData {
@JsonProperty(ATTR_ACCESS_TOKEN)
public final String accessToken;
@JsonProperty(ATTR_CONNECTION_URL)
public final String connectionURL;
@JsonCreator
public SEBClientProctoringConnectionData(
public SEBProctoringConnectionData(
@JsonProperty(ATTR_CONNECTION_TOKEN) final String connectionToken,
@JsonProperty(ATTR_SERVER_HOST) final String serverHost,
@JsonProperty(ATTR_SERVER_URL) final String serverURL,
@JsonProperty(ATTR_ROOM_NAME) final String roomName,
@JsonProperty(ATTR_SUBJECT) final String subject,
@JsonProperty(ATTR_ACCESS_TOKEN) final String accessToken,
@JsonProperty(ATTR_CONNECTION_URL) final String connectionURL) {
@JsonProperty(ATTR_ACCESS_TOKEN) final String accessToken) {
this.connectionToken = connectionToken;
this.serverHost = serverHost;
this.serverURL = serverURL;
this.roomName = roomName;
this.subject = subject;
this.accessToken = accessToken;
this.connectionURL = connectionURL;
}
public String getConnectionToken() {
return this.connectionToken;
}
public String getServerHost() {
@ -69,10 +74,6 @@ public class SEBClientProctoringConnectionData {
return this.serverURL;
}
public String getConnectionURL() {
return this.connectionURL;
}
public String getRoomName() {
return this.roomName;
}
@ -94,8 +95,6 @@ public class SEBClientProctoringConnectionData {
builder.append(this.subject);
builder.append(", accessToken=");
builder.append(this.accessToken);
builder.append(", connectionURL=");
builder.append(this.connectionURL);
builder.append("]");
return builder.toString();
}

View file

@ -22,7 +22,7 @@ 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.exam.SEBClientProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnectionData;
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;
@ -51,8 +51,8 @@ public class ProctoringServlet extends HttpServlet {
" const options = {\n" +
" parentNode: document.querySelector('#proctoring'),\n" +
" roomName: '%s',\n" +
// " width: 600,\n" +
" height: 400,\n" +
" width: window.innerWidth,\n" +
" height: window.innerHeight,\n" +
" jwt: '%s',\n" +
" configOverwrite: { startAudioOnly: false, startWithAudioMuted: true, startWithVideoMuted: true, disable1On1Mode: true },\n" +
" interfaceConfigOverwrite: { " +
@ -100,8 +100,8 @@ public class ProctoringServlet extends HttpServlet {
return;
}
final SEBClientProctoringConnectionData proctoringConnectionData =
(SEBClientProctoringConnectionData) httpSession.getAttribute(SESSION_ATTR_PROCTORING_DATA);
final SEBProctoringConnectionData proctoringConnectionData =
(SEBProctoringConnectionData) httpSession.getAttribute(SESSION_ATTR_PROCTORING_DATA);
final String script = String.format(
HTML,

View file

@ -27,7 +27,7 @@ import ch.ethz.seb.sebserver.gbl.model.EntityKey;
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.ProctoringSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBClientProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
@ -54,7 +54,7 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetIndicator
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetProctoringSettings;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.logs.GetExtendedClientEventPage;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetClientConnectionData;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetProctorDataForSEBClient;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetProctorRoomConnectionData;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser;
import ch.ethz.seb.sebserver.gui.service.session.ClientConnectionDetails;
import ch.ethz.seb.sebserver.gui.service.session.InstructionProcessor;
@ -277,10 +277,7 @@ public class MonitoringClientConnection implements TemplateComposer {
.withEntityKey(parentEntityKey)
.withExec(action -> this.openProctorScreen(action, connectionToken))
.noEventPropagation()
.publishIf(() -> proctoringEnabled)
;
.publishIf(() -> proctoringEnabled);
}
// @formatter:off
@ -295,12 +292,12 @@ public class MonitoringClientConnection implements TemplateComposer {
// @formatter:on
private PageAction openProctorScreen(final PageAction action, final String connectionToken) {
final SEBClientProctoringConnectionData proctoringConnectionData =
this.pageService.getRestService().getBuilder(GetProctorDataForSEBClient.class)
.withURIVariable(API.PARAM_MODEL_ID, action.getEntityKey().modelId)
.withURIVariable(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken)
.call()
.getOrThrow();
final SEBProctoringConnectionData proctoringConnectionData = this.pageService.getRestService()
.getBuilder(GetProctorRoomConnectionData.class)
.withURIVariable(API.PARAM_MODEL_ID, action.getEntityKey().modelId)
.withQueryParam(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken)
.call()
.getOrThrow();
final Encoder urlEncoder = Base64.getUrlEncoder().withoutPadding();
final String roomName = urlEncoder.encodeToString(Utils.toByteArray(connectionToken));
@ -316,6 +313,9 @@ public class MonitoringClientConnection implements TemplateComposer {
this.guiServiceInfo.getExternalServerURIBuilder().toUriString(),
roomName);
javaScriptExecutor.execute(script);
this.pageService.getCurrentUser()
.getProctoringGUIService()
.registerProctoringWindow(roomName);
return action;
}

View file

@ -15,6 +15,8 @@ import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Function;
import org.eclipse.rap.rwt.RWT;
import org.eclipse.rap.rwt.client.service.JavaScriptExecutor;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
@ -31,6 +33,8 @@ import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
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.ProctoringSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
@ -38,6 +42,8 @@ import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gbl.util.Tuple;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.gui.GuiServiceInfo;
import ch.ethz.seb.sebserver.gui.ProctoringServlet;
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
import ch.ethz.seb.sebserver.gui.service.ResourceService;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
@ -53,10 +59,13 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
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.GetIndicators;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetProctoringSettings;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetClientConnectionDataList;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.GetProctorRoomConnectionData;
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;
import ch.ethz.seb.sebserver.gui.service.session.ProctoringGUIService;
@Lazy
@Component
@ -80,18 +89,21 @@ public class MonitoringRunningExam implements TemplateComposer {
private final PageService pageService;
private final ResourceService resourceService;
private final InstructionProcessor instructionProcessor;
private final GuiServiceInfo guiServiceInfo;
private final long pollInterval;
protected MonitoringRunningExam(
final ServerPushService serverPushService,
final PageService pageService,
final InstructionProcessor instructionProcessor,
final GuiServiceInfo guiServiceInfo,
@Value("${sebserver.gui.webservice.poll-interval:1000}") final long pollInterval) {
this.serverPushService = serverPushService;
this.pageService = pageService;
this.resourceService = pageService.getResourceService();
this.instructionProcessor = instructionProcessor;
this.guiServiceInfo = guiServiceInfo;
this.pollInterval = pollInterval;
}
@ -145,7 +157,8 @@ public class MonitoringRunningExam implements TemplateComposer {
pageContext,
ActionDefinition.MONITOR_EXAM_CLIENT_CONNECTION,
ActionDefinition.MONITOR_EXAM_QUIT_SELECTED,
ActionDefinition.MONITOR_EXAM_DISABLE_SELECTED_CONNECTION));
ActionDefinition.MONITOR_EXAM_DISABLE_SELECTED_CONNECTION,
ActionDefinition.MONITOR_EXAM_NEW_PROCTOR_ROOM));
this.serverPushService.runServerPush(
new ServerPushContext(content, Utils.truePredicate()),
@ -275,6 +288,136 @@ public class MonitoringRunningExam implements TemplateComposer {
}
}
final boolean proctoringEnabled = restService
.getBuilder(GetProctoringSettings.class)
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
.call()
.map(ProctoringSettings::getEnableProctoring)
.getOr(false);
if (proctoringEnabled) {
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_NEW_PROCTOR_ROOM)
.withEntityKey(entityKey)
.withSelect(
clientTable::getSelection,
action -> newProctoringRoom(clientTable, action),
EMPTY_SELECTION_TEXT_KEY)
.noEventPropagation()
.publishIf(privilege, false);
final ProctoringGUIService proctoringGUIService = this.pageService
.getCurrentUser()
.getProctoringGUIService();
proctoringGUIService.roomNames().forEach(roomName -> {
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_VIEW_PROCTOR_ROOM)
.withEntityKey(entityKey)
.withExec(a -> showProctoringRoom(roomName, clientTable, a))
.withNameAttributes(roomName)
.noEventPropagation()
.publish();
actionBuilder.newAction(ActionDefinition.MONITOR_EXAM_CLOSE_PROCTOR_ROOM)
.withEntityKey(entityKey)
.withExec(a -> closeProctoringRoom(roomName, clientTable, a))
.withNameAttributes(roomName)
.publish();
});
}
}
// @formatter:off
private static final String OPEN_SINGEL_ROOM_SCRIPT =
"var existingWin = window.open('', '%s', 'height=800,width=1200,location=no,scrollbars=yes,status=no,menubar=yes,toolbar=yes,titlebar=yes');\n" +
"if(existingWin.location.href === 'about:blank'){\n" +
" existingWin.location.href = '%s/proctoring/%s';\n" +
" existingWin.focus();\n" +
"} else {\n" +
" existingWin.focus();\n" +
"}";
// @formatter:on
private PageAction closeProctoringRoom(
final String roomName,
final ClientConnectionTable clientTable,
final PageAction action) {
final ProctoringGUIService proctoringGUIService = this.pageService
.getCurrentUser()
.getProctoringGUIService();
proctoringGUIService.closeRoom(roomName);
return action;
}
private PageAction newProctoringRoom(
final ClientConnectionTable clientTable,
final PageAction action) {
final ProctoringGUIService proctoringGUIService = this.pageService
.getCurrentUser()
.getProctoringGUIService();
final String newRoomName = proctoringGUIService.createNewRoomName();
final Set<String> connectionTokens = clientTable.getConnectionTokens(
ClientConnection.getStatusPredicate(ConnectionStatus.ACTIVE),
true);
proctoringGUIService.registerNewProcotringRoom(
action.getEntityKey().modelId,
newRoomName,
connectionTokens);
this.pageService.pageActionBuilder(action.pageContext())
.newAction(ActionDefinition.MONITOR_EXAM_VIEW_PROCTOR_ROOM)
.withEntityKey(action.getEntityKey())
.withExec(a -> showProctoringRoom(newRoomName, clientTable, a))
.withNameAttributes(newRoomName)
.noEventPropagation()
.publish();
this.pageService.pageActionBuilder(action.pageContext())
.newAction(ActionDefinition.MONITOR_EXAM_CLOSE_PROCTOR_ROOM)
.withEntityKey(action.getEntityKey())
.withExec(a -> closeProctoringRoom(newRoomName, clientTable, a))
.withNameAttributes(newRoomName)
.publish();
return showProctoringRoom(newRoomName, clientTable, action);
}
private PageAction showProctoringRoom(
final String roomName,
final ClientConnectionTable clientTable,
final PageAction action) {
final SEBProctoringConnectionData proctoringConnectionData = this.pageService.getRestService()
.getBuilder(GetProctorRoomConnectionData.class)
.withURIVariable(API.PARAM_MODEL_ID, action.getEntityKey().modelId)
.withQueryParam(SEBProctoringConnectionData.ATTR_ROOM_NAME, roomName)
.call()
.getOrThrow();
RWT.getUISession().getHttpSession().setAttribute(
ProctoringServlet.SESSION_ATTR_PROCTORING_DATA,
proctoringConnectionData);
final String script = String.format(
OPEN_SINGEL_ROOM_SCRIPT,
roomName,
this.guiServiceInfo.getExternalServerURIBuilder().toUriString(),
roomName);
RWT.getClient()
.getService(JavaScriptExecutor.class)
.execute(script);
this.pageService.getCurrentUser()
.getProctoringGUIService()
.registerProctoringWindow(roomName);
return action;
}
private static Function<PageAction, PageAction> showStateViewAction(

View file

@ -29,7 +29,8 @@ public enum ActionCategory {
LOGS_USER_ACTIVITY_LIST(new LocTextKey("sebserver.userlogs.list.actions"), 1),
LOGS_SEB_CLIENT_LIST(new LocTextKey("sebserver.userlogs.list.actions"), 1),
VARIA(new LocTextKey("sebserver.overall.action.category.varia"), 0),
FILTER(new LocTextKey("sebserver.overall.action.category.filter"), 50);
FILTER(new LocTextKey("sebserver.exam.monitoring.action.category.filter"), 50),
PROCTORING(new LocTextKey("sebserver.exam.overall.action.category.proctoring"), 60);
public final LocTextKey title;
public final int slotPosition;

View file

@ -684,6 +684,22 @@ public enum ActionDefinition {
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
ActionCategory.FILTER),
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.SHOW,
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
ActionCategory.PROCTORING),
MONITOR_EXAM_CLOSE_PROCTOR_ROOM(
new LocTextKey("sebserver.monitoring.exam.action.closeroom"),
ImageIcon.DELETE,
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
ActionCategory.PROCTORING),
LOGS_USER_ACTIVITY_LIST(
new LocTextKey("sebserver.logs.activity.userlogs"),
PageStateDefinitionImpl.USER_ACTIVITY_LOGS),
@ -745,4 +761,8 @@ public enum ActionDefinition {
this.category = category;
}
public LocTextKey getTitle(final Object... args) {
return new LocTextKey(this.title.name, args);
}
}

View file

@ -81,7 +81,7 @@ public class ActionPane implements TemplateComposer {
final Tree treeForGroup = getTreeForGroup(actionTrees, parent, event.action.definition, true);
final TreeItem actionItem = ActionPane.this.widgetFactory.treeItemLocalized(
treeForGroup,
event.action.definition.title);
event.action.getTitle());
final Image image = event.active
? event.action.definition.icon.getImage(parent.getDisplay())
@ -114,6 +114,7 @@ public class ActionPane implements TemplateComposer {
continue;
}
final PageAction action = (PageAction) actionItem.getData(ACTION_EVENT_CALL_KEY);
final Image image = event.activation
? ad.icon.getImage(parent.getDisplay())
: ad.icon.getGreyedImage(parent.getDisplay());
@ -122,18 +123,21 @@ public class ActionPane implements TemplateComposer {
actionItem.setForeground(null);
} else {
actionItem.setForeground(new Color(parent.getDisplay(), new RGBA(150, 150, 150, 50)));
ActionPane.this.pageService.getPolyglotPageService().injectI18n(actionItem, ad.title);
ActionPane.this.pageService.getPolyglotPageService().injectI18n(
actionItem,
(action != null) ? action.getTitle() : ad.title);
}
}
if (event.decoration != null) {
final TreeItem actionItemToDecorate = findAction(actionTrees, parent, event.decoration._1);
final PageAction action = (PageAction) actionItemToDecorate.getData(ACTION_EVENT_CALL_KEY);
if (actionItemToDecorate != null && event.decoration._2 != null) {
actionItemToDecorate.setImage(0,
event.decoration._2.icon.getImage(parent.getDisplay()));
ActionPane.this.pageService.getPolyglotPageService().injectI18n(
actionItemToDecorate,
event.decoration._2.title);
(action != null) ? action.getTitle() : event.decoration._2.title);
}
}
});
@ -250,7 +254,7 @@ public class ActionPane implements TemplateComposer {
final PageAction switchAction = action.getSwitchAction();
if (switchAction != null) {
final PolyglotPageService polyglotPageService = this.pageService.getPolyglotPageService();
polyglotPageService.injectI18n(treeItem, switchAction.definition.title);
polyglotPageService.injectI18n(treeItem, switchAction.getTitle());
treeItem.setImage(switchAction.definition.icon.getImage(treeItem.getDisplay()));
treeItem.setData(ACTION_EVENT_CALL_KEY, switchAction);
}

View file

@ -454,6 +454,7 @@ public interface PageService {
private boolean fireActionEvent = true;
private boolean ignoreMoveAwayFromEdit = false;
private PageAction switchAction;
private Object[] titleArgs;
protected PageActionBuilder(final PageService pageService, final PageContext pageContext) {
this.pageService = pageService;
@ -486,7 +487,8 @@ public interface PageService {
exec,
fireActionEvent,
ignoreMoveAwayFromEdit,
switchAction);
switchAction,
titleArgs);
}
public PageActionBuilder publish() {
@ -520,6 +522,11 @@ public interface PageService {
return this;
}
public PageActionBuilder withNameAttributes(final Object... attributes) {
this.titleArgs = attributes;
return this;
}
public PageActionBuilder withSelectionSupplier(final Supplier<Set<EntityKey>> selectionSupplier) {
this.selectionSupplier = selectionSupplier;
return this;

View file

@ -43,6 +43,7 @@ public final class PageAction {
final boolean fireActionEvent;
final boolean ignoreMoveAwayFromEdit;
private PageAction switchAction;
final Object[] titleArgs;
final LocTextKey successMessage;
@ -56,7 +57,8 @@ public final class PageAction {
final Function<PageAction, PageAction> exec,
final boolean fireActionEvent,
final boolean ignoreMoveAwayFromEdit,
final PageAction switchAction) {
final PageAction switchAction,
final Object[] titleArgs) {
this.definition = definition;
this.confirm = confirm;
@ -68,6 +70,7 @@ public final class PageAction {
this.fireActionEvent = fireActionEvent;
this.ignoreMoveAwayFromEdit = ignoreMoveAwayFromEdit;
this.switchAction = switchAction;
this.titleArgs = titleArgs;
if (this.switchAction != null) {
this.switchAction.switchAction = this;
}
@ -91,6 +94,14 @@ public final class PageAction {
return Constants.EMPTY_NOTE;
}
public LocTextKey getTitle() {
if (this.titleArgs != null) {
return this.definition.getTitle(this.titleArgs);
} else {
return this.definition.title;
}
}
public PageAction getSwitchAction() {
return this.switchAction;
}
@ -277,7 +288,8 @@ public final class PageAction {
source.exec,
source.fireActionEvent,
source.ignoreMoveAwayFromEdit,
source.switchAction);
source.switchAction,
source.titleArgs);
}
}

View file

@ -0,0 +1,42 @@
/*
* Copyright (c) 2020 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.model.exam.SEBProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
@Lazy
@Component
@GuiProfile
public class GetProctorRoomConnectionData extends RestCall<SEBProctoringConnectionData> {
public GetProctorRoomConnectionData() {
super(new TypeKey<>(
CallType.GET_SINGLE,
EntityType.EXAM_PROCTOR_DATA,
new TypeReference<SEBProctoringConnectionData>() {
}),
HttpMethod.GET,
MediaType.APPLICATION_FORM_URLENCODED,
API.EXAM_MONITORING_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT
+ API.PROCTOR_PATH_SEGMENT);
}
}

View file

@ -8,6 +8,8 @@
package ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session;
import java.util.List;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
@ -17,27 +19,27 @@ 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.exam.SEBClientProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
@Lazy
@Component
@GuiProfile
public class GetProctorDataForSEBClient extends RestCall<SEBClientProctoringConnectionData> {
public class SEBClientsJoinProctorRoom extends RestCall<List<SEBProctoringConnectionData>> {
public GetProctorDataForSEBClient() {
public SEBClientsJoinProctorRoom() {
super(new TypeKey<>(
CallType.GET_SINGLE,
EntityType.EXAM_PROCTOR_DATA,
new TypeReference<SEBClientProctoringConnectionData>() {
new TypeReference<List<SEBProctoringConnectionData>>() {
}),
HttpMethod.GET,
HttpMethod.POST,
MediaType.APPLICATION_FORM_URLENCODED,
API.EXAM_MONITORING_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT
+ API.PROCTOR_PATH_SEGMENT
+ API.EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT);
+ API.PROCTOR_JOIN_ROOM_PATH_SEGMENT);
}
}

View file

@ -0,0 +1,44 @@
/*
* Copyright (c) 2020 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.List;
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.exam.SEBProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
@Lazy
@Component
@GuiProfile
public class SEBClientsLeaveProctorRoom extends RestCall<List<SEBProctoringConnectionData>> {
public SEBClientsLeaveProctorRoom() {
super(new TypeKey<>(
CallType.GET_SINGLE,
EntityType.EXAM_PROCTOR_DATA,
new TypeReference<List<SEBProctoringConnectionData>>() {
}),
HttpMethod.POST,
MediaType.APPLICATION_FORM_URLENCODED,
API.EXAM_MONITORING_ENDPOINT
+ API.MODEL_ID_VAR_PATH_SEGMENT
+ API.PROCTOR_PATH_SEGMENT
+ API.PROCTOR_LEAVE_ROOM_PATH_SEGMENT);
}
}

View file

@ -34,6 +34,8 @@ import ch.ethz.seb.sebserver.gbl.model.GrantEntity;
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;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.session.ProctoringGUIService;
@Component
@GuiProfile
@ -46,10 +48,19 @@ public class CurrentUser {
private SEBServerAuthorizationContext authContext = null;
private Map<RoleTypeKey, Privilege> privileges = null;
private final Map<String, String> attributes;
private final ProctoringGUIService proctoringGUIService;
public CurrentUser(
final AuthorizationContextHolder authorizationContextHolder,
final RestService restService) {
public CurrentUser(final AuthorizationContextHolder authorizationContextHolder) {
this.authorizationContextHolder = authorizationContextHolder;
this.attributes = new HashMap<>();
this.proctoringGUIService = new ProctoringGUIService(restService);
}
public ProctoringGUIService getProctoringGUIService() {
return this.proctoringGUIService;
}
public void putAttribute(final String name, final String value) {
@ -179,6 +190,7 @@ public class CurrentUser {
this.attributes.clear();
}
this.proctoringGUIService.clear();
this.privileges = null;
if (isAvailable()) {

View file

@ -0,0 +1,189 @@
/*
* Copyright (c) 2020 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.session;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import org.apache.tomcat.util.buf.StringUtils;
import org.eclipse.rap.rwt.RWT;
import org.eclipse.rap.rwt.client.service.JavaScriptExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.SEBClientsJoinProctorRoom;
public class ProctoringGUIService {
private static final Logger log = LoggerFactory.getLogger(ProctoringGUIService.class);
private static final String CLOSE_ROOM_SCRIPT = "var existingWin = window.open('', '%s'); existingWin.close()";
private final AtomicInteger counter = new AtomicInteger(1);
private final RestService restService;
final Set<String> openWindows = new HashSet<>();
final Map<String, RoomConnectionData> rooms = new HashMap<>();
public ProctoringGUIService(final RestService restService) {
this.restService = restService;
}
public String createNewRoomName() {
return "Room-" + this.counter.getAndIncrement();
}
public void registerProctoringWindow(final String window) {
this.openWindows.add(window);
}
public Set<String> roomNames() {
return this.rooms.keySet();
}
public void registerNewProcotringRoom(
final String examId,
final String roomName,
final Collection<String> connectionTokens) {
final List<SEBProctoringConnectionData> connections =
this.restService.getBuilder(SEBClientsJoinProctorRoom.class)
.withURIVariable(API.PARAM_MODEL_ID, examId)
.withFormParam(SEBProctoringConnectionData.ATTR_ROOM_NAME, roomName)
.withFormParam(
API.EXAM_API_SEB_CONNECTION_TOKEN,
StringUtils.join(connectionTokens, Constants.LIST_SEPARATOR_CHAR))
.call()
.getOrThrow();
this.rooms.put(roomName, new RoomConnectionData(roomName, examId, connections));
this.openWindows.add(roomName);
}
public void addConnectionsToRoom(
final String examId,
final String room,
final Collection<String> connectionTokens) {
if (this.rooms.containsKey(room)) {
final RoomConnectionData roomConnectionData = this.rooms.get(room);
if (!roomConnectionData.examId.equals(examId) || !roomConnectionData.roomName.equals(room)) {
throw new IllegalArgumentException("Exam identifier mismatch");
}
final List<SEBProctoringConnectionData> newConnections =
this.restService.getBuilder(SEBClientsJoinProctorRoom.class)
.withURIVariable(API.PARAM_MODEL_ID, examId)
.withFormParam(SEBProctoringConnectionData.ATTR_ROOM_NAME, room)
.withFormParam(
API.EXAM_API_SEB_CONNECTION_TOKEN,
StringUtils.join(connectionTokens, Constants.LIST_SEPARATOR_CHAR))
.call()
.getOrThrow();
roomConnectionData.connections.addAll(newConnections);
}
}
public void removeConnectionsFromRoom(
final String examId,
final String room,
final Collection<String> connectionTokens) {
}
public Result<List<SEBProctoringConnectionData>> closeRoom(final String name) {
return Result.tryCatch(() -> {
closeWindow(name);
final RoomConnectionData roomConnectionData = this.rooms.remove(name);
if (roomConnectionData != null) {
// first send instruction to leave this room and join the personal single room
final String connectionsString = StringUtils.join(
roomConnectionData.connections
.stream()
.map(c -> c.connectionToken)
.collect(Collectors.toList()),
Constants.LIST_SEPARATOR_CHAR);
// NOTE: uncomment this if we need to send first a LEAVE instruction before sending the JOIN instruction for the single room
// this.restService.getBuilder(LeaveProctorRoom.class)
// .withURIVariable(API.PARAM_MODEL_ID, roomConnectionData.examId)
// .withFormParam(SEBProctoringConnectionData.ATTR_ROOM_NAME, name)
// .withFormParam(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionsString)
// .call()
// .getOrThrow();
return this.restService.getBuilder(SEBClientsJoinProctorRoom.class)
.withURIVariable(API.PARAM_MODEL_ID, roomConnectionData.examId)
.withFormParam(SEBProctoringConnectionData.ATTR_ROOM_NAME, name)
.withFormParam(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionsString)
.call()
.getOrThrow();
}
return Collections.emptyList();
});
}
public void clear() {
if (!this.rooms.isEmpty()) {
this.rooms.keySet().stream().forEach(this::closeRoom);
this.rooms.clear();
}
if (!this.openWindows.isEmpty()) {
this.openWindows.stream()
.forEach(room -> closeWindow(room));
this.openWindows.clear();
}
}
private void closeWindow(final String room) {
try {
RWT.getClient().getService(JavaScriptExecutor.class)
.execute(String.format(CLOSE_ROOM_SCRIPT, room));
} catch (final Exception e) {
log.info("Failed to close opened proctoring window: {}", room);
}
}
private static final class RoomConnectionData {
final String roomName;
final String examId;
final Collection<SEBProctoringConnectionData> connections;
protected RoomConnectionData(
final String roomName,
final String examId,
final Collection<SEBProctoringConnectionData> connections) {
this.roomName = roomName;
this.examId = examId;
this.connections = connections != null ? new ArrayList<>(connections) : new ArrayList<>();
}
}
}

View file

@ -10,8 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.exam;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings.ServerType;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBClientProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.util.Result;
public interface ExamProctoringService {
@ -20,19 +19,21 @@ public interface ExamProctoringService {
Result<Boolean> testExamProctoring(final ProctoringSettings examProctoring);
public Result<SEBClientProctoringConnectionData> createProctoringConnectionData(
Result<SEBProctoringConnectionData> createProctorPrivateRoomConnection(
final ProctoringSettings examProctoring,
final String connectionToken);
Result<SEBProctoringConnectionData> createProctorPublicRoomConnection(
final ProctoringSettings examProctoring,
final String roomName);
Result<SEBProctoringConnectionData> createClientPrivateRoomConnection(
final ProctoringSettings examProctoring,
final String connectionToken);
Result<SEBProctoringConnectionData> createClientPublicRoomConnection(
final ProctoringSettings examProctoring,
final String connectionToken,
final boolean server);
Result<SEBClientProctoringConnectionData> createProctoringConnectionData(
final ProctoringSettings examProctoring,
ClientConnection clientConnection,
boolean server);
Result<SEBClientProctoringConnectionData> createProcotringDataForRoom(
final ProctoringSettings examProctoring,
final String roomName,
final boolean server);
final String roomName);
}

View file

@ -25,8 +25,8 @@ import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings.ServerType;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBClientProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Cryptor;
import ch.ethz.seb.sebserver.gbl.util.Result;
@ -72,64 +72,101 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
}
@Override
public Result<SEBClientProctoringConnectionData> createProctoringConnectionData(
public Result<SEBProctoringConnectionData> createProctorPrivateRoomConnection(
final ProctoringSettings examProctoring,
final String connectionToken,
final boolean server) {
final String connectionToken) {
return Result.tryCatch(() -> {
return createProctoringConnectionData(
examProctoring,
this.examSessionService
.getConnectionData(connectionToken)
.getOrThrow().clientConnection,
server)
.getOrThrow();
});
}
@Override
public Result<SEBClientProctoringConnectionData> createProctoringConnectionData(
final ProctoringSettings examProctoring,
final ClientConnection clientConnection,
final boolean server) {
return Result.tryCatch(() -> {
final ClientConnectionData clientConnection = this.examSessionService.getConnectionData(connectionToken)
.getOrThrow();
final long expTime = forExam(examProctoring);
final Encoder urlEncoder = Base64.getUrlEncoder().withoutPadding();
final String roomName = urlEncoder.encodeToString(
Utils.toByteArray(clientConnection.connectionToken));
Utils.toByteArray(clientConnection.clientConnection.connectionToken));
return createProctoringConnectionData(
connectionToken,
examProctoring.serverURL,
examProctoring.appKey,
examProctoring.getAppSecret(),
this.authorizationService.getUserService().getCurrentUser().getUsername(),
(server) ? "seb-server" : "seb-client",
"seb-server",
roomName,
clientConnection.userSessionId,
clientConnection.clientConnection.userSessionId,
expTime)
.getOrThrow();
});
}
@Override
public Result<SEBClientProctoringConnectionData> createProcotringDataForRoom(
public Result<SEBProctoringConnectionData> createProctorPublicRoomConnection(
final ProctoringSettings examProctoring,
final String roomName,
final boolean server) {
final String roomName) {
return Result.tryCatch(() -> {
return createProctoringConnectionData(
null,
examProctoring.serverURL,
examProctoring.appKey,
examProctoring.getAppSecret(),
this.authorizationService.getUserService().getCurrentUser().getUsername(),
"seb-server",
roomName,
roomName,
forExam(examProctoring))
.getOrThrow();
});
}
@Override
public Result<SEBProctoringConnectionData> createClientPrivateRoomConnection(
final ProctoringSettings examProctoring,
final String connectionToken) {
// TODO Auto-generated method stub
return Result.tryCatch(() -> {
final ClientConnectionData clientConnection = this.examSessionService.getConnectionData(connectionToken)
.getOrThrow();
final Encoder urlEncoder = Base64.getUrlEncoder().withoutPadding();
final String roomName = urlEncoder.encodeToString(
Utils.toByteArray(clientConnection.clientConnection.connectionToken));
return createProctoringConnectionData(
null,
examProctoring.serverURL,
examProctoring.appKey,
examProctoring.getAppSecret(),
clientConnection.clientConnection.userSessionId,
"seb-server",
roomName,
clientConnection.clientConnection.userSessionId,
forExam(examProctoring))
.getOrThrow();
});
}
@Override
public Result<SEBProctoringConnectionData> createClientPublicRoomConnection(
final ProctoringSettings examProctoring,
final String connectionToken,
final String roomName) {
return Result.tryCatch(() -> {
final long expTime = forExam(examProctoring);
final ClientConnectionData connectionData = this.examSessionService.getConnectionData(connectionToken)
.getOrThrow();
return createProctoringConnectionData(
connectionToken,
examProctoring.serverURL,
examProctoring.appKey,
examProctoring.getAppSecret(),
this.authorizationService.getUserService().getCurrentUser().getUsername(),
(server) ? "seb-server" : "seb-client",
connectionData.clientConnection.userSessionId,
"seb-client",
roomName,
roomName,
expTime)
@ -138,7 +175,8 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
}
public Result<SEBClientProctoringConnectionData> createProctoringConnectionData(
public Result<SEBProctoringConnectionData> createProctoringConnectionData(
final String connectionToken,
final String url,
final String appKey,
final CharSequence appSecret,
@ -165,18 +203,13 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
expTime,
host);
final StringBuilder builder = new StringBuilder();
final String connectionURL = builder.append(roomUrl)
.append("?jwt=")
.append(token).toString();
return new SEBClientProctoringConnectionData(
return new SEBProctoringConnectionData(
connectionToken,
host,
roomUrl,
roomName,
subject,
token,
connectionURL);
token);
});
}

View file

@ -28,7 +28,7 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam.ExamType;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings.ServerType;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBClientProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
@ -360,12 +360,11 @@ public class SEBClientConnectionServiceImpl implements SEBClientConnectionServic
.getExamProctoring(clientConnection.examId)
.getOrThrow();
final SEBClientProctoringConnectionData proctoringData =
final SEBProctoringConnectionData proctoringData =
this.examAdminService.getExamProctoringService(proctoringSettings.serverType)
.flatMap(s -> s.createProctoringConnectionData(
.flatMap(s -> s.createClientPrivateRoomConnection(
proctoringSettings,
clientConnection.connectionToken,
false))
clientConnection.connectionToken))
.getOrThrow();
final Map<String, String> attributes = new HashMap<>();

View file

@ -234,6 +234,9 @@ public class SEBInstructionServiceImpl implements SEBInstructionService {
}
private ClientInstructionRecord chacheInstruction(final ClientInstructionRecord instruction) {
System.out.println("************* register instruction: " + instruction);
final String connectionToken = instruction.getConnectionToken();
if (this.instructions.containsKey(connectionToken)) {
// check if previous instruction is still valid

View file

@ -11,11 +11,13 @@ package ch.ethz.seb.sebserver.webservice.weblayer.api;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
@ -43,7 +45,7 @@ import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.Page;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBClientProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection.ConnectionStatus;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction;
@ -281,32 +283,49 @@ public class ExamMonitoringController {
}
//***********************************************************************************************
//**** Proctoring
@RequestMapping(
path = API.MODEL_ID_VAR_PATH_SEGMENT
+ API.PROCTOR_PATH_SEGMENT
+ API.EXAM_MONITORING_SEB_CONNECTION_TOKEN_PATH_SEGMENT,
+ API.PROCTOR_PATH_SEGMENT,
method = RequestMethod.GET,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public SEBClientProctoringConnectionData getClientSingleRoomProctoringData(
public SEBProctoringConnectionData getProctorSingleRoom(
@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,
@PathVariable(name = API.EXAM_API_SEB_CONNECTION_TOKEN) final String connectionToken) {
@RequestParam(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = false) final String connectionToken,
@RequestParam(name = SEBProctoringConnectionData.ATTR_ROOM_NAME, required = false) final String roomName) {
this.authorization.check(
PrivilegeType.READ,
EntityType.EXAM,
institutionId);
return this.examSessionService.getRunningExam(examId)
.flatMap(this.authorization::checkRead)
.flatMap(this.examAdminService::getExamProctoring)
.flatMap(proc -> this.examAdminService
.getExamProctoringService(proc.serverType)
.flatMap(s -> s.createProctoringConnectionData(proc, connectionToken, true)))
.getOrThrow();
if (StringUtils.isNotBlank(connectionToken)) {
return this.examSessionService.getRunningExam(examId)
.flatMap(this.authorization::checkRead)
.flatMap(this.examAdminService::getExamProctoring)
.flatMap(proc -> this.examAdminService
.getExamProctoringService(proc.serverType)
.flatMap(s -> s.createProctorPrivateRoomConnection(proc, connectionToken)))
.getOrThrow();
} else if (StringUtils.isNotBlank(roomName)) {
return this.examSessionService.getRunningExam(examId)
.flatMap(this.authorization::checkRead)
.flatMap(this.examAdminService::getExamProctoring)
.flatMap(proc -> this.examAdminService
.getExamProctoringService(proc.serverType)
.flatMap(s -> s.createProctorPublicRoomConnection(proc, roomName)))
.getOrThrow();
} else {
return null;
}
}
@RequestMapping(
@ -315,62 +334,14 @@ public class ExamMonitoringController {
+ API.PROCTOR_JOIN_ROOM_PATH_SEGMENT,
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public SEBClientProctoringConnectionData joinProctoringRoom(
public List<SEBProctoringConnectionData> joinProctoringRoom(
@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,
@RequestParam(
name = SEBClientProctoringConnectionData.ATTR_ROOM_NAME,
required = true) final String roomName,
@RequestParam(
name = API.EXAM_API_SEB_CONNECTION_TOKEN,
required = true) final String connectionTokens) {
this.authorization.check(
PrivilegeType.READ,
EntityType.EXAM,
institutionId);
final ProctoringSettings settings = this.examSessionService
.getRunningExam(examId)
.flatMap(this.authorization::checkRead)
.flatMap(this.examAdminService::getExamProctoring)
.getOrThrow();
final SEBClientProctoringConnectionData result = this.examAdminService
.getExamProctoringService(settings.serverType)
.flatMap(s -> s.createProcotringDataForRoom(settings, roomName, false))
.getOrThrow();
if (StringUtils.isNotBlank(connectionTokens)) {
(connectionTokens.contains(Constants.LIST_SEPARATOR)
? Arrays.asList(StringUtils.split(connectionTokens, Constants.LIST_SEPARATOR))
: Arrays.asList(connectionTokens)).stream()
.forEach(connectionToken -> sendJoinInstruction(examId, connectionToken, result)
.onError(error -> log.error(
"Failed to send proctoring leave instruction to client: {} ",
connectionToken, error)));
}
return result;
}
@RequestMapping(
path = API.MODEL_ID_VAR_PATH_SEGMENT
+ API.PROCTOR_PATH_SEGMENT
+ API.PROCTOR_LEAVE_ROOM_PATH_SEGMENT,
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public void leaveProctoringRoom(
@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,
@RequestParam(
name = SEBClientProctoringConnectionData.ATTR_ROOM_NAME,
name = SEBProctoringConnectionData.ATTR_ROOM_NAME,
required = true) final String roomName,
@RequestParam(
name = API.EXAM_API_SEB_CONNECTION_TOKEN,
@ -391,17 +362,85 @@ public class ExamMonitoringController {
.getExamProctoringService(settings.serverType)
.getOrThrow();
(connectionTokens.contains(Constants.LIST_SEPARATOR)
? Arrays.asList(StringUtils.split(connectionTokens, Constants.LIST_SEPARATOR))
: Arrays.asList(connectionTokens)).stream()
.forEach(connectionToken -> examProctoringService
.createProctoringConnectionData(settings, connectionToken, false)
.flatMap(data -> sendLeaveInstruction(examId, connectionToken, data))
.onError(error -> log.error(
"Failed to send proctoring leave instruction to client: {} ",
connectionToken, error)));
if (StringUtils.isNotBlank(connectionTokens)) {
return (connectionTokens.contains(Constants.LIST_SEPARATOR)
? Arrays.asList(StringUtils.split(connectionTokens, Constants.LIST_SEPARATOR))
: Arrays.asList(connectionTokens))
.stream()
.map(connectionToken -> {
final SEBProctoringConnectionData data = examProctoringService
.createClientPublicRoomConnection(settings, connectionToken, roomName)
.getOrThrow();
sendJoinInstruction(examId, connectionToken, data)
.onError(error -> log.error(
"Failed to send proctoring leave instruction to client: {} ",
connectionToken, error));
return data;
}).collect(Collectors.toList());
}
return Collections.emptyList();
}
@RequestMapping(
path = API.MODEL_ID_VAR_PATH_SEGMENT
+ API.PROCTOR_PATH_SEGMENT
+ API.PROCTOR_LEAVE_ROOM_PATH_SEGMENT,
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public List<SEBProctoringConnectionData> leaveProctoringRoom(
@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,
@RequestParam(
name = SEBProctoringConnectionData.ATTR_ROOM_NAME,
required = true) final String roomName,
@RequestParam(
name = API.EXAM_API_SEB_CONNECTION_TOKEN,
required = true) final String connectionTokens) {
this.authorization.check(
PrivilegeType.READ,
EntityType.EXAM,
institutionId);
final ProctoringSettings settings = this.examSessionService
.getRunningExam(examId)
.flatMap(this.authorization::checkRead)
.flatMap(this.examAdminService::getExamProctoring)
.getOrThrow();
final ExamProctoringService examProctoringService = this.examAdminService
.getExamProctoringService(settings.serverType)
.getOrThrow();
if (StringUtils.isNotBlank(connectionTokens)) {
return (connectionTokens.contains(Constants.LIST_SEPARATOR)
? Arrays.asList(StringUtils.split(connectionTokens, Constants.LIST_SEPARATOR))
: Arrays.asList(connectionTokens))
.stream()
.map(connectionToken -> {
final SEBProctoringConnectionData data = examProctoringService
.createClientPublicRoomConnection(settings, connectionToken, roomName)
.getOrThrow();
sendLeaveInstruction(examId, connectionTokens, data)
.onError(error -> log.error(
"Failed to send proctoring leave instruction for common room to client: {} ",
connectionToken, error));
return data;
}).collect(Collectors.toList());
}
return Collections.emptyList();
}
//**** Proctoring
//***********************************************************************************************
private boolean hasRunningExamPrivilege(final Long examId, final Long institution) {
return hasRunningExamPrivilege(
this.examSessionService.getRunningExam(examId).getOr(null),
@ -420,7 +459,7 @@ public class ExamMonitoringController {
private Result<Void> sendJoinInstruction(
final Long examId,
final String connectionToken,
final SEBClientProctoringConnectionData data) {
final SEBProctoringConnectionData data) {
return sendProctorInstruction(
examId,
@ -432,7 +471,7 @@ public class ExamMonitoringController {
private Result<Void> sendLeaveInstruction(
final Long examId,
final String connectionToken,
final SEBClientProctoringConnectionData data) {
final SEBProctoringConnectionData data) {
return sendProctorInstruction(
examId,
@ -444,7 +483,7 @@ public class ExamMonitoringController {
private Result<Void> sendProctorInstruction(
final Long examId,
final String connectionToken,
final SEBClientProctoringConnectionData data,
final SEBProctoringConnectionData data,
final String method) {
final Map<String, String> attributes = new HashMap<>();
attributes.put(

View file

@ -1428,6 +1428,11 @@ 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.newroom=New Proctor Room
sebserver.monitoring.exam.action.viewroom=View Room {0}
sebserver.monitoring.exam.action.closeroom=Close Room {0}
sebserver.exam.monitoring.action.category.filter=Filter
sebserver.exam.overall.action.category.proctoring=Proctoring
sebserver.monitoring.exam.info.pleaseSelect=At first please select an Exam from the list

View file

@ -24,7 +24,7 @@ public class CurrentUserTest extends GuiIntegrationTest {
public void testCurrentUserLoginAndGet() {
final OAuth2AuthorizationContextHolder authorizationContextHolder = getAuthorizationContextHolder();
final CurrentUser currentUser = new CurrentUser(authorizationContextHolder);
final CurrentUser currentUser = new CurrentUser(authorizationContextHolder, null);
// no user is logged in for now
try {
@ -51,7 +51,7 @@ public class CurrentUserTest extends GuiIntegrationTest {
public void testCurrentUserPrivileges() {
final OAuth2AuthorizationContextHolder authorizationContextHolder = getAuthorizationContextHolder();
final CurrentUser currentUser = new CurrentUser(authorizationContextHolder);
final CurrentUser currentUser = new CurrentUser(authorizationContextHolder, null);
// login as SEB Administrator
authorizationContextHolder.getAuthorizationContext().login("admin", "admin");
@ -64,7 +64,7 @@ public class CurrentUserTest extends GuiIntegrationTest {
@Test
public void testCurrentUserLogin() {
final OAuth2AuthorizationContextHolder authorizationContextHolder = login("admin", "admin");
final CurrentUser currentUser = new CurrentUser(authorizationContextHolder);
final CurrentUser currentUser = new CurrentUser(authorizationContextHolder, null);
final UserInfo userInfo = currentUser.getOrHandleError(error -> {
fail("expecting no error here");
return null;

View file

@ -14,7 +14,7 @@ import static org.junit.Assert.assertNotNull;
import org.junit.Test;
import org.mockito.Mockito;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBClientProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.util.Cryptor;
public class ExamJITSIProctoringServiceTest {
@ -25,7 +25,8 @@ public class ExamJITSIProctoringServiceTest {
Mockito.when(cryptorMock.decrypt(Mockito.any())).thenReturn("fbvgeghergrgrthrehreg123");
final ExamJITSIProctoringService examJITSIProctoringService =
new ExamJITSIProctoringService(null, null, cryptorMock);
final SEBClientProctoringConnectionData data = examJITSIProctoringService.createProctoringConnectionData(
final SEBProctoringConnectionData data = examJITSIProctoringService.createProctoringConnectionData(
"connectionToken",
"https://seb-jitsi.example.ch",
"test-app",
"fbvgeghergrgrthrehreg123",
@ -43,9 +44,6 @@ public class ExamJITSIProctoringServiceTest {
assertEquals(
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb250ZXh0Ijp7InVzZXIiOnsibmFtZSI6IlRlc3QgTmFtZSJ9fSwiaXNzIjoidGVzdC1hcHAiLCJhdWQiOiJ0ZXN0LWNsaWVudCIsInN1YiI6InNlYi1qaXRzaS5leGFtcGxlLmNoIiwicm9vbSI6IlNvbWVSb29tIiwiZXhwIjoxNjA5NDU5MjAwfQ.4ovqUkG6jrLvkDEZNdhbtFI_DFLDFsM2eBJHhcYq7a4",
data.accessToken);
assertEquals(
"https://seb-jitsi.example.ch/SomeRoom?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb250ZXh0Ijp7InVzZXIiOnsibmFtZSI6IlRlc3QgTmFtZSJ9fSwiaXNzIjoidGVzdC1hcHAiLCJhdWQiOiJ0ZXN0LWNsaWVudCIsInN1YiI6InNlYi1qaXRzaS5leGFtcGxlLmNoIiwicm9vbSI6IlNvbWVSb29tIiwiZXhwIjoxNjA5NDU5MjAwfQ.4ovqUkG6jrLvkDEZNdhbtFI_DFLDFsM2eBJHhcYq7a4",
data.connectionURL);
}