SEBSERV-335, SEBSERV-363 GUI improvements
This commit is contained in:
parent
bf32a713ea
commit
edce7275ca
27 changed files with 1038 additions and 394 deletions
|
@ -158,6 +158,7 @@ public final class API {
|
|||
public static final String EXAM_ADMINISTRATION_CHECK_IMPORTED_PATH_SEGMENT = "/check-imported";
|
||||
public static final String EXAM_ADMINISTRATION_SEB_RESTRICTION_CHAPTERS_PATH_SEGMENT = "/chapters";
|
||||
public static final String EXAM_ADMINISTRATION_PROCTORING_PATH_SEGMENT = "/proctoring";
|
||||
public static final String EXAM_ADMINISTRATION_PROCTORING_RESET_PATH_SEGMENT = "reset";
|
||||
public static final String EXAM_ADMINISTRATION_SEB_SECURITY_KEY_GRANTS_PATH_SEGMENT = "/grant";
|
||||
public static final String EXAM_ADMINISTRATION_SEB_SECURITY_KEY_INFO_PATH_SEGMENT = "/sebkeyinfo";
|
||||
|
||||
|
|
|
@ -42,10 +42,18 @@ public class ProctoringServiceSettings implements Entity {
|
|||
public static final String ATTR_ENABLE_PROCTORING = "enableProctoring";
|
||||
public static final String ATTR_SERVER_TYPE = "serverType";
|
||||
public static final String ATTR_SERVER_URL = "serverURL";
|
||||
|
||||
// Jitsi access (former also Zoom)
|
||||
public static final String ATTR_APP_KEY = "appKey";
|
||||
public static final String ATTR_APP_SECRET = "appSecret";
|
||||
|
||||
// Zoom Access
|
||||
public static final String ATTR_ACCOUNT_ID = "accountId";
|
||||
public static final String ATTR_ACCOUNT_CLIENT_ID = "clientId";
|
||||
public static final String ATTR_ACCOUNT_CLIENT_SECRET = "clientSecret";
|
||||
public static final String ATTR_SDK_KEY = "sdkKey";
|
||||
public static final String ATTR_SDK_SECRET = "sdkSecret";
|
||||
|
||||
public static final String ATTR_COLLECTING_ROOM_SIZE = "collectingRoomSize";
|
||||
public static final String ATTR_ENABLED_FEATURES = "enabledFeatures";
|
||||
public static final String ATTR_COLLECT_ALL_ROOM_NAME = "collectAllRoomName";
|
||||
|
@ -71,6 +79,15 @@ public class ProctoringServiceSettings implements Entity {
|
|||
@JsonProperty(ATTR_APP_SECRET)
|
||||
public final CharSequence appSecret;
|
||||
|
||||
@JsonProperty(ATTR_ACCOUNT_ID)
|
||||
public final String accountId;
|
||||
|
||||
@JsonProperty(ATTR_ACCOUNT_CLIENT_ID)
|
||||
public final String clientId;
|
||||
|
||||
@JsonProperty(ATTR_ACCOUNT_CLIENT_SECRET)
|
||||
public final CharSequence clientSecret;
|
||||
|
||||
@JsonProperty(ATTR_SDK_KEY)
|
||||
public final String sdkKey;
|
||||
|
||||
|
@ -100,6 +117,9 @@ public class ProctoringServiceSettings implements Entity {
|
|||
@JsonProperty(ATTR_SERVICE_IN_USE) final Boolean serviceInUse,
|
||||
@JsonProperty(ATTR_APP_KEY) final String appKey,
|
||||
@JsonProperty(ATTR_APP_SECRET) final CharSequence appSecret,
|
||||
@JsonProperty(ATTR_ACCOUNT_ID) final String accountId,
|
||||
@JsonProperty(ATTR_ACCOUNT_CLIENT_ID) final String clientId,
|
||||
@JsonProperty(ATTR_ACCOUNT_CLIENT_SECRET) final CharSequence clientSecret,
|
||||
@JsonProperty(ATTR_SDK_KEY) final String sdkKey,
|
||||
@JsonProperty(ATTR_SDK_SECRET) final CharSequence sdkSecret,
|
||||
@JsonProperty(ATTR_USE_ZOOM_APP_CLIENT_COLLECTING_ROOM) final Boolean useZoomAppClientForCollectingRoom) {
|
||||
|
@ -113,11 +133,50 @@ public class ProctoringServiceSettings implements Entity {
|
|||
this.serviceInUse = serviceInUse;
|
||||
this.appKey = StringUtils.trim(appKey);
|
||||
this.appSecret = appSecret;
|
||||
this.accountId = StringUtils.trim(accountId);
|
||||
this.clientId = clientId;
|
||||
this.clientSecret = clientSecret;
|
||||
this.sdkKey = StringUtils.trim(sdkKey);
|
||||
this.sdkSecret = sdkSecret;
|
||||
this.useZoomAppClientForCollectingRoom = BooleanUtils.toBoolean(useZoomAppClientForCollectingRoom);
|
||||
}
|
||||
|
||||
public ProctoringServiceSettings(final Long examId) {
|
||||
this.examId = examId;
|
||||
this.enableProctoring = false;
|
||||
this.serverType = null;
|
||||
this.serverURL = null;
|
||||
this.collectingRoomSize = 20;
|
||||
this.enabledFeatures = EnumSet.allOf(ProctoringFeature.class);
|
||||
this.serviceInUse = false;
|
||||
this.appKey = null;
|
||||
this.appSecret = null;
|
||||
this.accountId = null;
|
||||
this.clientId = null;
|
||||
this.clientSecret = null;
|
||||
this.sdkKey = null;
|
||||
this.sdkSecret = null;
|
||||
this.useZoomAppClientForCollectingRoom = false;
|
||||
}
|
||||
|
||||
public ProctoringServiceSettings(final Long examId, final ProctoringServiceSettings copyOf) {
|
||||
this.examId = examId;
|
||||
this.enableProctoring = copyOf.enableProctoring;
|
||||
this.serverType = copyOf.serverType;
|
||||
this.serverURL = copyOf.serverURL;
|
||||
this.collectingRoomSize = copyOf.collectingRoomSize;
|
||||
this.enabledFeatures = copyOf.enabledFeatures;
|
||||
this.serviceInUse = false;
|
||||
this.appKey = copyOf.appKey;
|
||||
this.appSecret = copyOf.appSecret;
|
||||
this.accountId = copyOf.accountId;
|
||||
this.clientId = copyOf.clientId;
|
||||
this.clientSecret = copyOf.clientSecret;
|
||||
this.sdkKey = copyOf.sdkKey;
|
||||
this.sdkSecret = copyOf.sdkSecret;
|
||||
this.useZoomAppClientForCollectingRoom = copyOf.useZoomAppClientForCollectingRoom;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModelId() {
|
||||
return (this.examId != null) ? String.valueOf(this.examId) : null;
|
||||
|
@ -165,6 +224,18 @@ public class ProctoringServiceSettings implements Entity {
|
|||
return this.appSecret;
|
||||
}
|
||||
|
||||
public String getAccountId() {
|
||||
return this.accountId;
|
||||
}
|
||||
|
||||
public String getClientId() {
|
||||
return this.clientId;
|
||||
}
|
||||
|
||||
public CharSequence getClientSecret() {
|
||||
return this.clientSecret;
|
||||
}
|
||||
|
||||
public String getSdkKey() {
|
||||
return this.sdkKey;
|
||||
}
|
||||
|
@ -222,18 +293,20 @@ public class ProctoringServiceSettings implements Entity {
|
|||
builder.append(this.serverURL);
|
||||
builder.append(", appKey=");
|
||||
builder.append(this.appKey);
|
||||
builder.append(", appSecret=");
|
||||
builder.append("--");
|
||||
builder.append(", accountId=");
|
||||
builder.append(this.accountId);
|
||||
builder.append(", clientId=");
|
||||
builder.append(this.clientId);
|
||||
builder.append(", sdkKey=");
|
||||
builder.append(this.sdkKey);
|
||||
builder.append(", sdkSecret=");
|
||||
builder.append("--");
|
||||
builder.append(", collectingRoomSize=");
|
||||
builder.append(this.collectingRoomSize);
|
||||
builder.append(", enabledFeatures=");
|
||||
builder.append(this.enabledFeatures);
|
||||
builder.append(", serviceInUse=");
|
||||
builder.append(this.serviceInUse);
|
||||
builder.append(", useZoomAppClientForCollectingRoom=");
|
||||
builder.append(this.useZoomAppClientForCollectingRoom);
|
||||
builder.append("]");
|
||||
return builder.toString();
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import java.util.Objects;
|
|||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
import org.apache.tomcat.util.buf.StringUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
@ -68,7 +68,7 @@ public class AppSignatureKeyInfo implements ModelIdAware {
|
|||
|
||||
@Override
|
||||
public String getModelId() {
|
||||
return this.key;
|
||||
return StringUtils.isNoneBlank(this.key) ? this.key : "-1";
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
|
|
|
@ -18,6 +18,7 @@ public enum ActionCategory {
|
|||
LMS_SETUP_LIST(new LocTextKey("sebserver.lmssetup.list.actions"), 1),
|
||||
QUIZ_LIST(new LocTextKey("sebserver.quizdiscovery.list.actions"), 1),
|
||||
EXAM_LIST(new LocTextKey("sebserver.exam.list.actions"), 1),
|
||||
EXAM_SECURITY(new LocTextKey("sebserver.exam.security.actions"), 1),
|
||||
EXAM_TEMPLATE_LIST(new LocTextKey("sebserver.examtemplate.list.actions"), 1),
|
||||
INDICATOR_TEMPLATE_LIST(new LocTextKey("sebserver.examtemplate.indicator.list.actions"), 1),
|
||||
CLIENT_GROUP_TEMPLATE_LIST(new LocTextKey("sebserver.examtemplate.clientgroup.list.actions"), 2),
|
||||
|
|
|
@ -301,27 +301,27 @@ public enum ActionDefinition {
|
|||
new LocTextKey("sebserver.exam.action.sebrestriction.details"),
|
||||
ImageIcon.RESTRICTION,
|
||||
PageStateDefinitionImpl.EXAM_VIEW,
|
||||
ActionCategory.FORM),
|
||||
ActionCategory.EXAM_SECURITY),
|
||||
EXAM_ENABLE_SEB_RESTRICTION(
|
||||
new LocTextKey("sebserver.exam.action.sebrestriction.enable"),
|
||||
ImageIcon.UNLOCK,
|
||||
PageStateDefinitionImpl.EXAM_VIEW,
|
||||
ActionCategory.FORM),
|
||||
ActionCategory.EXAM_SECURITY),
|
||||
EXAM_DISABLE_SEB_RESTRICTION(
|
||||
new LocTextKey("sebserver.exam.action.sebrestriction.disable"),
|
||||
ImageIcon.LOCK,
|
||||
PageStateDefinitionImpl.EXAM_VIEW,
|
||||
ActionCategory.FORM),
|
||||
ActionCategory.EXAM_SECURITY),
|
||||
EXAM_PROCTORING_ON(
|
||||
new LocTextKey("sebserver.exam.proctoring.actions.open"),
|
||||
ImageIcon.VISIBILITY,
|
||||
PageStateDefinitionImpl.EXAM_VIEW,
|
||||
ActionCategory.FORM),
|
||||
ActionCategory.EXAM_SECURITY),
|
||||
EXAM_PROCTORING_OFF(
|
||||
new LocTextKey("sebserver.exam.proctoring.actions.open"),
|
||||
ImageIcon.VISIBILITY_OFF,
|
||||
PageStateDefinitionImpl.EXAM_VIEW,
|
||||
ActionCategory.FORM),
|
||||
ActionCategory.EXAM_SECURITY),
|
||||
|
||||
EXAM_CONFIGURATION_NEW(
|
||||
new LocTextKey("sebserver.exam.configuration.action.list.new"),
|
||||
|
@ -419,12 +419,12 @@ public enum ActionDefinition {
|
|||
new LocTextKey("sebserver.exam.signaturekey.action.edit"),
|
||||
ImageIcon.SHIELD,
|
||||
PageStateDefinitionImpl.SECURITY_KEY_EDIT,
|
||||
ActionCategory.FORM),
|
||||
ActionCategory.EXAM_SECURITY),
|
||||
EXAM_SECURITY_KEY_DISABLED(
|
||||
new LocTextKey("sebserver.exam.signaturekey.action.edit"),
|
||||
ImageIcon.NO_SHIELD,
|
||||
PageStateDefinitionImpl.SECURITY_KEY_EDIT,
|
||||
ActionCategory.FORM),
|
||||
ActionCategory.EXAM_SECURITY),
|
||||
EXAM_RELOAD_SECURITY_KEY_VIEW(
|
||||
new LocTextKey("sebserver.exam.signaturekey.action.edit"),
|
||||
ImageIcon.SHIELD,
|
||||
|
@ -441,11 +441,21 @@ public enum ActionDefinition {
|
|||
ImageIcon.CANCEL,
|
||||
PageStateDefinitionImpl.EXAM_VIEW,
|
||||
ActionCategory.FORM),
|
||||
EXAM_SECURITY_KEY_BACK_MODIFY(
|
||||
new LocTextKey("sebserver.exam.signaturekey.action.back"),
|
||||
ImageIcon.BACK,
|
||||
PageStateDefinitionImpl.EXAM_VIEW,
|
||||
ActionCategory.FORM),
|
||||
EXAM_SECURITY_KEY_SHOW_ADD_GRANT_POPUP(
|
||||
new LocTextKey("sebserver.exam.signaturekey.action.addGrant"),
|
||||
ImageIcon.ADD,
|
||||
PageStateDefinitionImpl.SECURITY_KEY_EDIT,
|
||||
ActionCategory.APP_SIGNATURE_KEY_LIST),
|
||||
EXAM_SECURITY_KEY_SHOW_ASK_POPUP(
|
||||
new LocTextKey("sebserver.exam.signaturekey.action.showASK"),
|
||||
ImageIcon.ADD,
|
||||
PageStateDefinitionImpl.SECURITY_KEY_EDIT,
|
||||
ActionCategory.APP_SIGNATURE_KEY_LIST),
|
||||
EXAM_SECURITY_KEY_SHOW_GRANT_POPUP(
|
||||
new LocTextKey("sebserver.exam.signaturekey.action.showGrant"),
|
||||
ImageIcon.SHOW,
|
||||
|
@ -915,7 +925,7 @@ public enum ActionDefinition {
|
|||
|
||||
MONITOR_EXAM_BACK_TO_OVERVIEW(
|
||||
new LocTextKey("sebserver.monitoring.exam.action.detail.view"),
|
||||
ImageIcon.SHOW,
|
||||
ImageIcon.BACK,
|
||||
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
|
||||
ActionCategory.FORM),
|
||||
|
||||
|
@ -1025,7 +1035,7 @@ public enum ActionDefinition {
|
|||
ActionCategory.CLIENT_EVENT_LIST),
|
||||
FINISHED_EXAM_BACK_TO_OVERVIEW(
|
||||
new LocTextKey("sebserver.finished.exam.action.detail.view"),
|
||||
ImageIcon.SHOW,
|
||||
ImageIcon.BACK,
|
||||
PageStateDefinitionImpl.FINISHED_EXAM,
|
||||
ActionCategory.FORM),
|
||||
FINISHED_EXAM_EXPORT_CSV(
|
||||
|
|
|
@ -78,6 +78,7 @@ public class AddSecurityKeyGrantPopup {
|
|||
public PageAction showGrantPopup(final PageAction action, final AppSignatureKeyInfo appSignatureKeyInfo) {
|
||||
final PageContext pageContext = action.pageContext();
|
||||
final PopupComposer popupComposer = new PopupComposer(this.pageService, pageContext, appSignatureKeyInfo);
|
||||
final boolean readonly = action.pageContext().isReadonly();
|
||||
try {
|
||||
final ModalInputDialog<FormHandle<?>> dialog =
|
||||
new ModalInputDialog<>(
|
||||
|
@ -90,7 +91,7 @@ public class AddSecurityKeyGrantPopup {
|
|||
formHandle,
|
||||
appSignatureKeyInfo);
|
||||
|
||||
if (appSignatureKeyInfo.key == null) {
|
||||
if (appSignatureKeyInfo.key == null || readonly) {
|
||||
dialog.open(
|
||||
TITLE_TEXT_KEY,
|
||||
popupComposer);
|
||||
|
@ -130,6 +131,7 @@ public class AddSecurityKeyGrantPopup {
|
|||
widgetFactory.addFormSubContextHeader(parent, TITLE_TEXT_INFO, null);
|
||||
final boolean hasASK = this.appSignatureKeyInfo.key != null;
|
||||
final PageContext formContext = this.pageContext.copyOf(parent);
|
||||
final boolean readonly = this.pageContext.isReadonly();
|
||||
final FormHandle<?> form = this.pageService.formBuilder(formContext)
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
|
@ -140,7 +142,7 @@ public class AddSecurityKeyGrantPopup {
|
|||
: Constants.EMPTY_NOTE)
|
||||
.readonly(true))
|
||||
|
||||
.addFieldIf(() -> hasASK,
|
||||
.addFieldIf(() -> hasASK && !readonly,
|
||||
() -> FormBuilder.text(
|
||||
Domain.SEB_SECURITY_KEY_REGISTRY.ATTR_TAG,
|
||||
TITLE_TEXT_FORM_TAG)
|
||||
|
|
|
@ -452,6 +452,14 @@ public class ExamForm implements TemplateComposer {
|
|||
exam.getName()))
|
||||
.publishIf(() -> editable && readonly)
|
||||
|
||||
.newAction(ActionDefinition.EXAM_SECURITY_KEY_ENABLED)
|
||||
.withEntityKey(entityKey)
|
||||
.publishIf(() -> signatureKeyCheckEnabled && readonly)
|
||||
|
||||
.newAction(ActionDefinition.EXAM_SECURITY_KEY_DISABLED)
|
||||
.withEntityKey(entityKey)
|
||||
.publishIf(() -> !signatureKeyCheckEnabled && readonly)
|
||||
|
||||
.newAction(ActionDefinition.EXAM_MODIFY_SEB_RESTRICTION_DETAILS)
|
||||
.withEntityKey(entityKey)
|
||||
.withExec(this.examSEBRestrictionSettings.settingsFunction(this.pageService))
|
||||
|
@ -473,14 +481,6 @@ public class ExamForm implements TemplateComposer {
|
|||
.publishIf(() -> sebRestrictionAvailable && readonly && modifyGrant && !importFromQuizData
|
||||
&& BooleanUtils.isTrue(isRestricted))
|
||||
|
||||
.newAction(ActionDefinition.EXAM_SECURITY_KEY_ENABLED)
|
||||
.withEntityKey(entityKey)
|
||||
.publishIf(() -> signatureKeyCheckEnabled && readonly)
|
||||
|
||||
.newAction(ActionDefinition.EXAM_SECURITY_KEY_DISABLED)
|
||||
.withEntityKey(entityKey)
|
||||
.publishIf(() -> !signatureKeyCheckEnabled && readonly)
|
||||
|
||||
.newAction(ActionDefinition.EXAM_PROCTORING_ON)
|
||||
.withEntityKey(entityKey)
|
||||
.withExec(this.proctoringSettingsPopup.settingsFunction(this.pageService, modifyGrant && editable))
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
package ch.ethz.seb.sebserver.gui.content.exam;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.eclipse.swt.widgets.Composite;
|
||||
|
@ -21,6 +23,7 @@ import ch.ethz.seb.sebserver.gbl.model.Domain;
|
|||
import ch.ethz.seb.sebserver.gbl.model.Entity;
|
||||
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.Exam.ExamStatus;
|
||||
import ch.ethz.seb.sebserver.gbl.model.institution.AppSignatureKeyInfo;
|
||||
import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
|
||||
|
@ -31,6 +34,7 @@ import ch.ethz.seb.sebserver.gui.form.FormHandle;
|
|||
import ch.ethz.seb.sebserver.gui.service.ResourceService;
|
||||
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
|
||||
import ch.ethz.seb.sebserver.gui.service.page.PageContext;
|
||||
import ch.ethz.seb.sebserver.gui.service.page.PageContext.AttributeKeys;
|
||||
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.TemplateComposer;
|
||||
|
@ -116,6 +120,7 @@ public class ExamSignatureKeyForm implements TemplateComposer {
|
|||
final boolean signatureKeyCheckEnabled = BooleanUtils.toBoolean(
|
||||
exam.additionalAttributes.get(Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED));
|
||||
final String ct = exam.additionalAttributes.get(Exam.ADDITIONAL_ATTR_NUMERICAL_TRUST_THRESHOLD);
|
||||
final boolean readonly = exam.status == ExamStatus.FINISHED || exam.status == ExamStatus.ARCHIVED;
|
||||
|
||||
final Composite content = widgetFactory
|
||||
.defaultPageLayout(pageContext.getParent(), TILE);
|
||||
|
@ -132,7 +137,8 @@ public class ExamSignatureKeyForm implements TemplateComposer {
|
|||
Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED,
|
||||
FORM_ENABLED,
|
||||
String.valueOf(signatureKeyCheckEnabled))
|
||||
.withInputSpan(1))
|
||||
.withInputSpan(1)
|
||||
.readonly(readonly))
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
Exam.ADDITIONAL_ATTR_NUMERICAL_TRUST_THRESHOLD,
|
||||
|
@ -144,7 +150,8 @@ public class ExamSignatureKeyForm implements TemplateComposer {
|
|||
}
|
||||
})
|
||||
.mandatory()
|
||||
.withInputSpan(1))
|
||||
.withInputSpan(1)
|
||||
.readonly(readonly))
|
||||
|
||||
.build();
|
||||
|
||||
|
@ -174,19 +181,23 @@ public class ExamSignatureKeyForm implements TemplateComposer {
|
|||
AppSignatureKeyInfo::getNumberOfConnections)
|
||||
.widthProportion(1))
|
||||
|
||||
.withDefaultAction(table -> actionBuilder
|
||||
.newAction(ActionDefinition.EXAM_SECURITY_KEY_SHOW_ADD_GRANT_POPUP)
|
||||
.withParentEntityKey(entityKey)
|
||||
.withExec(action -> this.addSecurityKeyGrantPopup.showGrantPopup(
|
||||
action,
|
||||
table.getSingleSelectedROWData()))
|
||||
.noEventPropagation()
|
||||
.ignoreMoveAwayFromEdit()
|
||||
.create())
|
||||
.withDefaultActionIf(
|
||||
() -> !readonly,
|
||||
table -> actionBuilder
|
||||
.newAction(ActionDefinition.EXAM_SECURITY_KEY_SHOW_ADD_GRANT_POPUP)
|
||||
.withParentEntityKey(entityKey)
|
||||
.withExec(action -> this.addSecurityKeyGrantPopup.showGrantPopup(
|
||||
action,
|
||||
table.getSingleSelectedROWData()))
|
||||
.noEventPropagation()
|
||||
.ignoreMoveAwayFromEdit()
|
||||
.create())
|
||||
|
||||
.withSelectionListener(this.pageService.getSelectionPublisher(
|
||||
pageContext,
|
||||
ActionDefinition.EXAM_SECURITY_KEY_SHOW_ADD_GRANT_POPUP))
|
||||
.withSelectionListenerIf(
|
||||
() -> !readonly,
|
||||
this.pageService.getSelectionPublisher(
|
||||
pageContext,
|
||||
ActionDefinition.EXAM_SECURITY_KEY_SHOW_ADD_GRANT_POPUP))
|
||||
|
||||
.compose(pageContext.copyOf(content));
|
||||
|
||||
|
@ -214,15 +225,17 @@ public class ExamSignatureKeyForm implements TemplateComposer {
|
|||
GRANT_LIST_TAG,
|
||||
SecurityKey::getTag).widthProportion(1))
|
||||
|
||||
.withDefaultAction(table -> actionBuilder
|
||||
.newAction(ActionDefinition.EXAM_SECURITY_KEY_SHOW_GRANT_POPUP)
|
||||
.withParentEntityKey(entityKey)
|
||||
.withExec(action -> this.securityKeyGrantPopup.showGrantPopup(
|
||||
action,
|
||||
table.getSingleSelectedROWData()))
|
||||
.noEventPropagation()
|
||||
.ignoreMoveAwayFromEdit()
|
||||
.create())
|
||||
.withDefaultActionIf(
|
||||
() -> !readonly,
|
||||
table -> actionBuilder
|
||||
.newAction(ActionDefinition.EXAM_SECURITY_KEY_SHOW_GRANT_POPUP)
|
||||
.withParentEntityKey(entityKey)
|
||||
.withExec(action -> this.securityKeyGrantPopup.showGrantPopup(
|
||||
action,
|
||||
table.getSingleSelectedROWData()))
|
||||
.noEventPropagation()
|
||||
.ignoreMoveAwayFromEdit()
|
||||
.create())
|
||||
|
||||
.withSelectionListener(this.pageService.getSelectionPublisher(
|
||||
pageContext,
|
||||
|
@ -235,30 +248,43 @@ public class ExamSignatureKeyForm implements TemplateComposer {
|
|||
.withEntityKey(entityKey)
|
||||
.withExec(action -> this.saveSettings(action, form.getForm()))
|
||||
.ignoreMoveAwayFromEdit()
|
||||
.publish()
|
||||
.publishIf(() -> !readonly)
|
||||
|
||||
.newAction(ActionDefinition.EXAM_SECURITY_KEY_CANCEL_MODIFY)
|
||||
.withExec(this.pageService.backToCurrentFunction())
|
||||
.publish()
|
||||
.publishIf(() -> !readonly)
|
||||
|
||||
.newAction(ActionDefinition.EXAM_SECURITY_KEY_BACK_MODIFY)
|
||||
.withEntityKey(entityKey)
|
||||
//.withExec(this.pageService.backToCurrentFunction())
|
||||
.publishIf(() -> readonly)
|
||||
|
||||
.newAction(ActionDefinition.EXAM_SECURITY_KEY_SHOW_ADD_GRANT_POPUP)
|
||||
.withParentEntityKey(entityKey)
|
||||
.withSelect(
|
||||
connectionInfoTable::getMultiSelection,
|
||||
action -> this.addSecurityKeyGrantPopup.showGrantPopup(
|
||||
action,
|
||||
connectionInfoTable.getSingleSelectedROWData()),
|
||||
this.addGrant(connectionInfoTable),
|
||||
APP_SIG_KEY_LIST_EMPTY_SELECTION_TEXT_KEY)
|
||||
.ignoreMoveAwayFromEdit()
|
||||
.noEventPropagation()
|
||||
.publish(false)
|
||||
.publishIf(() -> !readonly, false)
|
||||
|
||||
.newAction(ActionDefinition.EXAM_SECURITY_KEY_SHOW_ASK_POPUP)
|
||||
.withParentEntityKey(entityKey)
|
||||
.withAttribute(AttributeKeys.READ_ONLY, String.valueOf(readonly))
|
||||
.withSelect(
|
||||
connectionInfoTable::getMultiSelection,
|
||||
this.showASK(connectionInfoTable),
|
||||
APP_SIG_KEY_LIST_EMPTY_SELECTION_TEXT_KEY)
|
||||
.ignoreMoveAwayFromEdit()
|
||||
.noEventPropagation()
|
||||
.publishIf(() -> readonly, false)
|
||||
|
||||
.newAction(ActionDefinition.EXAM_SECURITY_KEY_SHOW_GRANT_POPUP)
|
||||
.withEntityKey(entityKey)
|
||||
.withSelect(
|
||||
grantsList::getMultiSelection,
|
||||
action -> this.securityKeyGrantPopup.showGrantPopup(action,
|
||||
grantsList.getSingleSelectedROWData()),
|
||||
this.showGrant(grantsList),
|
||||
GRANT_LIST_EMPTY_SELECTION_TEXT_KEY)
|
||||
.ignoreMoveAwayFromEdit()
|
||||
.noEventPropagation()
|
||||
|
@ -272,9 +298,7 @@ public class ExamSignatureKeyForm implements TemplateComposer {
|
|||
this::deleteGrant,
|
||||
GRANT_LIST_EMPTY_SELECTION_TEXT_KEY)
|
||||
.ignoreMoveAwayFromEdit()
|
||||
.publish(false)
|
||||
|
||||
;
|
||||
.publishIf(() -> !readonly, false);
|
||||
}
|
||||
|
||||
private PageAction saveSettings(final PageAction action, final Form form) {
|
||||
|
@ -293,6 +317,36 @@ public class ExamSignatureKeyForm implements TemplateComposer {
|
|||
return action;
|
||||
}
|
||||
|
||||
private Function<PageAction, PageAction> addGrant(final EntityTable<AppSignatureKeyInfo> connectionInfoTable) {
|
||||
return action -> {
|
||||
final EntityKey singleSelection = action.getSingleSelection();
|
||||
if (singleSelection != null) {
|
||||
this.addSecurityKeyGrantPopup.showGrantPopup(action, connectionInfoTable.getSingleSelectedROWData());
|
||||
}
|
||||
return action;
|
||||
};
|
||||
}
|
||||
|
||||
private Function<PageAction, PageAction> showASK(final EntityTable<AppSignatureKeyInfo> connectionInfoTable) {
|
||||
return action -> {
|
||||
final EntityKey singleSelection = action.getSingleSelection();
|
||||
if (singleSelection != null) {
|
||||
this.addSecurityKeyGrantPopup.showGrantPopup(action, connectionInfoTable.getSingleSelectedROWData());
|
||||
}
|
||||
return action;
|
||||
};
|
||||
}
|
||||
|
||||
private Function<PageAction, PageAction> showGrant(final EntityTable<SecurityKey> grantsList) {
|
||||
return action -> {
|
||||
final EntityKey singleSelection = action.getSingleSelection();
|
||||
if (singleSelection != null) {
|
||||
this.securityKeyGrantPopup.showGrantPopup(action, grantsList.getSingleSelectedROWData());
|
||||
}
|
||||
return action;
|
||||
};
|
||||
}
|
||||
|
||||
private PageAction deleteGrant(final PageAction action) {
|
||||
final EntityKey parentEntityKey = action.getParentEntityKey();
|
||||
final EntityKey singleSelection = action.getSingleSelection();
|
||||
|
|
|
@ -10,13 +10,16 @@ package ch.ethz.seb.sebserver.gui.content.exam;
|
|||
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumSet;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.eclipse.swt.SWT;
|
||||
import org.eclipse.swt.layout.RowData;
|
||||
import org.eclipse.swt.widgets.Button;
|
||||
import org.eclipse.swt.widgets.Composite;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
@ -47,9 +50,11 @@ import ch.ethz.seb.sebserver.gui.service.page.impl.ModalInputDialog;
|
|||
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.GetExamProctoringSettings;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.ResetProctoringSettings;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.SaveExamProctoringSettings;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.template.GetExamTemplateProctoringSettings;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.template.SaveExamTemplateProctoringSettings;
|
||||
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
|
||||
|
||||
@Lazy
|
||||
@Component
|
||||
|
@ -72,10 +77,24 @@ public class ProctoringSettingsPopup {
|
|||
new LocTextKey("sebserver.exam.proctoring.form.url");
|
||||
private final static LocTextKey SEB_PROCTORING_FORM_ROOM_SIZE =
|
||||
new LocTextKey("sebserver.exam.proctoring.form.collectingRoomSize");
|
||||
private final static LocTextKey SEB_PROCTORING_FORM_APPKEY =
|
||||
new LocTextKey("sebserver.exam.proctoring.form.appkey");
|
||||
private final static LocTextKey SEB_PROCTORING_FORM_SECRET =
|
||||
new LocTextKey("sebserver.exam.proctoring.form.secret");
|
||||
|
||||
private final static LocTextKey SEB_PROCTORING_FORM_APPKEY_JITSI =
|
||||
new LocTextKey("sebserver.exam.proctoring.form.appkey.jitsi");
|
||||
private final static LocTextKey SEB_PROCTORING_FORM_SECRET_JITSI =
|
||||
new LocTextKey("sebserver.exam.proctoring.form.secret.jitsi");
|
||||
|
||||
private final static LocTextKey SEB_PROCTORING_FORM_APPKEY_ZOOM =
|
||||
new LocTextKey("sebserver.exam.proctoring.form.appkey.zoom");
|
||||
private final static LocTextKey SEB_PROCTORING_FORM_SECRET_ZOOM =
|
||||
new LocTextKey("sebserver.exam.proctoring.form.secret.zoom");
|
||||
|
||||
private final static LocTextKey SEB_PROCTORING_FORM_ACCOUNT_ID =
|
||||
new LocTextKey("sebserver.exam.proctoring.form.accountId");
|
||||
private final static LocTextKey SEB_PROCTORING_FORM_CLIENT_ID =
|
||||
new LocTextKey("sebserver.exam.proctoring.form.clientId");
|
||||
private final static LocTextKey SEB_PROCTORING_FORM_CLIENT_SECRET =
|
||||
new LocTextKey("sebserver.exam.proctoring.form.clientSecret");
|
||||
|
||||
private final static LocTextKey SEB_PROCTORING_FORM_SDKKEY =
|
||||
new LocTextKey("sebserver.exam.proctoring.form.sdkkey");
|
||||
private final static LocTextKey SEB_PROCTORING_FORM_SDKSECRET =
|
||||
|
@ -86,6 +105,13 @@ public class ProctoringSettingsPopup {
|
|||
private final static LocTextKey SEB_PROCTORING_FORM_FEATURES =
|
||||
new LocTextKey("sebserver.exam.proctoring.form.features");
|
||||
|
||||
private final static LocTextKey SAVE_TEXT_KEY =
|
||||
new LocTextKey("sebserver.exam.proctoring.form.saveSettings");
|
||||
private final static LocTextKey RESET_TEXT_KEY =
|
||||
new LocTextKey("sebserver.exam.proctoring.form.resetSettings");
|
||||
private final static LocTextKey RESET_CONFIRM_KEY =
|
||||
new LocTextKey("sebserver.exam.proctoring.form.resetConfirm");
|
||||
|
||||
Function<PageAction, PageAction> settingsFunction(final PageService pageService, final boolean modifyGrant) {
|
||||
|
||||
return action -> {
|
||||
|
@ -102,32 +128,90 @@ public class ProctoringSettingsPopup {
|
|||
.setDialogWidth(860)
|
||||
.setDialogHeight(600);
|
||||
|
||||
final SEBProctoringPropertiesForm bindFormContext = new SEBProctoringPropertiesForm(
|
||||
pageService,
|
||||
pageContext);
|
||||
|
||||
final Predicate<FormHandle<?>> doBind = formHandle -> doSaveSettings(
|
||||
pageService,
|
||||
pageContext,
|
||||
formHandle);
|
||||
|
||||
final ResetButtonHandler resetButtonHandler = new ResetButtonHandler();
|
||||
if (modifyGrant) {
|
||||
dialog.open(
|
||||
|
||||
final BiConsumer<Composite, Supplier<FormHandle<?>>> actionComposer = (composite, handle) -> {
|
||||
final WidgetFactory widgetFactory = pageService.getWidgetFactory();
|
||||
|
||||
final Button save = widgetFactory.buttonLocalized(composite, SAVE_TEXT_KEY);
|
||||
save.setLayoutData(new RowData());
|
||||
save.addListener(SWT.Selection, event -> {
|
||||
if (doSaveSettings(pageService, pageContext, handle.get())) {
|
||||
dialog.close();
|
||||
}
|
||||
});
|
||||
|
||||
final Button reset = widgetFactory.buttonLocalized(composite, RESET_TEXT_KEY);
|
||||
reset.setLayoutData(new RowData());
|
||||
reset.addListener(SWT.Selection, event -> {
|
||||
pageContext.applyConfirmDialog(RESET_CONFIRM_KEY, apply -> {
|
||||
if (apply && doResetSettings(pageService, pageContext)) {
|
||||
dialog.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
resetButtonHandler.set(reset);
|
||||
};
|
||||
|
||||
final SEBProctoringPropertiesForm bindFormContext = new SEBProctoringPropertiesForm(
|
||||
pageService,
|
||||
pageContext,
|
||||
resetButtonHandler);
|
||||
|
||||
dialog.openWithActions(
|
||||
SEB_PROCTORING_FORM_TITLE,
|
||||
doBind,
|
||||
actionComposer,
|
||||
Utils.EMPTY_EXECUTION,
|
||||
bindFormContext);
|
||||
} else {
|
||||
dialog.open(
|
||||
SEB_PROCTORING_FORM_TITLE,
|
||||
pageContext,
|
||||
pc -> bindFormContext.compose(pc.getParent()));
|
||||
pc -> new SEBProctoringPropertiesForm(
|
||||
pageService,
|
||||
pageContext,
|
||||
resetButtonHandler));
|
||||
}
|
||||
|
||||
return action;
|
||||
};
|
||||
}
|
||||
|
||||
private static final class ResetButtonHandler {
|
||||
|
||||
Button resetBotton = null;
|
||||
boolean enabled = false;
|
||||
|
||||
void set(final Button resetBotton) {
|
||||
this.resetBotton = resetBotton;
|
||||
resetBotton.setEnabled(this.enabled);
|
||||
}
|
||||
|
||||
void enable(final boolean enable) {
|
||||
this.enabled = enable;
|
||||
if (this.resetBotton != null) {
|
||||
this.resetBotton.setEnabled(enable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean doResetSettings(
|
||||
final PageService pageService,
|
||||
final PageContext pageContext) {
|
||||
|
||||
final EntityKey entityKey = pageContext.getEntityKey();
|
||||
|
||||
return pageService.getRestService()
|
||||
.getBuilder(ResetProctoringSettings.class)
|
||||
.withURIVariable(API.PARAM_MODEL_ID, entityKey.modelId)
|
||||
.call()
|
||||
.onError(error -> {
|
||||
log.error("Failed to rest proctoring settings for exam: {}", entityKey, error);
|
||||
pageContext.notifyUnexpectedError(error);
|
||||
}).map(settings -> true).getOr(false);
|
||||
}
|
||||
|
||||
private boolean doSaveSettings(
|
||||
final PageService pageService,
|
||||
final PageContext pageContext,
|
||||
|
@ -168,8 +252,14 @@ public class ProctoringSettingsPopup {
|
|||
false,
|
||||
form.getFieldValue(ProctoringServiceSettings.ATTR_APP_KEY),
|
||||
form.getFieldValue(ProctoringServiceSettings.ATTR_APP_SECRET),
|
||||
|
||||
form.getFieldValue(ProctoringServiceSettings.ATTR_ACCOUNT_ID),
|
||||
form.getFieldValue(ProctoringServiceSettings.ATTR_ACCOUNT_CLIENT_ID),
|
||||
form.getFieldValue(ProctoringServiceSettings.ATTR_ACCOUNT_CLIENT_SECRET),
|
||||
|
||||
form.getFieldValue(ProctoringServiceSettings.ATTR_SDK_KEY),
|
||||
form.getFieldValue(ProctoringServiceSettings.ATTR_SDK_SECRET),
|
||||
|
||||
BooleanUtils.toBoolean(form.getFieldValue(
|
||||
ProctoringServiceSettings.ATTR_USE_ZOOM_APP_CLIENT_COLLECTING_ROOM)));
|
||||
|
||||
|
@ -217,23 +307,22 @@ public class ProctoringSettingsPopup {
|
|||
|
||||
private final PageService pageService;
|
||||
private final PageContext pageContext;
|
||||
private final ResetButtonHandler resetButtonHandler;
|
||||
|
||||
protected SEBProctoringPropertiesForm(
|
||||
final PageService pageService,
|
||||
final PageContext pageContext) {
|
||||
final PageContext pageContext,
|
||||
final ResetButtonHandler resetButtonHandler) {
|
||||
|
||||
this.pageService = pageService;
|
||||
this.pageContext = pageContext;
|
||||
|
||||
this.resetButtonHandler = resetButtonHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Supplier<FormHandle<?>> compose(final Composite parent) {
|
||||
final RestService restService = this.pageService.getRestService();
|
||||
final ResourceService resourceService = this.pageService.getResourceService();
|
||||
final EntityKey entityKey = this.pageContext.getEntityKey();
|
||||
final boolean isReadonly = BooleanUtils.toBoolean(
|
||||
this.pageContext.getAttribute(PageContext.AttributeKeys.FORCE_READ_ONLY));
|
||||
|
||||
final Composite content = this.pageService
|
||||
.getWidgetFactory()
|
||||
|
@ -248,12 +337,170 @@ public class ProctoringSettingsPopup {
|
|||
.call()
|
||||
.getOrThrow();
|
||||
|
||||
final PageContext formContext = this.pageContext
|
||||
this.resetButtonHandler.enable(proctoringSettings.serviceInUse);
|
||||
final FormHandleAnchor formHandleAnchor = new FormHandleAnchor();
|
||||
formHandleAnchor.formContext = this.pageContext
|
||||
.copyOf(content)
|
||||
.clearEntityKeys();
|
||||
|
||||
final FormHandle<ProctoringServiceSettings> formHandle = this.pageService.formBuilder(
|
||||
formContext)
|
||||
buildFormAccordingToService(
|
||||
proctoringSettings,
|
||||
proctoringSettings.serverType.name(),
|
||||
formHandleAnchor);
|
||||
|
||||
return () -> formHandleAnchor.formHandle;
|
||||
}
|
||||
|
||||
private void buildFormAccordingToService(
|
||||
final ProctoringServiceSettings proctoringServiceSettings,
|
||||
final String serviceType,
|
||||
final FormHandleAnchor formHandleAnchor) {
|
||||
|
||||
if (ProctoringServerType.JITSI_MEET.name().equals(serviceType)) {
|
||||
PageService.clearComposite(formHandleAnchor.formContext.getParent());
|
||||
formHandleAnchor.formHandle = buildFormForJitsi(proctoringServiceSettings, formHandleAnchor);
|
||||
} else if (ProctoringServerType.ZOOM.name().equals(serviceType)) {
|
||||
PageService.clearComposite(formHandleAnchor.formContext.getParent());
|
||||
formHandleAnchor.formHandle = buildFormForZoom(proctoringServiceSettings, formHandleAnchor);
|
||||
}
|
||||
|
||||
if (proctoringServiceSettings.serviceInUse) {
|
||||
final Form form = formHandleAnchor.formHandle.getForm();
|
||||
form.getFieldInput(ProctoringServiceSettings.ATTR_SERVER_TYPE).setEnabled(false);
|
||||
form.getFieldInput(ProctoringServiceSettings.ATTR_SERVER_URL).setEnabled(false);
|
||||
}
|
||||
|
||||
formHandleAnchor.formContext.getParent().getParent().getParent().layout(true, true);
|
||||
}
|
||||
|
||||
private FormHandle<ProctoringServiceSettings> buildFormForJitsi(
|
||||
final ProctoringServiceSettings proctoringSettings,
|
||||
final FormHandleAnchor formHandleAnchor) {
|
||||
|
||||
final ResourceService resourceService = this.pageService.getResourceService();
|
||||
final boolean isReadonly = BooleanUtils.toBoolean(
|
||||
this.pageContext.getAttribute(PageContext.AttributeKeys.FORCE_READ_ONLY));
|
||||
|
||||
final FormBuilder formBuilder = buildHeader(proctoringSettings, formHandleAnchor, isReadonly);
|
||||
|
||||
formBuilder.addField(FormBuilder.singleSelection(
|
||||
ProctoringServiceSettings.ATTR_SERVER_TYPE,
|
||||
SEB_PROCTORING_FORM_TYPE,
|
||||
ProctoringServerType.JITSI_MEET.name(),
|
||||
resourceService::examProctoringTypeResources)
|
||||
.withSelectionListener(form -> buildFormAccordingToService(
|
||||
proctoringSettings,
|
||||
form.getFieldValue(ProctoringServiceSettings.ATTR_SERVER_TYPE),
|
||||
formHandleAnchor)))
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
ProctoringServiceSettings.ATTR_SERVER_URL,
|
||||
SEB_PROCTORING_FORM_URL,
|
||||
proctoringSettings.serverURL)
|
||||
.mandatory())
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
ProctoringServiceSettings.ATTR_APP_KEY,
|
||||
SEB_PROCTORING_FORM_APPKEY_JITSI,
|
||||
proctoringSettings.appKey))
|
||||
.withEmptyCellSeparation(false)
|
||||
|
||||
.addField(FormBuilder.password(
|
||||
ProctoringServiceSettings.ATTR_APP_SECRET,
|
||||
SEB_PROCTORING_FORM_SECRET_JITSI,
|
||||
(proctoringSettings.appSecret != null)
|
||||
? String.valueOf(proctoringSettings.appSecret)
|
||||
: null));
|
||||
|
||||
return buildFooter(proctoringSettings, resourceService, formBuilder);
|
||||
}
|
||||
|
||||
private FormHandle<ProctoringServiceSettings> buildFormForZoom(
|
||||
final ProctoringServiceSettings proctoringSettings,
|
||||
final FormHandleAnchor formHandleAnchor) {
|
||||
|
||||
final ResourceService resourceService = this.pageService.getResourceService();
|
||||
final boolean isReadonly = BooleanUtils.toBoolean(
|
||||
this.pageContext.getAttribute(PageContext.AttributeKeys.FORCE_READ_ONLY));
|
||||
|
||||
final FormBuilder formBuilder = buildHeader(proctoringSettings, formHandleAnchor, isReadonly);
|
||||
|
||||
formBuilder
|
||||
.addField(FormBuilder.singleSelection(
|
||||
ProctoringServiceSettings.ATTR_SERVER_TYPE,
|
||||
SEB_PROCTORING_FORM_TYPE,
|
||||
ProctoringServerType.ZOOM.name(),
|
||||
resourceService::examProctoringTypeResources)
|
||||
|
||||
.withSelectionListener(form -> buildFormAccordingToService(
|
||||
proctoringSettings,
|
||||
form.getFieldValue(ProctoringServiceSettings.ATTR_SERVER_TYPE),
|
||||
formHandleAnchor)))
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
ProctoringServiceSettings.ATTR_SERVER_URL,
|
||||
SEB_PROCTORING_FORM_URL,
|
||||
proctoringSettings.serverURL)
|
||||
.mandatory())
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
ProctoringServiceSettings.ATTR_APP_KEY,
|
||||
SEB_PROCTORING_FORM_APPKEY_ZOOM,
|
||||
proctoringSettings.appKey))
|
||||
.withEmptyCellSeparation(false)
|
||||
|
||||
.addField(FormBuilder.password(
|
||||
ProctoringServiceSettings.ATTR_APP_SECRET,
|
||||
SEB_PROCTORING_FORM_SECRET_ZOOM,
|
||||
(proctoringSettings.appSecret != null)
|
||||
? String.valueOf(proctoringSettings.appSecret)
|
||||
: null))
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
ProctoringServiceSettings.ATTR_ACCOUNT_ID,
|
||||
SEB_PROCTORING_FORM_ACCOUNT_ID,
|
||||
proctoringSettings.accountId)
|
||||
.mandatory())
|
||||
.withEmptyCellSeparation(false)
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
ProctoringServiceSettings.ATTR_ACCOUNT_CLIENT_ID,
|
||||
SEB_PROCTORING_FORM_CLIENT_ID,
|
||||
proctoringSettings.clientId)
|
||||
.mandatory())
|
||||
.withEmptyCellSeparation(false)
|
||||
|
||||
.addField(FormBuilder.password(
|
||||
ProctoringServiceSettings.ATTR_ACCOUNT_CLIENT_SECRET,
|
||||
SEB_PROCTORING_FORM_CLIENT_SECRET,
|
||||
(proctoringSettings.clientSecret != null)
|
||||
? String.valueOf(proctoringSettings.clientSecret)
|
||||
: null)
|
||||
.mandatory())
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
ProctoringServiceSettings.ATTR_SDK_KEY,
|
||||
SEB_PROCTORING_FORM_SDKKEY,
|
||||
proctoringSettings.sdkKey)
|
||||
.mandatory())
|
||||
.withEmptyCellSeparation(false)
|
||||
|
||||
.addField(FormBuilder.password(
|
||||
ProctoringServiceSettings.ATTR_SDK_SECRET,
|
||||
SEB_PROCTORING_FORM_SDKSECRET,
|
||||
(proctoringSettings.sdkSecret != null)
|
||||
? String.valueOf(proctoringSettings.sdkSecret)
|
||||
: null)
|
||||
.mandatory());
|
||||
|
||||
return buildFooter(proctoringSettings, resourceService, formBuilder);
|
||||
|
||||
}
|
||||
|
||||
private FormBuilder buildHeader(final ProctoringServiceSettings proctoringSettings,
|
||||
final FormHandleAnchor formHandleAnchor, final boolean isReadonly) {
|
||||
final FormBuilder formBuilder = this.pageService.formBuilder(
|
||||
formHandleAnchor.formContext)
|
||||
.withDefaultSpanInput(5)
|
||||
.withEmptyCellSeparation(true)
|
||||
.withDefaultSpanEmptyCell(1)
|
||||
|
@ -270,49 +517,13 @@ public class ProctoringSettingsPopup {
|
|||
.addField(FormBuilder.checkbox(
|
||||
ProctoringServiceSettings.ATTR_ENABLE_PROCTORING,
|
||||
SEB_PROCTORING_FORM_ENABLE,
|
||||
String.valueOf(proctoringSettings.enableProctoring)))
|
||||
String.valueOf(proctoringSettings.enableProctoring)));
|
||||
return formBuilder;
|
||||
}
|
||||
|
||||
.addField(FormBuilder.singleSelection(
|
||||
ProctoringServiceSettings.ATTR_SERVER_TYPE,
|
||||
SEB_PROCTORING_FORM_TYPE,
|
||||
proctoringSettings.serverType.name(),
|
||||
resourceService::examProctoringTypeResources))
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
ProctoringServiceSettings.ATTR_SERVER_URL,
|
||||
SEB_PROCTORING_FORM_URL,
|
||||
proctoringSettings.serverURL)
|
||||
.mandatory())
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
ProctoringServiceSettings.ATTR_APP_KEY,
|
||||
SEB_PROCTORING_FORM_APPKEY,
|
||||
proctoringSettings.appKey)
|
||||
.mandatory())
|
||||
.withEmptyCellSeparation(false)
|
||||
|
||||
.addField(FormBuilder.password(
|
||||
ProctoringServiceSettings.ATTR_APP_SECRET,
|
||||
SEB_PROCTORING_FORM_SECRET,
|
||||
(proctoringSettings.appSecret != null)
|
||||
? String.valueOf(proctoringSettings.appSecret)
|
||||
: null)
|
||||
.mandatory())
|
||||
|
||||
.addField(FormBuilder.text(
|
||||
ProctoringServiceSettings.ATTR_SDK_KEY,
|
||||
SEB_PROCTORING_FORM_SDKKEY,
|
||||
proctoringSettings.sdkKey))
|
||||
.withEmptyCellSeparation(false)
|
||||
|
||||
.addField(FormBuilder.password(
|
||||
ProctoringServiceSettings.ATTR_SDK_SECRET,
|
||||
SEB_PROCTORING_FORM_SDKSECRET,
|
||||
(proctoringSettings.sdkSecret != null)
|
||||
? String.valueOf(proctoringSettings.sdkSecret)
|
||||
: null))
|
||||
|
||||
.withDefaultSpanInput(1)
|
||||
private FormHandle<ProctoringServiceSettings> buildFooter(final ProctoringServiceSettings proctoringSettings,
|
||||
final ResourceService resourceService, final FormBuilder formBuilder) {
|
||||
return formBuilder.withDefaultSpanInput(1)
|
||||
.addField(FormBuilder.text(
|
||||
ProctoringServiceSettings.ATTR_COLLECTING_ROOM_SIZE,
|
||||
SEB_PROCTORING_FORM_ROOM_SIZE,
|
||||
|
@ -337,14 +548,14 @@ public class ProctoringSettingsPopup {
|
|||
resourceService::examProctoringFeaturesResources))
|
||||
|
||||
.build();
|
||||
|
||||
if (proctoringSettings.serviceInUse) {
|
||||
formHandle.getForm().getFieldInput(ProctoringServiceSettings.ATTR_SERVER_TYPE).setEnabled(false);
|
||||
formHandle.getForm().getFieldInput(ProctoringServiceSettings.ATTR_SERVER_URL).setEnabled(false);
|
||||
}
|
||||
|
||||
return () -> formHandle;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class FormHandleAnchor {
|
||||
|
||||
FormHandle<ProctoringServiceSettings> formHandle;
|
||||
PageContext formContext;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
package ch.ethz.seb.sebserver.gui.service.page.impl;
|
||||
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
|
@ -17,6 +18,8 @@ import org.eclipse.swt.SWT;
|
|||
import org.eclipse.swt.graphics.Rectangle;
|
||||
import org.eclipse.swt.layout.GridData;
|
||||
import org.eclipse.swt.layout.GridLayout;
|
||||
import org.eclipse.swt.layout.RowData;
|
||||
import org.eclipse.swt.layout.RowLayout;
|
||||
import org.eclipse.swt.widgets.Button;
|
||||
import org.eclipse.swt.widgets.Composite;
|
||||
import org.eclipse.swt.widgets.Dialog;
|
||||
|
@ -110,9 +113,53 @@ public class ModalInputDialog<T> extends Dialog {
|
|||
open(title, predicate, cancelCallback, contentComposer);
|
||||
}
|
||||
|
||||
public void openWithActions(
|
||||
final LocTextKey title,
|
||||
final BiConsumer<Composite, Supplier<T>> actionComposer,
|
||||
final Runnable cancelCallback,
|
||||
final ModalInputDialogComposer<T> contentComposer) {
|
||||
|
||||
// Create the selection dialog window
|
||||
this.shell = new Shell(getParent(), getStyle());
|
||||
this.shell.setText(getText());
|
||||
this.shell.setData(RWT.CUSTOM_VARIANT, CustomVariant.MESSAGE.key);
|
||||
this.shell.setText(this.widgetFactory.getI18nSupport().getText(title));
|
||||
this.shell.setLayout(new GridLayout(1, true));
|
||||
final GridData gridData2 = new GridData(SWT.FILL, SWT.BOTTOM, false, false);
|
||||
this.shell.setLayoutData(gridData2);
|
||||
|
||||
final Composite main = new Composite(this.shell, SWT.NONE);
|
||||
main.setLayout(new GridLayout());
|
||||
final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true);
|
||||
gridData.horizontalSpan = 2;
|
||||
gridData.widthHint = this.dialogWidth;
|
||||
main.setLayoutData(gridData);
|
||||
final Supplier<T> valueSupplier = contentComposer.compose(main);
|
||||
gridData.heightHint = calcDialogHeight(main);
|
||||
|
||||
final Composite actions = new Composite(this.shell, SWT.NONE);
|
||||
final RowLayout rowLayout = new RowLayout(SWT.HORIZONTAL);
|
||||
actions.setLayout(rowLayout);
|
||||
if (actionComposer != null) {
|
||||
actionComposer.accept(actions, valueSupplier);
|
||||
}
|
||||
|
||||
actions.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, true));
|
||||
final Button cancel = this.widgetFactory.buttonLocalized(actions, CANCEL_TEXT_KEY);
|
||||
cancel.setLayoutData(new RowData());
|
||||
cancel.addListener(SWT.Selection, event -> {
|
||||
if (cancelCallback != null) {
|
||||
cancelCallback.run();
|
||||
}
|
||||
this.shell.close();
|
||||
});
|
||||
|
||||
finishUp(this.shell);
|
||||
}
|
||||
|
||||
public void open(
|
||||
final LocTextKey title,
|
||||
final Predicate<T> callback,
|
||||
final Predicate<T> okCallback,
|
||||
final Runnable cancelCallback,
|
||||
final ModalInputDialogComposer<T> contentComposer) {
|
||||
|
||||
|
@ -142,7 +189,7 @@ public class ModalInputDialog<T> extends Dialog {
|
|||
ok.addListener(SWT.Selection, event -> {
|
||||
if (valueSupplier != null) {
|
||||
final T result = valueSupplier.get();
|
||||
if (callback.test(result)) {
|
||||
if (okCallback.test(result)) {
|
||||
this.shell.close();
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -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.exam;
|
||||
|
||||
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.ProctoringServiceSettings;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
|
||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall;
|
||||
|
||||
@Lazy
|
||||
@Component
|
||||
@GuiProfile
|
||||
public class ResetProctoringSettings extends RestCall<ProctoringServiceSettings> {
|
||||
|
||||
public ResetProctoringSettings() {
|
||||
super(new TypeKey<>(
|
||||
CallType.SAVE,
|
||||
EntityType.EXAM_PROCTOR_DATA,
|
||||
new TypeReference<ProctoringServiceSettings>() {
|
||||
}),
|
||||
HttpMethod.POST,
|
||||
MediaType.APPLICATION_JSON,
|
||||
API.EXAM_ADMINISTRATION_ENDPOINT
|
||||
+ API.MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ API.EXAM_ADMINISTRATION_PROCTORING_PATH_SEGMENT
|
||||
+ API.EXAM_ADMINISTRATION_PROCTORING_RESET_PATH_SEGMENT);
|
||||
}
|
||||
|
||||
}
|
|
@ -157,6 +157,16 @@ public class TableBuilder<ROW extends ModelIdAware> {
|
|||
return this;
|
||||
}
|
||||
|
||||
public TableBuilder<ROW> withSelectionListenerIf(
|
||||
final BooleanSupplier check,
|
||||
final Consumer<EntityTable<ROW>> selectionListener) {
|
||||
|
||||
if (check.getAsBoolean()) {
|
||||
this.selectionListener = selectionListener;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public TableBuilder<ROW> withContentChangeListener(final Consumer<Integer> contentChangeListener) {
|
||||
this.contentChangeListener = contentChangeListener;
|
||||
return this;
|
||||
|
|
|
@ -147,7 +147,8 @@ public class WidgetFactory {
|
|||
NOTIFICATION("notification.png"),
|
||||
VERIFY("verify.png"),
|
||||
SHIELD("shield.png"),
|
||||
NO_SHIELD("no_shield.png");
|
||||
NO_SHIELD("no_shield.png"),
|
||||
BACK("back.png");
|
||||
|
||||
public String fileName;
|
||||
private ImageData image = null;
|
||||
|
|
|
@ -116,6 +116,8 @@ public interface ExamAdminService {
|
|||
* @return ExamProctoringService instance */
|
||||
Result<ExamProctoringService> getExamProctoringService(final Long examId);
|
||||
|
||||
Result<Exam> resetProctoringSettings(Exam exam);
|
||||
|
||||
/** This archives a finished exam and set it to archived state as well as the assigned
|
||||
* exam configurations that are also set to archived state.
|
||||
*
|
||||
|
|
|
@ -60,7 +60,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
|
|||
private static final Logger log = LoggerFactory.getLogger(ExamAdminServiceImpl.class);
|
||||
|
||||
private final ExamDAO examDAO;
|
||||
private final ProctoringAdminService proctoringServiceSettingsService;
|
||||
private final ProctoringAdminService proctoringAdminService;
|
||||
private final AdditionalAttributesDAO additionalAttributesDAO;
|
||||
private final ConfigurationNodeDAO configurationNodeDAO;
|
||||
private final ExamConfigurationMapDAO examConfigurationMapDAO;
|
||||
|
@ -70,7 +70,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
|
|||
|
||||
protected ExamAdminServiceImpl(
|
||||
final ExamDAO examDAO,
|
||||
final ProctoringAdminService proctoringServiceSettingsService,
|
||||
final ProctoringAdminService proctoringAdminService,
|
||||
final AdditionalAttributesDAO additionalAttributesDAO,
|
||||
final ConfigurationNodeDAO configurationNodeDAO,
|
||||
final ExamConfigurationMapDAO examConfigurationMapDAO,
|
||||
|
@ -79,7 +79,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
|
|||
final @Value("${sebserver.webservice.api.admin.exam.app.signature.key.numerical.threshold:2}") int defaultNumericalTrustThreshold) {
|
||||
|
||||
this.examDAO = examDAO;
|
||||
this.proctoringServiceSettingsService = proctoringServiceSettingsService;
|
||||
this.proctoringAdminService = proctoringAdminService;
|
||||
this.additionalAttributesDAO = additionalAttributesDAO;
|
||||
this.configurationNodeDAO = configurationNodeDAO;
|
||||
this.examConfigurationMapDAO = examConfigurationMapDAO;
|
||||
|
@ -200,7 +200,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
|
|||
|
||||
@Override
|
||||
public Result<ProctoringServiceSettings> getProctoringServiceSettings(final Long examId) {
|
||||
return this.proctoringServiceSettingsService.getProctoringSettings(new EntityKey(examId, EntityType.EXAM));
|
||||
return this.proctoringAdminService.getProctoringSettings(new EntityKey(examId, EntityType.EXAM));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -209,7 +209,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
|
|||
final Long examId,
|
||||
final ProctoringServiceSettings proctoringServiceSettings) {
|
||||
|
||||
return this.proctoringServiceSettingsService
|
||||
return this.proctoringAdminService
|
||||
.saveProctoringServiceSettings(
|
||||
new EntityKey(examId, EntityType.EXAM),
|
||||
proctoringServiceSettings)
|
||||
|
@ -239,10 +239,30 @@ public class ExamAdminServiceImpl implements ExamAdminService {
|
|||
@Override
|
||||
public Result<ExamProctoringService> getExamProctoringService(final Long examId) {
|
||||
return getProctoringServiceSettings(examId)
|
||||
.flatMap(settings -> this.proctoringServiceSettingsService
|
||||
.flatMap(settings -> this.proctoringAdminService
|
||||
.getExamProctoringService(settings.serverType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Exam> resetProctoringSettings(final Exam exam) {
|
||||
return getProctoringServiceSettings(exam.id)
|
||||
.map(settings -> {
|
||||
ProctoringServiceSettings resetSettings;
|
||||
if (exam.examTemplateId != null) {
|
||||
// get settings from origin template
|
||||
resetSettings = this.proctoringAdminService
|
||||
.getProctoringSettings(new EntityKey(exam.examTemplateId, EntityType.EXAM_TEMPLATE))
|
||||
.map(template -> new ProctoringServiceSettings(exam.id, template))
|
||||
.getOr(new ProctoringServiceSettings(exam.id));
|
||||
} else {
|
||||
// create new reseted settings
|
||||
resetSettings = new ProctoringServiceSettings(exam.id);
|
||||
}
|
||||
return resetSettings;
|
||||
}).flatMap(settings -> saveProctoringServiceSettings(exam.id, settings))
|
||||
.map(settings -> exam);
|
||||
}
|
||||
|
||||
private Result<Exam> initAdditionalAttributesForMoodleExams(final Exam exam) {
|
||||
return Result.tryCatch(() -> {
|
||||
final LmsAPITemplate lmsTemplate = this.lmsAPIService
|
||||
|
|
|
@ -91,6 +91,9 @@ public class ProctoringAdminServiceImpl implements ProctoringAdminService {
|
|||
: false,
|
||||
getString(mapping, ProctoringServiceSettings.ATTR_APP_KEY),
|
||||
getString(mapping, ProctoringServiceSettings.ATTR_APP_SECRET),
|
||||
getString(mapping, ProctoringServiceSettings.ATTR_ACCOUNT_ID),
|
||||
getString(mapping, ProctoringServiceSettings.ATTR_ACCOUNT_CLIENT_ID),
|
||||
getString(mapping, ProctoringServiceSettings.ATTR_ACCOUNT_CLIENT_SECRET),
|
||||
getString(mapping, ProctoringServiceSettings.ATTR_SDK_KEY),
|
||||
getString(mapping, ProctoringServiceSettings.ATTR_SDK_SECRET),
|
||||
getBoolean(mapping,
|
||||
|
@ -139,19 +142,42 @@ public class ProctoringAdminServiceImpl implements ProctoringAdminService {
|
|||
ProctoringServiceSettings.ATTR_COLLECTING_ROOM_SIZE,
|
||||
String.valueOf(proctoringServiceSettings.collectingRoomSize));
|
||||
|
||||
this.additionalAttributesDAO.saveAdditionalAttribute(
|
||||
parentEntityKey.entityType,
|
||||
entityId,
|
||||
ProctoringServiceSettings.ATTR_APP_KEY,
|
||||
StringUtils.trim(proctoringServiceSettings.appKey));
|
||||
if (StringUtils.isNotBlank(proctoringServiceSettings.appKey)) {
|
||||
this.additionalAttributesDAO.saveAdditionalAttribute(
|
||||
parentEntityKey.entityType,
|
||||
entityId,
|
||||
ProctoringServiceSettings.ATTR_APP_KEY,
|
||||
StringUtils.trim(proctoringServiceSettings.appKey));
|
||||
|
||||
this.additionalAttributesDAO.saveAdditionalAttribute(
|
||||
parentEntityKey.entityType,
|
||||
entityId,
|
||||
ProctoringServiceSettings.ATTR_APP_SECRET,
|
||||
this.cryptor.encrypt(Utils.trim(proctoringServiceSettings.appSecret))
|
||||
.getOrThrow()
|
||||
.toString());
|
||||
this.additionalAttributesDAO.saveAdditionalAttribute(
|
||||
parentEntityKey.entityType,
|
||||
entityId,
|
||||
ProctoringServiceSettings.ATTR_APP_SECRET,
|
||||
this.cryptor.encrypt(Utils.trim(proctoringServiceSettings.appSecret))
|
||||
.getOrThrow()
|
||||
.toString());
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(proctoringServiceSettings.accountId)) {
|
||||
this.additionalAttributesDAO.saveAdditionalAttribute(
|
||||
parentEntityKey.entityType,
|
||||
entityId,
|
||||
ProctoringServiceSettings.ATTR_ACCOUNT_ID,
|
||||
StringUtils.trim(proctoringServiceSettings.accountId));
|
||||
this.additionalAttributesDAO.saveAdditionalAttribute(
|
||||
parentEntityKey.entityType,
|
||||
entityId,
|
||||
ProctoringServiceSettings.ATTR_ACCOUNT_CLIENT_ID,
|
||||
StringUtils.trim(proctoringServiceSettings.clientId));
|
||||
|
||||
this.additionalAttributesDAO.saveAdditionalAttribute(
|
||||
parentEntityKey.entityType,
|
||||
entityId,
|
||||
ProctoringServiceSettings.ATTR_ACCOUNT_CLIENT_SECRET,
|
||||
this.cryptor.encrypt(Utils.trim(proctoringServiceSettings.clientSecret))
|
||||
.getOrThrow()
|
||||
.toString());
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(proctoringServiceSettings.sdkKey)) {
|
||||
this.additionalAttributesDAO.saveAdditionalAttribute(
|
||||
|
|
|
@ -17,10 +17,13 @@ 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.util.Result;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ProctoringAdminService;
|
||||
|
||||
/** Defines functionality to deal with proctoring rooms in a generic way (independent from meeting service) */
|
||||
public interface ExamProctoringRoomService {
|
||||
|
||||
ProctoringAdminService getProctoringAdminService();
|
||||
|
||||
/** Get all existing default proctoring rooms of an exam.
|
||||
*
|
||||
* @param examId The exam identifier
|
||||
|
@ -136,4 +139,6 @@ public interface ExamProctoringRoomService {
|
|||
* @return Result refer to void or to an error when happened */
|
||||
Result<Void> notifyRoomOpened(Long examId, String roomName);
|
||||
|
||||
Result<Exam> cleanupAllRooms(Exam exam);
|
||||
|
||||
}
|
||||
|
|
|
@ -84,6 +84,11 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
|
|||
this.sendBroadcastReset = sendBroadcastReset;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProctoringAdminService getProctoringAdminService() {
|
||||
return this.proctoringAdminService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Collection<RemoteProctoringRoom>> getProctoringCollectingRooms(final Long examId) {
|
||||
return this.remoteProctoringRoomDAO.getCollectingRooms(examId);
|
||||
|
@ -298,6 +303,19 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
|
|||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Exam> cleanupAllRooms(final Exam exam) {
|
||||
return this.clientConnectionDAO
|
||||
.getAllActiveConnectionTokens(exam.id)
|
||||
.map(activeConnections -> {
|
||||
if (activeConnections != null && !activeConnections.isEmpty()) {
|
||||
throw new IllegalStateException("There are still active connections for this exam");
|
||||
}
|
||||
return exam;
|
||||
})
|
||||
.flatMap(this::disposeRoomsForExam);
|
||||
}
|
||||
|
||||
private void assignToCollectingRoom(final ClientConnectionRecord cc) {
|
||||
synchronized (RESERVE_ROOM_LOCK) {
|
||||
try {
|
||||
|
|
|
@ -15,7 +15,9 @@ import java.util.Base64;
|
|||
import java.util.Base64.Encoder;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
@ -36,6 +38,10 @@ import org.springframework.http.HttpMethod;
|
|||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext;
|
||||
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
|
||||
import org.springframework.security.oauth2.client.resource.BaseOAuth2ProtectedResourceDetails;
|
||||
import org.springframework.security.oauth2.client.token.DefaultAccessTokenRequest;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestClientResponseException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
@ -86,15 +92,12 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
|
||||
private static final String TOKEN_ENCODE_ALG = "HmacSHA256";
|
||||
|
||||
@Deprecated
|
||||
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}";
|
||||
@Deprecated
|
||||
private static final String ZOOM_SDK_ACCESS_TOKEN_PAYLOAD =
|
||||
"{\"appKey\":\"%s\",\"iat\":%s,\"exp\":%s,\"tokenExp\":%s}";
|
||||
|
||||
private static final Map<String, String> SEB_API_NAME_INSTRUCTION_NAME_MAPPING = Utils.immutableMapOf(Arrays.asList(
|
||||
new Tuple<>(
|
||||
|
@ -125,7 +128,6 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
private final Cryptor cryptor;
|
||||
private final AsyncService asyncService;
|
||||
private final JSONMapper jsonMapper;
|
||||
private final ZoomRestTemplate zoomRestTemplate;
|
||||
private final RemoteProctoringRoomDAO remoteProctoringRoomDAO;
|
||||
private final AuthorizationService authorizationService;
|
||||
private final SEBClientInstructionService sebInstructionService;
|
||||
|
@ -149,7 +151,6 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
this.cryptor = cryptor;
|
||||
this.asyncService = asyncService;
|
||||
this.jsonMapper = jsonMapper;
|
||||
this.zoomRestTemplate = new ZoomRestTemplate(this);
|
||||
this.remoteProctoringRoomDAO = remoteProctoringRoomDAO;
|
||||
this.authorizationService = authorizationService;
|
||||
this.sebInstructionService = sebInstructionService;
|
||||
|
@ -173,16 +174,26 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
|
||||
try {
|
||||
|
||||
final ClientCredentials credentials = new ClientCredentials(
|
||||
final ProctoringServiceSettings proctoringServiceSettings = new ProctoringServiceSettings(
|
||||
proctoringSettings.examId,
|
||||
proctoringSettings.enableProctoring,
|
||||
proctoringSettings.serverType,
|
||||
proctoringSettings.serverURL,
|
||||
proctoringSettings.collectingRoomSize,
|
||||
proctoringSettings.enabledFeatures,
|
||||
proctoringSettings.serviceInUse,
|
||||
proctoringSettings.appKey,
|
||||
this.cryptor
|
||||
.encrypt(proctoringSettings.appSecret)
|
||||
.getOrThrow());
|
||||
this.cryptor.encrypt(proctoringSettings.appSecret).getOrThrow(),
|
||||
proctoringSettings.accountId,
|
||||
proctoringSettings.clientId,
|
||||
this.cryptor.encrypt(proctoringSettings.clientSecret).getOrThrow(),
|
||||
proctoringSettings.sdkKey,
|
||||
this.cryptor.encrypt(proctoringSettings.sdkSecret).getOrThrow(),
|
||||
proctoringSettings.useZoomAppClientForCollectingRoom);
|
||||
|
||||
final ResponseEntity<String> result = this.zoomRestTemplate
|
||||
.testServiceConnection(
|
||||
proctoringSettings.serverURL,
|
||||
credentials);
|
||||
final ZoomRestTemplate newRestTemplate = createNewRestTemplate(proctoringServiceSettings);
|
||||
|
||||
final ResponseEntity<String> result = newRestTemplate.testServiceConnection();
|
||||
|
||||
if (result.getStatusCode() != HttpStatus.OK) {
|
||||
throw new APIMessageException(Arrays.asList(
|
||||
|
@ -415,13 +426,8 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
roomData.getAdditionalRoomData(),
|
||||
AdditionalZoomRoomData.class);
|
||||
|
||||
final ClientCredentials credentials = new ClientCredentials(
|
||||
proctoringSettings.appKey,
|
||||
proctoringSettings.appSecret);
|
||||
|
||||
this.deleteAdHocMeeting(
|
||||
proctoringSettings,
|
||||
credentials,
|
||||
additionalZoomRoomData.meeting_id,
|
||||
additionalZoomRoomData.user_id)
|
||||
.getOrThrow();
|
||||
|
@ -521,15 +527,10 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
final ProctoringServiceSettings proctoringSettings) {
|
||||
|
||||
return Result.tryCatch(() -> {
|
||||
final ClientCredentials credentials = new ClientCredentials(
|
||||
proctoringSettings.appKey,
|
||||
proctoringSettings.appSecret);
|
||||
|
||||
final ZoomRestTemplate zoomRestTemplate = getZoomRestTemplate(proctoringSettings);
|
||||
// First create a new user/host for the new room
|
||||
final ResponseEntity<String> createUser = this.zoomRestTemplate.createUser(
|
||||
proctoringSettings.serverURL,
|
||||
credentials,
|
||||
roomName);
|
||||
final ResponseEntity<String> createUser = zoomRestTemplate.createUser(roomName);
|
||||
|
||||
final int statusCodeValue = createUser.getStatusCodeValue();
|
||||
if (statusCodeValue >= 400) {
|
||||
|
@ -540,16 +541,11 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
createUser.getBody(),
|
||||
UserResponse.class);
|
||||
|
||||
this.zoomRestTemplate.applyUserSettings(
|
||||
proctoringSettings.serverURL,
|
||||
credentials,
|
||||
userResponse.id);
|
||||
zoomRestTemplate.applyUserSettings(userResponse.id);
|
||||
|
||||
// Then create new meeting with the ad-hoc user/host
|
||||
final CharSequence meetingPwd = UUID.randomUUID().toString().subSequence(0, 9);
|
||||
final ResponseEntity<String> createMeeting = this.zoomRestTemplate.createMeeting(
|
||||
proctoringSettings.serverURL,
|
||||
credentials,
|
||||
final ResponseEntity<String> createMeeting = zoomRestTemplate.createMeeting(
|
||||
userResponse.id,
|
||||
subject,
|
||||
duration,
|
||||
|
@ -580,69 +576,18 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
|
||||
private Result<Void> deleteAdHocMeeting(
|
||||
final ProctoringServiceSettings proctoringSettings,
|
||||
final ClientCredentials credentials,
|
||||
final Long meetingId,
|
||||
final String userId) {
|
||||
|
||||
return Result.tryCatch(() -> {
|
||||
|
||||
this.zoomRestTemplate.deleteMeeting(proctoringSettings.serverURL, credentials, meetingId);
|
||||
this.zoomRestTemplate.deleteUser(proctoringSettings.serverURL, credentials, userId);
|
||||
final ZoomRestTemplate zoomRestTemplate = getZoomRestTemplate(proctoringSettings);
|
||||
zoomRestTemplate.deleteMeeting(meetingId);
|
||||
zoomRestTemplate.deleteUser(userId);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
private String createJWTForAPIAccess(
|
||||
final ClientCredentials credentials,
|
||||
final Long expTime) {
|
||||
|
||||
try {
|
||||
|
||||
final CharSequence decryptedSecret = this.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 String createSDKJWT(
|
||||
final ClientCredentials sdkCredentials,
|
||||
final Long expTime,
|
||||
|
@ -706,62 +651,6 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
}
|
||||
}
|
||||
|
||||
// private String createJWTForSDKAccess(
|
||||
// final ClientCredentials sdkCredentials,
|
||||
// final Long expTime) {
|
||||
//
|
||||
// try {
|
||||
//
|
||||
// final CharSequence decryptedSecret = this.cryptor
|
||||
// .decrypt(sdkCredentials.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));
|
||||
//
|
||||
// // epoch time in seconds
|
||||
// final long secondsNow = Utils.getSecondsNow();
|
||||
//
|
||||
// final String jwtPayload = String.format(
|
||||
// ZOOM_SDK_ACCESS_TOKEN_PAYLOAD
|
||||
// .replaceAll(" ", "")
|
||||
// .replaceAll("\n", ""),
|
||||
// sdkCredentials.clientIdAsString(),
|
||||
// secondsNow,
|
||||
// expTime,
|
||||
// expTime);
|
||||
//
|
||||
// if (log.isTraceEnabled()) {
|
||||
// log.trace("Zoom SDK 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 long expiryTimeforExam(final ProctoringServiceSettings examProctoring) {
|
||||
|
||||
// NOTE: following is the original code that includes the exam end time but seems to make trouble for OLAT
|
||||
|
@ -795,44 +684,93 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
return nowPlusOneDayInSeconds - 10;
|
||||
}
|
||||
|
||||
private final static class ZoomRestTemplate {
|
||||
private final LinkedHashMap<Long, ZoomRestTemplate> restTemplatesCache = new LinkedHashMap<>();
|
||||
|
||||
private static final int LIZENSED_USER = 2;
|
||||
private static final String API_TEST_ENDPOINT = "v2/users";
|
||||
private static final String API_CREATE_USER_ENDPOINT = "v2/users";
|
||||
private static final String API_APPLY_USER_SETTINGS_ENDPOINT = "v2/users/{userId}/settings";
|
||||
private static final String API_DELETE_USER_ENDPOINT = "v2/users/{userid}?action=delete";
|
||||
private static final String API_USER_CUST_CREATE = "custCreate";
|
||||
private static final String API_ZOOM_ROOM_USER = "SEBProctoringRoomUser";
|
||||
private static final String API_CREATE_MEETING_ENDPOINT = "v2/users/{userid}/meetings";
|
||||
private static final String API_DELETE_MEETING_ENDPOINT = "v2/meetings/{meetingid}";
|
||||
private static final String API_END_MEETING_ENDPOINT = "v2/meetings/{meetingid}/status";
|
||||
private synchronized ZoomRestTemplate getZoomRestTemplate(final ProctoringServiceSettings proctoringSettings) {
|
||||
if (!this.restTemplatesCache.containsKey(proctoringSettings.examId)) {
|
||||
this.restTemplatesCache.put(
|
||||
proctoringSettings.examId,
|
||||
createNewRestTemplate(proctoringSettings));
|
||||
} else {
|
||||
final ZoomRestTemplate zoomRestTemplate = this.restTemplatesCache.get(proctoringSettings.examId);
|
||||
if (!zoomRestTemplate.isValid(proctoringSettings)) {
|
||||
this.restTemplatesCache.remove(proctoringSettings.examId);
|
||||
this.restTemplatesCache.put(
|
||||
proctoringSettings.examId,
|
||||
createNewRestTemplate(proctoringSettings));
|
||||
}
|
||||
}
|
||||
|
||||
private final ZoomProctoringService zoomProctoringService;
|
||||
private final RestTemplate restTemplate;
|
||||
private final CircuitBreaker<ResponseEntity<String>> circuitBreaker;
|
||||
if (this.restTemplatesCache.size() > 5) {
|
||||
final Long toRemove = this.restTemplatesCache.keySet().iterator().next();
|
||||
if (!Objects.equals(proctoringSettings.examId, toRemove)) {
|
||||
this.restTemplatesCache.remove(toRemove);
|
||||
}
|
||||
}
|
||||
|
||||
public ZoomRestTemplate(final ZoomProctoringService zoomProctoringService) {
|
||||
return this.restTemplatesCache.get(proctoringSettings.examId);
|
||||
}
|
||||
|
||||
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.info("Create new JWTZoomRestTemplate for settings: {}", proctoringSettings);
|
||||
return new JWTZoomRestTemplate(this, proctoringSettings);
|
||||
}
|
||||
}
|
||||
|
||||
private static abstract class ZoomRestTemplate {
|
||||
|
||||
protected static final int LIZENSED_USER = 2;
|
||||
protected static final String API_TEST_ENDPOINT = "v2/users";
|
||||
protected static final String API_CREATE_USER_ENDPOINT = "v2/users";
|
||||
protected static final String API_APPLY_USER_SETTINGS_ENDPOINT = "v2/users/{userId}/settings";
|
||||
protected static final String API_DELETE_USER_ENDPOINT = "v2/users/{userid}?action=delete";
|
||||
protected static final String API_USER_CUST_CREATE = "custCreate";
|
||||
protected static final String API_ZOOM_ROOM_USER = "SEBProctoringRoomUser";
|
||||
protected static final String API_CREATE_MEETING_ENDPOINT = "v2/users/{userid}/meetings";
|
||||
protected static final String API_DELETE_MEETING_ENDPOINT = "v2/meetings/{meetingid}";
|
||||
protected static final String API_END_MEETING_ENDPOINT = "v2/meetings/{meetingid}/status";
|
||||
|
||||
protected final ZoomProctoringService zoomProctoringService;
|
||||
protected final CircuitBreaker<ResponseEntity<String>> circuitBreaker;
|
||||
protected final ProctoringServiceSettings proctoringSettings;
|
||||
|
||||
protected ClientCredentials credentials;
|
||||
protected RestTemplate restTemplate;
|
||||
|
||||
public ZoomRestTemplate(
|
||||
final ZoomProctoringService zoomProctoringService,
|
||||
final ProctoringServiceSettings proctoringSettings) {
|
||||
|
||||
this.zoomProctoringService = zoomProctoringService;
|
||||
this.restTemplate = new RestTemplate(zoomProctoringService.clientHttpRequestFactoryService
|
||||
.getClientHttpRequestFactory()
|
||||
.getOrThrow());
|
||||
|
||||
this.circuitBreaker = zoomProctoringService.asyncService.createCircuitBreaker(
|
||||
2,
|
||||
10 * Constants.SECOND_IN_MILLIS,
|
||||
10 * Constants.SECOND_IN_MILLIS);
|
||||
this.proctoringSettings = proctoringSettings;
|
||||
initConnection();
|
||||
}
|
||||
|
||||
public ResponseEntity<String> testServiceConnection(
|
||||
final String zoomServerUrl,
|
||||
final ClientCredentials credentials) {
|
||||
protected abstract void initConnection();
|
||||
|
||||
protected abstract HttpHeaders getHeaders();
|
||||
|
||||
boolean isValid(final ProctoringServiceSettings proctoringSettings) {
|
||||
return Objects.equals(proctoringSettings.serverURL, this.proctoringSettings.serverURL) &&
|
||||
Objects.equals(proctoringSettings.appKey, this.proctoringSettings.appKey) &&
|
||||
Objects.equals(proctoringSettings.accountId, this.proctoringSettings.accountId) &&
|
||||
Objects.equals(proctoringSettings.clientId, this.proctoringSettings.clientId);
|
||||
}
|
||||
|
||||
public ResponseEntity<String> testServiceConnection() {
|
||||
|
||||
try {
|
||||
|
||||
final String url = UriComponentsBuilder
|
||||
.fromUriString(zoomServerUrl)
|
||||
.fromUriString(this.proctoringSettings.serverURL)
|
||||
.path(API_TEST_ENDPOINT)
|
||||
.queryParam("status", "active")
|
||||
.queryParam("page_size", "10")
|
||||
|
@ -840,7 +778,7 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
.queryParam("data_type", "Json")
|
||||
.build()
|
||||
.toUriString();
|
||||
return exchange(url, HttpMethod.GET, credentials);
|
||||
return exchange(url, HttpMethod.GET);
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to test zoom service connection: ", e);
|
||||
|
@ -848,17 +786,15 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
}
|
||||
}
|
||||
|
||||
public ResponseEntity<String> createUser(
|
||||
final String zoomServerUrl,
|
||||
final ClientCredentials credentials,
|
||||
final String roomName) {
|
||||
public ResponseEntity<String> createUser(final String roomName) {
|
||||
|
||||
try {
|
||||
|
||||
final String url = UriComponentsBuilder
|
||||
.fromUriString(zoomServerUrl)
|
||||
.fromUriString(this.proctoringSettings.serverURL)
|
||||
.path(API_CREATE_USER_ENDPOINT)
|
||||
.toUriString();
|
||||
final String host = new URL(zoomServerUrl).getHost();
|
||||
final String host = new URL(this.proctoringSettings.serverURL).getHost();
|
||||
final CreateUserRequest createUserRequest = new CreateUserRequest(
|
||||
API_USER_CUST_CREATE,
|
||||
new CreateUserRequest.UserInfo(
|
||||
|
@ -868,7 +804,7 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
API_ZOOM_ROOM_USER));
|
||||
|
||||
final String body = this.zoomProctoringService.jsonMapper.writeValueAsString(createUserRequest);
|
||||
final HttpHeaders headers = getHeaders(credentials);
|
||||
final HttpHeaders headers = getHeaders();
|
||||
headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
|
||||
return exchange(url, HttpMethod.POST, body, headers);
|
||||
|
||||
|
@ -878,13 +814,10 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
}
|
||||
}
|
||||
|
||||
public ResponseEntity<String> applyUserSettings(
|
||||
final String zoomServerUrl,
|
||||
final ClientCredentials credentials,
|
||||
final String userId) {
|
||||
public ResponseEntity<String> applyUserSettings(final String userId) {
|
||||
try {
|
||||
final String url = UriComponentsBuilder
|
||||
.fromUriString(zoomServerUrl)
|
||||
.fromUriString(this.proctoringSettings.serverURL)
|
||||
.path(API_APPLY_USER_SETTINGS_ENDPOINT)
|
||||
.buildAndExpand(userId)
|
||||
.normalize()
|
||||
|
@ -892,7 +825,7 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
|
||||
final ApplyUserSettingsRequest applySettingsRequest = new ApplyUserSettingsRequest();
|
||||
final String body = this.zoomProctoringService.jsonMapper.writeValueAsString(applySettingsRequest);
|
||||
final HttpHeaders headers = getHeaders(credentials);
|
||||
final HttpHeaders headers = getHeaders();
|
||||
|
||||
headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
|
||||
final ResponseEntity<String> exchange = exchange(url, HttpMethod.PATCH, body, headers);
|
||||
|
@ -904,8 +837,6 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
}
|
||||
|
||||
public ResponseEntity<String> createMeeting(
|
||||
final String zoomServerUrl,
|
||||
final ClientCredentials credentials,
|
||||
final String userId,
|
||||
final String topic,
|
||||
final int duration,
|
||||
|
@ -915,7 +846,7 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
try {
|
||||
|
||||
final String url = UriComponentsBuilder
|
||||
.fromUriString(zoomServerUrl)
|
||||
.fromUriString(this.proctoringSettings.serverURL)
|
||||
.path(API_CREATE_MEETING_ENDPOINT)
|
||||
.buildAndExpand(userId)
|
||||
.toUriString();
|
||||
|
@ -927,7 +858,7 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
waitingRoom);
|
||||
|
||||
final String body = this.zoomProctoringService.jsonMapper.writeValueAsString(createRoomRequest);
|
||||
final HttpHeaders headers = getHeaders(credentials);
|
||||
final HttpHeaders headers = getHeaders();
|
||||
headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
|
||||
return exchange(url, HttpMethod.POST, body, headers);
|
||||
|
||||
|
@ -937,21 +868,18 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
}
|
||||
}
|
||||
|
||||
public ResponseEntity<String> deleteMeeting(
|
||||
final String zoomServerUrl,
|
||||
final ClientCredentials credentials,
|
||||
final Long meetingId) {
|
||||
public ResponseEntity<String> deleteMeeting(final Long meetingId) {
|
||||
|
||||
// try to set set meeting status to ended first
|
||||
try {
|
||||
|
||||
final String url = UriComponentsBuilder
|
||||
.fromUriString(zoomServerUrl)
|
||||
.fromUriString(this.proctoringSettings.serverURL)
|
||||
.path(API_END_MEETING_ENDPOINT)
|
||||
.buildAndExpand(meetingId)
|
||||
.toUriString();
|
||||
|
||||
final HttpHeaders headers = getHeaders(credentials);
|
||||
final HttpHeaders headers = getHeaders();
|
||||
headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
|
||||
|
||||
final ResponseEntity<String> exchange = exchange(
|
||||
|
@ -976,12 +904,12 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
try {
|
||||
|
||||
final String url = UriComponentsBuilder
|
||||
.fromUriString(zoomServerUrl)
|
||||
.fromUriString(this.proctoringSettings.serverURL)
|
||||
.path(API_DELETE_MEETING_ENDPOINT)
|
||||
.buildAndExpand(meetingId)
|
||||
.toUriString();
|
||||
|
||||
return exchange(url, HttpMethod.DELETE, credentials);
|
||||
return exchange(url, HttpMethod.DELETE);
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.warn("Failed to delete Zoom ad-hoc meeting: {} cause: {} / {}",
|
||||
|
@ -992,20 +920,17 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
}
|
||||
}
|
||||
|
||||
public ResponseEntity<String> deleteUser(
|
||||
final String zoomServerUrl,
|
||||
final ClientCredentials credentials,
|
||||
final String userId) {
|
||||
public ResponseEntity<String> deleteUser(final String userId) {
|
||||
|
||||
try {
|
||||
final String url = UriComponentsBuilder
|
||||
.fromUriString(zoomServerUrl)
|
||||
.fromUriString(this.proctoringSettings.serverURL)
|
||||
.path(API_DELETE_USER_ENDPOINT)
|
||||
.buildAndExpand(userId)
|
||||
.normalize()
|
||||
.toUriString();
|
||||
|
||||
return exchange(url, HttpMethod.DELETE, credentials);
|
||||
return exchange(url, HttpMethod.DELETE);
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to delete Zoom ad-hoc user with id: {} cause: {} / {}",
|
||||
|
@ -1016,24 +941,11 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
}
|
||||
}
|
||||
|
||||
private HttpHeaders getHeaders(final ClientCredentials credentials) {
|
||||
final String jwt = this.zoomProctoringService
|
||||
.createJWTForAPIAccess(
|
||||
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 ResponseEntity<String> exchange(
|
||||
final String url,
|
||||
final HttpMethod method,
|
||||
final ClientCredentials credentials) {
|
||||
final HttpMethod method) {
|
||||
|
||||
return exchange(url, method, null, getHeaders(credentials));
|
||||
return exchange(url, method, null, getHeaders());
|
||||
}
|
||||
|
||||
private ResponseEntity<String> exchange(
|
||||
|
@ -1070,4 +982,138 @@ public class ZoomProctoringService implements ExamProctoringService {
|
|||
}
|
||||
}
|
||||
|
||||
private final static class OAuthZoomRestTemplate extends ZoomRestTemplate {
|
||||
|
||||
private BaseOAuth2ProtectedResourceDetails resource;
|
||||
|
||||
public OAuthZoomRestTemplate(
|
||||
final ZoomProctoringService zoomProctoringService,
|
||||
final ProctoringServiceSettings proctoringSettings) {
|
||||
|
||||
super(zoomProctoringService, proctoringSettings);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initConnection() {
|
||||
|
||||
if (this.resource == null) {
|
||||
|
||||
this.credentials = new ClientCredentials(
|
||||
this.proctoringSettings.clientId,
|
||||
this.proctoringSettings.clientSecret);
|
||||
|
||||
final CharSequence decryptedSecret = this.zoomProctoringService.cryptor
|
||||
.decrypt(this.credentials.secret)
|
||||
.getOrThrow();
|
||||
|
||||
this.resource = new BaseOAuth2ProtectedResourceDetails();
|
||||
this.resource.setAccessTokenUri(this.proctoringSettings.serverURL + "/oauth/token");
|
||||
this.resource.setClientId(this.credentials.clientIdAsString());
|
||||
this.resource.setClientSecret(decryptedSecret.toString());
|
||||
this.resource.setGrantType("account_credentials");
|
||||
|
||||
final DefaultAccessTokenRequest defaultAccessTokenRequest = new DefaultAccessTokenRequest();
|
||||
defaultAccessTokenRequest.set("account_id", this.proctoringSettings.accountId);
|
||||
this.restTemplate = new OAuth2RestTemplate(
|
||||
this.resource,
|
||||
new DefaultOAuth2ClientContext(defaultAccessTokenRequest));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders getHeaders() {
|
||||
final HttpHeaders httpHeaders = new HttpHeaders();
|
||||
httpHeaders.set(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
|
||||
return httpHeaders;
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -236,7 +236,7 @@ public interface ZoomRoomRequestResponse {
|
|||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
static class Settings {
|
||||
@JsonProperty final boolean host_video = false;
|
||||
@JsonProperty final boolean host_video = true;
|
||||
@JsonProperty final boolean mute_upon_entry = false;
|
||||
@JsonProperty final boolean join_before_host;
|
||||
@JsonProperty final int jbh_time = 0;
|
||||
|
|
|
@ -25,7 +25,6 @@ public class ProctoringSettingsValidator
|
|||
return false;
|
||||
}
|
||||
|
||||
//if (value.enableProctoring) {
|
||||
if (value.serverType == ProctoringServerType.JITSI_MEET || value.serverType == ProctoringServerType.ZOOM) {
|
||||
boolean passed = true;
|
||||
|
||||
|
@ -37,25 +36,44 @@ public class ProctoringSettingsValidator
|
|||
passed = false;
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(value.appKey)) {
|
||||
context.disableDefaultConstraintViolation();
|
||||
context
|
||||
.buildConstraintViolationWithTemplate("proctoringSettings:appKey:notNull")
|
||||
.addPropertyNode("appKey").addConstraintViolation();
|
||||
passed = false;
|
||||
if (value.serverType == ProctoringServerType.JITSI_MEET) {
|
||||
if (StringUtils.isBlank(value.appKey)) {
|
||||
context.disableDefaultConstraintViolation();
|
||||
context
|
||||
.buildConstraintViolationWithTemplate("proctoringSettings:appKey:notNull")
|
||||
.addPropertyNode("appKey").addConstraintViolation();
|
||||
passed = false;
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(value.appSecret)) {
|
||||
context.disableDefaultConstraintViolation();
|
||||
context
|
||||
.buildConstraintViolationWithTemplate("proctoringSettings:appSecret:notNull")
|
||||
.addPropertyNode("appSecret").addConstraintViolation();
|
||||
passed = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(value.appSecret)) {
|
||||
context.disableDefaultConstraintViolation();
|
||||
context
|
||||
.buildConstraintViolationWithTemplate("proctoringSettings:appSecret:notNull")
|
||||
.addPropertyNode("appSecret").addConstraintViolation();
|
||||
passed = false;
|
||||
if (value.serverType == ProctoringServerType.ZOOM) {
|
||||
if (StringUtils.isBlank(value.sdkKey)) {
|
||||
context.disableDefaultConstraintViolation();
|
||||
context
|
||||
.buildConstraintViolationWithTemplate("proctoringSettings:sdkKey:notNull")
|
||||
.addPropertyNode("sdkKey").addConstraintViolation();
|
||||
passed = false;
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(value.sdkSecret)) {
|
||||
context.disableDefaultConstraintViolation();
|
||||
context
|
||||
.buildConstraintViolationWithTemplate("proctoringSettings:sdkSecret:notNull")
|
||||
.addPropertyNode("sdkSecret").addConstraintViolation();
|
||||
passed = false;
|
||||
}
|
||||
}
|
||||
|
||||
return passed;
|
||||
}
|
||||
//}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -71,6 +71,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamTemplateService;
|
|||
import ch.ethz.seb.sebserver.webservice.servicelayer.institution.SecurityKeyService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.SEBRestrictionService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamProctoringRoomService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService;
|
||||
|
||||
|
@ -87,6 +88,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
|||
private final ExamSessionService examSessionService;
|
||||
private final SEBRestrictionService sebRestrictionService;
|
||||
private final SecurityKeyService securityKeyService;
|
||||
private final ExamProctoringRoomService examProctoringRoomService;
|
||||
|
||||
public ExamAdministrationController(
|
||||
final AuthorizationService authorization,
|
||||
|
@ -101,7 +103,8 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
|||
final ExamTemplateService examTemplateService,
|
||||
final ExamSessionService examSessionService,
|
||||
final SEBRestrictionService sebRestrictionService,
|
||||
final SecurityKeyService securityKeyService) {
|
||||
final SecurityKeyService securityKeyService,
|
||||
final ExamProctoringRoomService examProctoringRoomService) {
|
||||
|
||||
super(authorization,
|
||||
bulkActionService,
|
||||
|
@ -118,6 +121,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
|||
this.examSessionService = examSessionService;
|
||||
this.sebRestrictionService = sebRestrictionService;
|
||||
this.securityKeyService = securityKeyService;
|
||||
this.examProctoringRoomService = examProctoringRoomService;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -486,6 +490,29 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
|||
.getOrThrow();
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
path = API.MODEL_ID_VAR_PATH_SEGMENT
|
||||
+ API.EXAM_ADMINISTRATION_PROCTORING_PATH_SEGMENT
|
||||
+ API.EXAM_ADMINISTRATION_PROCTORING_RESET_PATH_SEGMENT,
|
||||
method = RequestMethod.POST,
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public Exam resetProctoringServiceSettings(
|
||||
@RequestParam(
|
||||
name = API.PARAM_INSTITUTION_ID,
|
||||
required = true,
|
||||
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId,
|
||||
@PathVariable(API.PARAM_MODEL_ID) final Long examId) {
|
||||
|
||||
checkModifyPrivilege(institutionId);
|
||||
|
||||
return this.entityDAO
|
||||
.byPK(examId)
|
||||
.flatMap(this.examProctoringRoomService::cleanupAllRooms)
|
||||
.flatMap(this.examAdminService::resetProctoringSettings)
|
||||
.getOrThrow();
|
||||
|
||||
}
|
||||
|
||||
// **** Proctoring
|
||||
// ****************************************************************************
|
||||
|
||||
|
|
|
@ -483,6 +483,7 @@ sebserver.quizdiscovery.quiz.details.additional.time_limit.toolitp=The time limi
|
|||
################################
|
||||
|
||||
sebserver.exam.list.actions=
|
||||
sebserver.exam.security.actions=
|
||||
sebserver.exam.list.title=Exam
|
||||
sebserver.exam.list.title.subtitle=
|
||||
sebserver.exam.list.column.institution=Institution
|
||||
|
@ -795,14 +796,28 @@ sebserver.exam.proctoring.form.url=Server URL
|
|||
sebserver.exam.proctoring.form.url.tooltip=The proctoring server URL
|
||||
sebserver.exam.proctoring.form.collectingRoomSize=Collecting Room Size
|
||||
sebserver.exam.proctoring.form.collectingRoomSize.tooltip=The size of proctor rooms to collect connecting SEB clients into.
|
||||
sebserver.exam.proctoring.form.appkey=App Key
|
||||
sebserver.exam.proctoring.form.appkey.tooltip=The application key of the proctoring service server
|
||||
sebserver.exam.proctoring.form.secret=App Secret
|
||||
sebserver.exam.proctoring.form.secret.tooltip=The secret used to access the proctoring service
|
||||
sebserver.exam.proctoring.form.sdkkey=SDK Key (Zoom - MacOS/iOS)
|
||||
sebserver.exam.proctoring.form.sdkkey.tooltip=The SDK key and secret are used for live proctoring with SEB clients for iOS and/or MacOS<br/>This is only relevant for proctoring with Zoom service.
|
||||
sebserver.exam.proctoring.form.sdksecret=SDK Secret (Zoom - MacOS/iOS)
|
||||
sebserver.exam.proctoring.form.sdksecret.tooltip=The SDK key and secret are used for live proctoring with SEB clients for iOS and/or MacOS<br/>This is only relevant for proctoring with Zoom service.
|
||||
|
||||
sebserver.exam.proctoring.form.appkey.jitsi=App Key
|
||||
sebserver.exam.proctoring.form.appkey.jitsi.tooltip=The application key of the proctoring service server
|
||||
sebserver.exam.proctoring.form.secret.jitsi=App Secret
|
||||
sebserver.exam.proctoring.form.secret.jitsi.tooltip=The secret used to access the proctoring service
|
||||
|
||||
sebserver.exam.proctoring.form.appkey.zoom=App Key (Deprecated)
|
||||
sebserver.exam.proctoring.form.appkey.zoom.tooltip=Since Zoom no longer supports JWT App, this is deprecated and the below Account ID, Client ID and Client Secret must be used
|
||||
sebserver.exam.proctoring.form.secret.zoom=App Secret (Deprecated)
|
||||
sebserver.exam.proctoring.form.secret.zoom.tooltip=Since Zoom no longer supports JWT App, this is deprecated and the below Account ID, Client ID and Client Secret must be used
|
||||
|
||||
sebserver.exam.proctoring.form.accountId=Account ID
|
||||
sebserver.exam.proctoring.form.accountId.tooltip=This is the Account ID from the Zoom Server-to-Server OAuth application and needed by SEB server to automatically create meetings
|
||||
sebserver.exam.proctoring.form.clientId=Client ID
|
||||
sebserver.exam.proctoring.form.clientId.tooltip=This is the Client ID from the Zoom Server-to-Server OAuth application and needed by SEB server to automatically create meetings
|
||||
sebserver.exam.proctoring.form.clientSecret=Client Secret
|
||||
sebserver.exam.proctoring.form.clientSecret.tooltip=This is the Client Secret from the Zoom Server-to-Server OAuth application and needed by SEB server to automatically create meetings
|
||||
|
||||
sebserver.exam.proctoring.form.sdkkey=SDK Key
|
||||
sebserver.exam.proctoring.form.sdkkey.tooltip=The SDK key and secret are used for live proctoring with SEB clients<br/>This is only relevant for proctoring with Zoom service.
|
||||
sebserver.exam.proctoring.form.sdksecret=SDK Secret
|
||||
sebserver.exam.proctoring.form.sdksecret.tooltip=The SDK key and secret are used for live proctoring with SEB clients<br/>This is only relevant for proctoring with Zoom service.
|
||||
sebserver.exam.proctoring.form.features=Enabled Features
|
||||
sebserver.exam.proctoring.form.features.TOWN_HALL=Town-Hall Room
|
||||
sebserver.exam.proctoring.form.features.ONE_TO_ONE=One to One Room
|
||||
|
@ -813,6 +828,10 @@ sebserver.exam.proctoring.form.features.SEND_REJOIN_COLLECTING_ROOM=Force rejoin
|
|||
sebserver.exam.proctoring.form.features.RESET_BROADCAST_ON_LAVE=Reset broadcast on leave
|
||||
sebserver.exam.proctoring.form.useZoomAppClient=Use Zoom App-Client
|
||||
sebserver.exam.proctoring.form.useZoomAppClient.tooltip=If this is set SEB Server opens a start link for the meeting instead of a new popup-window with the Zoom Web Client.<br/>A Zoom App Client must already be installed on the proctor's device or can be installed by following the instructions shown in the browser window.
|
||||
sebserver.exam.proctoring.form.saveSettings=Save Settings
|
||||
sebserver.exam.proctoring.form.resetSettings=Reset Settings
|
||||
sebserver.exam.proctoring.form.resetConfirm=Reset will first cleanup and remove all existing proctoring rooms for this exam and then reset to default or template settings.<br/>Please make sure there are no active SEB client connections for this exam, otherwise this reset will be denied.<br/><br/>Do you want to reset proctoring for this exam now?
|
||||
|
||||
|
||||
sebserver.exam.proctoring.type.servertype.JITSI_MEET=Jitsi Meet Server
|
||||
sebserver.exam.proctoring.type.servertype.JITSI_MEET.tooltip=Use a Jitsi Meet server for proctoring
|
||||
|
@ -829,7 +848,9 @@ sebserver.exam.proctoring.collecting.close.error=Failed to close the collecting
|
|||
sebserver.exam.signaturekey.action.edit=App Signature Key
|
||||
sebserver.exam.signaturekey.action.save=Save Settings
|
||||
sebserver.exam.signaturekey.action.cancel=Cancel and back to Exam
|
||||
sebserver.exam.signaturekey.action.back=Back to Exam
|
||||
sebserver.exam.signaturekey.action.addGrant=Add Security Grant
|
||||
sebserver.exam.signaturekey.action.showASK=Show App Signature Key
|
||||
sebserver.exam.signaturekey.action.showGrant=Show Security Grant
|
||||
sebserver.exam.signaturekey.action.deleteGrant=Delete Security Grant
|
||||
sebserver.exam.signaturekey.title=App Signature Key Overview
|
||||
|
|
BIN
src/main/resources/static/images/back.png
Normal file
BIN
src/main/resources/static/images/back.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 100 B |
|
@ -3597,6 +3597,9 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest {
|
|||
false,
|
||||
"apiKey",
|
||||
"apiSecret",
|
||||
"accountId",
|
||||
"clientId",
|
||||
"clientSecret",
|
||||
"sdkKey",
|
||||
"sdkSecret",
|
||||
false);
|
||||
|
@ -3721,6 +3724,7 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest {
|
|||
EnumSet.allOf(ProctoringFeature.class),
|
||||
true,
|
||||
"appKey", "appSecret",
|
||||
"accountId", "clientId", "clientSecret",
|
||||
"sdkKey", "sdkSecret",
|
||||
false);
|
||||
|
||||
|
|
|
@ -78,7 +78,9 @@ public class ExamProctoringRoomServiceTest extends AdministrationAPIIntegrationT
|
|||
2L,
|
||||
new ProctoringServiceSettings(
|
||||
2L, true, ProctoringServerType.JITSI_MEET, "", 1, null, false,
|
||||
"app-key", "app.secret", "sdk-key", "sdk.secret", false));
|
||||
"app-key", "app.secret", "accountId",
|
||||
"clientId",
|
||||
"clientSecret", "sdk-key", "sdk.secret", false));
|
||||
|
||||
assertTrue(this.examAdminService.isProctoringEnabled(2L).get());
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ public class ExamJITSIProctoringServiceTest {
|
|||
|
||||
final ProctoringServiceSettings proctoringServiceSettings = new ProctoringServiceSettings(
|
||||
1L, true, ProctoringServerType.JITSI_MEET, "URL?",
|
||||
null, null, null, null, null, null, null, null);
|
||||
null, null, null, null, null, null, null, null, null, null, null);
|
||||
|
||||
final Result<Boolean> testExamProctoring = jitsiProctoringService
|
||||
.testExamProctoring(proctoringServiceSettings);
|
||||
|
@ -59,7 +59,9 @@ public class ExamJITSIProctoringServiceTest {
|
|||
|
||||
final ProctoringServiceSettings proctoringServiceSettings = new ProctoringServiceSettings(
|
||||
1L, true, ProctoringServerType.JITSI_MEET, "http://jitsi.ch",
|
||||
2, null, true, "key", "secret", null, null, false);
|
||||
2, null, true, "key", "secret", "accountId",
|
||||
"clientId",
|
||||
"clientSecret", null, null, false);
|
||||
|
||||
final Result<ProctoringRoomConnection> proctorRoomConnection = jitsiProctoringService
|
||||
.getProctorRoomConnection(proctoringServiceSettings, "TestRoom", "Test-User");
|
||||
|
|
Loading…
Reference in a new issue