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_CHECK_IMPORTED_PATH_SEGMENT = "/check-imported";
|
||||||
public static final String EXAM_ADMINISTRATION_SEB_RESTRICTION_CHAPTERS_PATH_SEGMENT = "/chapters";
|
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_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_GRANTS_PATH_SEGMENT = "/grant";
|
||||||
public static final String EXAM_ADMINISTRATION_SEB_SECURITY_KEY_INFO_PATH_SEGMENT = "/sebkeyinfo";
|
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_ENABLE_PROCTORING = "enableProctoring";
|
||||||
public static final String ATTR_SERVER_TYPE = "serverType";
|
public static final String ATTR_SERVER_TYPE = "serverType";
|
||||||
public static final String ATTR_SERVER_URL = "serverURL";
|
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_KEY = "appKey";
|
||||||
public static final String ATTR_APP_SECRET = "appSecret";
|
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_KEY = "sdkKey";
|
||||||
public static final String ATTR_SDK_SECRET = "sdkSecret";
|
public static final String ATTR_SDK_SECRET = "sdkSecret";
|
||||||
|
|
||||||
public static final String ATTR_COLLECTING_ROOM_SIZE = "collectingRoomSize";
|
public static final String ATTR_COLLECTING_ROOM_SIZE = "collectingRoomSize";
|
||||||
public static final String ATTR_ENABLED_FEATURES = "enabledFeatures";
|
public static final String ATTR_ENABLED_FEATURES = "enabledFeatures";
|
||||||
public static final String ATTR_COLLECT_ALL_ROOM_NAME = "collectAllRoomName";
|
public static final String ATTR_COLLECT_ALL_ROOM_NAME = "collectAllRoomName";
|
||||||
|
@ -71,6 +79,15 @@ public class ProctoringServiceSettings implements Entity {
|
||||||
@JsonProperty(ATTR_APP_SECRET)
|
@JsonProperty(ATTR_APP_SECRET)
|
||||||
public final CharSequence appSecret;
|
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)
|
@JsonProperty(ATTR_SDK_KEY)
|
||||||
public final String sdkKey;
|
public final String sdkKey;
|
||||||
|
|
||||||
|
@ -100,6 +117,9 @@ public class ProctoringServiceSettings implements Entity {
|
||||||
@JsonProperty(ATTR_SERVICE_IN_USE) final Boolean serviceInUse,
|
@JsonProperty(ATTR_SERVICE_IN_USE) final Boolean serviceInUse,
|
||||||
@JsonProperty(ATTR_APP_KEY) final String appKey,
|
@JsonProperty(ATTR_APP_KEY) final String appKey,
|
||||||
@JsonProperty(ATTR_APP_SECRET) final CharSequence appSecret,
|
@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_KEY) final String sdkKey,
|
||||||
@JsonProperty(ATTR_SDK_SECRET) final CharSequence sdkSecret,
|
@JsonProperty(ATTR_SDK_SECRET) final CharSequence sdkSecret,
|
||||||
@JsonProperty(ATTR_USE_ZOOM_APP_CLIENT_COLLECTING_ROOM) final Boolean useZoomAppClientForCollectingRoom) {
|
@JsonProperty(ATTR_USE_ZOOM_APP_CLIENT_COLLECTING_ROOM) final Boolean useZoomAppClientForCollectingRoom) {
|
||||||
|
@ -113,11 +133,50 @@ public class ProctoringServiceSettings implements Entity {
|
||||||
this.serviceInUse = serviceInUse;
|
this.serviceInUse = serviceInUse;
|
||||||
this.appKey = StringUtils.trim(appKey);
|
this.appKey = StringUtils.trim(appKey);
|
||||||
this.appSecret = appSecret;
|
this.appSecret = appSecret;
|
||||||
|
this.accountId = StringUtils.trim(accountId);
|
||||||
|
this.clientId = clientId;
|
||||||
|
this.clientSecret = clientSecret;
|
||||||
this.sdkKey = StringUtils.trim(sdkKey);
|
this.sdkKey = StringUtils.trim(sdkKey);
|
||||||
this.sdkSecret = sdkSecret;
|
this.sdkSecret = sdkSecret;
|
||||||
this.useZoomAppClientForCollectingRoom = BooleanUtils.toBoolean(useZoomAppClientForCollectingRoom);
|
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
|
@Override
|
||||||
public String getModelId() {
|
public String getModelId() {
|
||||||
return (this.examId != null) ? String.valueOf(this.examId) : null;
|
return (this.examId != null) ? String.valueOf(this.examId) : null;
|
||||||
|
@ -165,6 +224,18 @@ public class ProctoringServiceSettings implements Entity {
|
||||||
return this.appSecret;
|
return this.appSecret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getAccountId() {
|
||||||
|
return this.accountId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getClientId() {
|
||||||
|
return this.clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CharSequence getClientSecret() {
|
||||||
|
return this.clientSecret;
|
||||||
|
}
|
||||||
|
|
||||||
public String getSdkKey() {
|
public String getSdkKey() {
|
||||||
return this.sdkKey;
|
return this.sdkKey;
|
||||||
}
|
}
|
||||||
|
@ -222,18 +293,20 @@ public class ProctoringServiceSettings implements Entity {
|
||||||
builder.append(this.serverURL);
|
builder.append(this.serverURL);
|
||||||
builder.append(", appKey=");
|
builder.append(", appKey=");
|
||||||
builder.append(this.appKey);
|
builder.append(this.appKey);
|
||||||
builder.append(", appSecret=");
|
builder.append(", accountId=");
|
||||||
builder.append("--");
|
builder.append(this.accountId);
|
||||||
|
builder.append(", clientId=");
|
||||||
|
builder.append(this.clientId);
|
||||||
builder.append(", sdkKey=");
|
builder.append(", sdkKey=");
|
||||||
builder.append(this.sdkKey);
|
builder.append(this.sdkKey);
|
||||||
builder.append(", sdkSecret=");
|
|
||||||
builder.append("--");
|
|
||||||
builder.append(", collectingRoomSize=");
|
builder.append(", collectingRoomSize=");
|
||||||
builder.append(this.collectingRoomSize);
|
builder.append(this.collectingRoomSize);
|
||||||
builder.append(", enabledFeatures=");
|
builder.append(", enabledFeatures=");
|
||||||
builder.append(this.enabledFeatures);
|
builder.append(this.enabledFeatures);
|
||||||
builder.append(", serviceInUse=");
|
builder.append(", serviceInUse=");
|
||||||
builder.append(this.serviceInUse);
|
builder.append(this.serviceInUse);
|
||||||
|
builder.append(", useZoomAppClientForCollectingRoom=");
|
||||||
|
builder.append(this.useZoomAppClientForCollectingRoom);
|
||||||
builder.append("]");
|
builder.append("]");
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ import java.util.Objects;
|
||||||
|
|
||||||
import javax.validation.constraints.NotNull;
|
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.JsonCreator;
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
@ -68,7 +68,7 @@ public class AppSignatureKeyInfo implements ModelIdAware {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getModelId() {
|
public String getModelId() {
|
||||||
return this.key;
|
return StringUtils.isNoneBlank(this.key) ? this.key : "-1";
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getKey() {
|
public String getKey() {
|
||||||
|
|
|
@ -18,6 +18,7 @@ public enum ActionCategory {
|
||||||
LMS_SETUP_LIST(new LocTextKey("sebserver.lmssetup.list.actions"), 1),
|
LMS_SETUP_LIST(new LocTextKey("sebserver.lmssetup.list.actions"), 1),
|
||||||
QUIZ_LIST(new LocTextKey("sebserver.quizdiscovery.list.actions"), 1),
|
QUIZ_LIST(new LocTextKey("sebserver.quizdiscovery.list.actions"), 1),
|
||||||
EXAM_LIST(new LocTextKey("sebserver.exam.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),
|
EXAM_TEMPLATE_LIST(new LocTextKey("sebserver.examtemplate.list.actions"), 1),
|
||||||
INDICATOR_TEMPLATE_LIST(new LocTextKey("sebserver.examtemplate.indicator.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),
|
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"),
|
new LocTextKey("sebserver.exam.action.sebrestriction.details"),
|
||||||
ImageIcon.RESTRICTION,
|
ImageIcon.RESTRICTION,
|
||||||
PageStateDefinitionImpl.EXAM_VIEW,
|
PageStateDefinitionImpl.EXAM_VIEW,
|
||||||
ActionCategory.FORM),
|
ActionCategory.EXAM_SECURITY),
|
||||||
EXAM_ENABLE_SEB_RESTRICTION(
|
EXAM_ENABLE_SEB_RESTRICTION(
|
||||||
new LocTextKey("sebserver.exam.action.sebrestriction.enable"),
|
new LocTextKey("sebserver.exam.action.sebrestriction.enable"),
|
||||||
ImageIcon.UNLOCK,
|
ImageIcon.UNLOCK,
|
||||||
PageStateDefinitionImpl.EXAM_VIEW,
|
PageStateDefinitionImpl.EXAM_VIEW,
|
||||||
ActionCategory.FORM),
|
ActionCategory.EXAM_SECURITY),
|
||||||
EXAM_DISABLE_SEB_RESTRICTION(
|
EXAM_DISABLE_SEB_RESTRICTION(
|
||||||
new LocTextKey("sebserver.exam.action.sebrestriction.disable"),
|
new LocTextKey("sebserver.exam.action.sebrestriction.disable"),
|
||||||
ImageIcon.LOCK,
|
ImageIcon.LOCK,
|
||||||
PageStateDefinitionImpl.EXAM_VIEW,
|
PageStateDefinitionImpl.EXAM_VIEW,
|
||||||
ActionCategory.FORM),
|
ActionCategory.EXAM_SECURITY),
|
||||||
EXAM_PROCTORING_ON(
|
EXAM_PROCTORING_ON(
|
||||||
new LocTextKey("sebserver.exam.proctoring.actions.open"),
|
new LocTextKey("sebserver.exam.proctoring.actions.open"),
|
||||||
ImageIcon.VISIBILITY,
|
ImageIcon.VISIBILITY,
|
||||||
PageStateDefinitionImpl.EXAM_VIEW,
|
PageStateDefinitionImpl.EXAM_VIEW,
|
||||||
ActionCategory.FORM),
|
ActionCategory.EXAM_SECURITY),
|
||||||
EXAM_PROCTORING_OFF(
|
EXAM_PROCTORING_OFF(
|
||||||
new LocTextKey("sebserver.exam.proctoring.actions.open"),
|
new LocTextKey("sebserver.exam.proctoring.actions.open"),
|
||||||
ImageIcon.VISIBILITY_OFF,
|
ImageIcon.VISIBILITY_OFF,
|
||||||
PageStateDefinitionImpl.EXAM_VIEW,
|
PageStateDefinitionImpl.EXAM_VIEW,
|
||||||
ActionCategory.FORM),
|
ActionCategory.EXAM_SECURITY),
|
||||||
|
|
||||||
EXAM_CONFIGURATION_NEW(
|
EXAM_CONFIGURATION_NEW(
|
||||||
new LocTextKey("sebserver.exam.configuration.action.list.new"),
|
new LocTextKey("sebserver.exam.configuration.action.list.new"),
|
||||||
|
@ -419,12 +419,12 @@ public enum ActionDefinition {
|
||||||
new LocTextKey("sebserver.exam.signaturekey.action.edit"),
|
new LocTextKey("sebserver.exam.signaturekey.action.edit"),
|
||||||
ImageIcon.SHIELD,
|
ImageIcon.SHIELD,
|
||||||
PageStateDefinitionImpl.SECURITY_KEY_EDIT,
|
PageStateDefinitionImpl.SECURITY_KEY_EDIT,
|
||||||
ActionCategory.FORM),
|
ActionCategory.EXAM_SECURITY),
|
||||||
EXAM_SECURITY_KEY_DISABLED(
|
EXAM_SECURITY_KEY_DISABLED(
|
||||||
new LocTextKey("sebserver.exam.signaturekey.action.edit"),
|
new LocTextKey("sebserver.exam.signaturekey.action.edit"),
|
||||||
ImageIcon.NO_SHIELD,
|
ImageIcon.NO_SHIELD,
|
||||||
PageStateDefinitionImpl.SECURITY_KEY_EDIT,
|
PageStateDefinitionImpl.SECURITY_KEY_EDIT,
|
||||||
ActionCategory.FORM),
|
ActionCategory.EXAM_SECURITY),
|
||||||
EXAM_RELOAD_SECURITY_KEY_VIEW(
|
EXAM_RELOAD_SECURITY_KEY_VIEW(
|
||||||
new LocTextKey("sebserver.exam.signaturekey.action.edit"),
|
new LocTextKey("sebserver.exam.signaturekey.action.edit"),
|
||||||
ImageIcon.SHIELD,
|
ImageIcon.SHIELD,
|
||||||
|
@ -441,11 +441,21 @@ public enum ActionDefinition {
|
||||||
ImageIcon.CANCEL,
|
ImageIcon.CANCEL,
|
||||||
PageStateDefinitionImpl.EXAM_VIEW,
|
PageStateDefinitionImpl.EXAM_VIEW,
|
||||||
ActionCategory.FORM),
|
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(
|
EXAM_SECURITY_KEY_SHOW_ADD_GRANT_POPUP(
|
||||||
new LocTextKey("sebserver.exam.signaturekey.action.addGrant"),
|
new LocTextKey("sebserver.exam.signaturekey.action.addGrant"),
|
||||||
ImageIcon.ADD,
|
ImageIcon.ADD,
|
||||||
PageStateDefinitionImpl.SECURITY_KEY_EDIT,
|
PageStateDefinitionImpl.SECURITY_KEY_EDIT,
|
||||||
ActionCategory.APP_SIGNATURE_KEY_LIST),
|
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(
|
EXAM_SECURITY_KEY_SHOW_GRANT_POPUP(
|
||||||
new LocTextKey("sebserver.exam.signaturekey.action.showGrant"),
|
new LocTextKey("sebserver.exam.signaturekey.action.showGrant"),
|
||||||
ImageIcon.SHOW,
|
ImageIcon.SHOW,
|
||||||
|
@ -915,7 +925,7 @@ public enum ActionDefinition {
|
||||||
|
|
||||||
MONITOR_EXAM_BACK_TO_OVERVIEW(
|
MONITOR_EXAM_BACK_TO_OVERVIEW(
|
||||||
new LocTextKey("sebserver.monitoring.exam.action.detail.view"),
|
new LocTextKey("sebserver.monitoring.exam.action.detail.view"),
|
||||||
ImageIcon.SHOW,
|
ImageIcon.BACK,
|
||||||
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
|
PageStateDefinitionImpl.MONITORING_RUNNING_EXAM,
|
||||||
ActionCategory.FORM),
|
ActionCategory.FORM),
|
||||||
|
|
||||||
|
@ -1025,7 +1035,7 @@ public enum ActionDefinition {
|
||||||
ActionCategory.CLIENT_EVENT_LIST),
|
ActionCategory.CLIENT_EVENT_LIST),
|
||||||
FINISHED_EXAM_BACK_TO_OVERVIEW(
|
FINISHED_EXAM_BACK_TO_OVERVIEW(
|
||||||
new LocTextKey("sebserver.finished.exam.action.detail.view"),
|
new LocTextKey("sebserver.finished.exam.action.detail.view"),
|
||||||
ImageIcon.SHOW,
|
ImageIcon.BACK,
|
||||||
PageStateDefinitionImpl.FINISHED_EXAM,
|
PageStateDefinitionImpl.FINISHED_EXAM,
|
||||||
ActionCategory.FORM),
|
ActionCategory.FORM),
|
||||||
FINISHED_EXAM_EXPORT_CSV(
|
FINISHED_EXAM_EXPORT_CSV(
|
||||||
|
|
|
@ -78,6 +78,7 @@ public class AddSecurityKeyGrantPopup {
|
||||||
public PageAction showGrantPopup(final PageAction action, final AppSignatureKeyInfo appSignatureKeyInfo) {
|
public PageAction showGrantPopup(final PageAction action, final AppSignatureKeyInfo appSignatureKeyInfo) {
|
||||||
final PageContext pageContext = action.pageContext();
|
final PageContext pageContext = action.pageContext();
|
||||||
final PopupComposer popupComposer = new PopupComposer(this.pageService, pageContext, appSignatureKeyInfo);
|
final PopupComposer popupComposer = new PopupComposer(this.pageService, pageContext, appSignatureKeyInfo);
|
||||||
|
final boolean readonly = action.pageContext().isReadonly();
|
||||||
try {
|
try {
|
||||||
final ModalInputDialog<FormHandle<?>> dialog =
|
final ModalInputDialog<FormHandle<?>> dialog =
|
||||||
new ModalInputDialog<>(
|
new ModalInputDialog<>(
|
||||||
|
@ -90,7 +91,7 @@ public class AddSecurityKeyGrantPopup {
|
||||||
formHandle,
|
formHandle,
|
||||||
appSignatureKeyInfo);
|
appSignatureKeyInfo);
|
||||||
|
|
||||||
if (appSignatureKeyInfo.key == null) {
|
if (appSignatureKeyInfo.key == null || readonly) {
|
||||||
dialog.open(
|
dialog.open(
|
||||||
TITLE_TEXT_KEY,
|
TITLE_TEXT_KEY,
|
||||||
popupComposer);
|
popupComposer);
|
||||||
|
@ -130,6 +131,7 @@ public class AddSecurityKeyGrantPopup {
|
||||||
widgetFactory.addFormSubContextHeader(parent, TITLE_TEXT_INFO, null);
|
widgetFactory.addFormSubContextHeader(parent, TITLE_TEXT_INFO, null);
|
||||||
final boolean hasASK = this.appSignatureKeyInfo.key != null;
|
final boolean hasASK = this.appSignatureKeyInfo.key != null;
|
||||||
final PageContext formContext = this.pageContext.copyOf(parent);
|
final PageContext formContext = this.pageContext.copyOf(parent);
|
||||||
|
final boolean readonly = this.pageContext.isReadonly();
|
||||||
final FormHandle<?> form = this.pageService.formBuilder(formContext)
|
final FormHandle<?> form = this.pageService.formBuilder(formContext)
|
||||||
|
|
||||||
.addField(FormBuilder.text(
|
.addField(FormBuilder.text(
|
||||||
|
@ -140,7 +142,7 @@ public class AddSecurityKeyGrantPopup {
|
||||||
: Constants.EMPTY_NOTE)
|
: Constants.EMPTY_NOTE)
|
||||||
.readonly(true))
|
.readonly(true))
|
||||||
|
|
||||||
.addFieldIf(() -> hasASK,
|
.addFieldIf(() -> hasASK && !readonly,
|
||||||
() -> FormBuilder.text(
|
() -> FormBuilder.text(
|
||||||
Domain.SEB_SECURITY_KEY_REGISTRY.ATTR_TAG,
|
Domain.SEB_SECURITY_KEY_REGISTRY.ATTR_TAG,
|
||||||
TITLE_TEXT_FORM_TAG)
|
TITLE_TEXT_FORM_TAG)
|
||||||
|
|
|
@ -452,6 +452,14 @@ public class ExamForm implements TemplateComposer {
|
||||||
exam.getName()))
|
exam.getName()))
|
||||||
.publishIf(() -> editable && readonly)
|
.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)
|
.newAction(ActionDefinition.EXAM_MODIFY_SEB_RESTRICTION_DETAILS)
|
||||||
.withEntityKey(entityKey)
|
.withEntityKey(entityKey)
|
||||||
.withExec(this.examSEBRestrictionSettings.settingsFunction(this.pageService))
|
.withExec(this.examSEBRestrictionSettings.settingsFunction(this.pageService))
|
||||||
|
@ -473,14 +481,6 @@ public class ExamForm implements TemplateComposer {
|
||||||
.publishIf(() -> sebRestrictionAvailable && readonly && modifyGrant && !importFromQuizData
|
.publishIf(() -> sebRestrictionAvailable && readonly && modifyGrant && !importFromQuizData
|
||||||
&& BooleanUtils.isTrue(isRestricted))
|
&& 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)
|
.newAction(ActionDefinition.EXAM_PROCTORING_ON)
|
||||||
.withEntityKey(entityKey)
|
.withEntityKey(entityKey)
|
||||||
.withExec(this.proctoringSettingsPopup.settingsFunction(this.pageService, modifyGrant && editable))
|
.withExec(this.proctoringSettingsPopup.settingsFunction(this.pageService, modifyGrant && editable))
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
|
|
||||||
package ch.ethz.seb.sebserver.gui.content.exam;
|
package ch.ethz.seb.sebserver.gui.content.exam;
|
||||||
|
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
import org.apache.commons.lang3.BooleanUtils;
|
import org.apache.commons.lang3.BooleanUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.eclipse.swt.widgets.Composite;
|
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.Entity;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
|
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;
|
||||||
|
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.AppSignatureKeyInfo;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey;
|
import ch.ethz.seb.sebserver.gbl.model.institution.SecurityKey;
|
||||||
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
|
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.ResourceService;
|
||||||
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
|
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;
|
||||||
|
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;
|
||||||
import ch.ethz.seb.sebserver.gui.service.page.PageService.PageActionBuilder;
|
import ch.ethz.seb.sebserver.gui.service.page.PageService.PageActionBuilder;
|
||||||
import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer;
|
import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer;
|
||||||
|
@ -116,6 +120,7 @@ public class ExamSignatureKeyForm implements TemplateComposer {
|
||||||
final boolean signatureKeyCheckEnabled = BooleanUtils.toBoolean(
|
final boolean signatureKeyCheckEnabled = BooleanUtils.toBoolean(
|
||||||
exam.additionalAttributes.get(Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED));
|
exam.additionalAttributes.get(Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED));
|
||||||
final String ct = exam.additionalAttributes.get(Exam.ADDITIONAL_ATTR_NUMERICAL_TRUST_THRESHOLD);
|
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
|
final Composite content = widgetFactory
|
||||||
.defaultPageLayout(pageContext.getParent(), TILE);
|
.defaultPageLayout(pageContext.getParent(), TILE);
|
||||||
|
@ -132,7 +137,8 @@ public class ExamSignatureKeyForm implements TemplateComposer {
|
||||||
Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED,
|
Exam.ADDITIONAL_ATTR_SIGNATURE_KEY_CHECK_ENABLED,
|
||||||
FORM_ENABLED,
|
FORM_ENABLED,
|
||||||
String.valueOf(signatureKeyCheckEnabled))
|
String.valueOf(signatureKeyCheckEnabled))
|
||||||
.withInputSpan(1))
|
.withInputSpan(1)
|
||||||
|
.readonly(readonly))
|
||||||
|
|
||||||
.addField(FormBuilder.text(
|
.addField(FormBuilder.text(
|
||||||
Exam.ADDITIONAL_ATTR_NUMERICAL_TRUST_THRESHOLD,
|
Exam.ADDITIONAL_ATTR_NUMERICAL_TRUST_THRESHOLD,
|
||||||
|
@ -144,7 +150,8 @@ public class ExamSignatureKeyForm implements TemplateComposer {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.mandatory()
|
.mandatory()
|
||||||
.withInputSpan(1))
|
.withInputSpan(1)
|
||||||
|
.readonly(readonly))
|
||||||
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
@ -174,19 +181,23 @@ public class ExamSignatureKeyForm implements TemplateComposer {
|
||||||
AppSignatureKeyInfo::getNumberOfConnections)
|
AppSignatureKeyInfo::getNumberOfConnections)
|
||||||
.widthProportion(1))
|
.widthProportion(1))
|
||||||
|
|
||||||
.withDefaultAction(table -> actionBuilder
|
.withDefaultActionIf(
|
||||||
.newAction(ActionDefinition.EXAM_SECURITY_KEY_SHOW_ADD_GRANT_POPUP)
|
() -> !readonly,
|
||||||
.withParentEntityKey(entityKey)
|
table -> actionBuilder
|
||||||
.withExec(action -> this.addSecurityKeyGrantPopup.showGrantPopup(
|
.newAction(ActionDefinition.EXAM_SECURITY_KEY_SHOW_ADD_GRANT_POPUP)
|
||||||
action,
|
.withParentEntityKey(entityKey)
|
||||||
table.getSingleSelectedROWData()))
|
.withExec(action -> this.addSecurityKeyGrantPopup.showGrantPopup(
|
||||||
.noEventPropagation()
|
action,
|
||||||
.ignoreMoveAwayFromEdit()
|
table.getSingleSelectedROWData()))
|
||||||
.create())
|
.noEventPropagation()
|
||||||
|
.ignoreMoveAwayFromEdit()
|
||||||
|
.create())
|
||||||
|
|
||||||
.withSelectionListener(this.pageService.getSelectionPublisher(
|
.withSelectionListenerIf(
|
||||||
pageContext,
|
() -> !readonly,
|
||||||
ActionDefinition.EXAM_SECURITY_KEY_SHOW_ADD_GRANT_POPUP))
|
this.pageService.getSelectionPublisher(
|
||||||
|
pageContext,
|
||||||
|
ActionDefinition.EXAM_SECURITY_KEY_SHOW_ADD_GRANT_POPUP))
|
||||||
|
|
||||||
.compose(pageContext.copyOf(content));
|
.compose(pageContext.copyOf(content));
|
||||||
|
|
||||||
|
@ -214,15 +225,17 @@ public class ExamSignatureKeyForm implements TemplateComposer {
|
||||||
GRANT_LIST_TAG,
|
GRANT_LIST_TAG,
|
||||||
SecurityKey::getTag).widthProportion(1))
|
SecurityKey::getTag).widthProportion(1))
|
||||||
|
|
||||||
.withDefaultAction(table -> actionBuilder
|
.withDefaultActionIf(
|
||||||
.newAction(ActionDefinition.EXAM_SECURITY_KEY_SHOW_GRANT_POPUP)
|
() -> !readonly,
|
||||||
.withParentEntityKey(entityKey)
|
table -> actionBuilder
|
||||||
.withExec(action -> this.securityKeyGrantPopup.showGrantPopup(
|
.newAction(ActionDefinition.EXAM_SECURITY_KEY_SHOW_GRANT_POPUP)
|
||||||
action,
|
.withParentEntityKey(entityKey)
|
||||||
table.getSingleSelectedROWData()))
|
.withExec(action -> this.securityKeyGrantPopup.showGrantPopup(
|
||||||
.noEventPropagation()
|
action,
|
||||||
.ignoreMoveAwayFromEdit()
|
table.getSingleSelectedROWData()))
|
||||||
.create())
|
.noEventPropagation()
|
||||||
|
.ignoreMoveAwayFromEdit()
|
||||||
|
.create())
|
||||||
|
|
||||||
.withSelectionListener(this.pageService.getSelectionPublisher(
|
.withSelectionListener(this.pageService.getSelectionPublisher(
|
||||||
pageContext,
|
pageContext,
|
||||||
|
@ -235,30 +248,43 @@ public class ExamSignatureKeyForm implements TemplateComposer {
|
||||||
.withEntityKey(entityKey)
|
.withEntityKey(entityKey)
|
||||||
.withExec(action -> this.saveSettings(action, form.getForm()))
|
.withExec(action -> this.saveSettings(action, form.getForm()))
|
||||||
.ignoreMoveAwayFromEdit()
|
.ignoreMoveAwayFromEdit()
|
||||||
.publish()
|
.publishIf(() -> !readonly)
|
||||||
|
|
||||||
.newAction(ActionDefinition.EXAM_SECURITY_KEY_CANCEL_MODIFY)
|
.newAction(ActionDefinition.EXAM_SECURITY_KEY_CANCEL_MODIFY)
|
||||||
.withExec(this.pageService.backToCurrentFunction())
|
.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)
|
.newAction(ActionDefinition.EXAM_SECURITY_KEY_SHOW_ADD_GRANT_POPUP)
|
||||||
.withParentEntityKey(entityKey)
|
.withParentEntityKey(entityKey)
|
||||||
.withSelect(
|
.withSelect(
|
||||||
connectionInfoTable::getMultiSelection,
|
connectionInfoTable::getMultiSelection,
|
||||||
action -> this.addSecurityKeyGrantPopup.showGrantPopup(
|
this.addGrant(connectionInfoTable),
|
||||||
action,
|
|
||||||
connectionInfoTable.getSingleSelectedROWData()),
|
|
||||||
APP_SIG_KEY_LIST_EMPTY_SELECTION_TEXT_KEY)
|
APP_SIG_KEY_LIST_EMPTY_SELECTION_TEXT_KEY)
|
||||||
.ignoreMoveAwayFromEdit()
|
.ignoreMoveAwayFromEdit()
|
||||||
.noEventPropagation()
|
.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)
|
.newAction(ActionDefinition.EXAM_SECURITY_KEY_SHOW_GRANT_POPUP)
|
||||||
.withEntityKey(entityKey)
|
.withEntityKey(entityKey)
|
||||||
.withSelect(
|
.withSelect(
|
||||||
grantsList::getMultiSelection,
|
grantsList::getMultiSelection,
|
||||||
action -> this.securityKeyGrantPopup.showGrantPopup(action,
|
this.showGrant(grantsList),
|
||||||
grantsList.getSingleSelectedROWData()),
|
|
||||||
GRANT_LIST_EMPTY_SELECTION_TEXT_KEY)
|
GRANT_LIST_EMPTY_SELECTION_TEXT_KEY)
|
||||||
.ignoreMoveAwayFromEdit()
|
.ignoreMoveAwayFromEdit()
|
||||||
.noEventPropagation()
|
.noEventPropagation()
|
||||||
|
@ -272,9 +298,7 @@ public class ExamSignatureKeyForm implements TemplateComposer {
|
||||||
this::deleteGrant,
|
this::deleteGrant,
|
||||||
GRANT_LIST_EMPTY_SELECTION_TEXT_KEY)
|
GRANT_LIST_EMPTY_SELECTION_TEXT_KEY)
|
||||||
.ignoreMoveAwayFromEdit()
|
.ignoreMoveAwayFromEdit()
|
||||||
.publish(false)
|
.publishIf(() -> !readonly, false);
|
||||||
|
|
||||||
;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private PageAction saveSettings(final PageAction action, final Form form) {
|
private PageAction saveSettings(final PageAction action, final Form form) {
|
||||||
|
@ -293,6 +317,36 @@ public class ExamSignatureKeyForm implements TemplateComposer {
|
||||||
return action;
|
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) {
|
private PageAction deleteGrant(final PageAction action) {
|
||||||
final EntityKey parentEntityKey = action.getParentEntityKey();
|
final EntityKey parentEntityKey = action.getParentEntityKey();
|
||||||
final EntityKey singleSelection = action.getSingleSelection();
|
final EntityKey singleSelection = action.getSingleSelection();
|
||||||
|
|
|
@ -10,13 +10,16 @@ package ch.ethz.seb.sebserver.gui.content.exam;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Predicate;
|
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.apache.commons.lang3.BooleanUtils;
|
import org.apache.commons.lang3.BooleanUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
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.eclipse.swt.widgets.Composite;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
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.page.impl.PageAction;
|
||||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService;
|
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.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.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.GetExamTemplateProctoringSettings;
|
||||||
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.template.SaveExamTemplateProctoringSettings;
|
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.exam.template.SaveExamTemplateProctoringSettings;
|
||||||
|
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
@Component
|
@Component
|
||||||
|
@ -72,10 +77,24 @@ public class ProctoringSettingsPopup {
|
||||||
new LocTextKey("sebserver.exam.proctoring.form.url");
|
new LocTextKey("sebserver.exam.proctoring.form.url");
|
||||||
private final static LocTextKey SEB_PROCTORING_FORM_ROOM_SIZE =
|
private final static LocTextKey SEB_PROCTORING_FORM_ROOM_SIZE =
|
||||||
new LocTextKey("sebserver.exam.proctoring.form.collectingRoomSize");
|
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_APPKEY_JITSI =
|
||||||
private final static LocTextKey SEB_PROCTORING_FORM_SECRET =
|
new LocTextKey("sebserver.exam.proctoring.form.appkey.jitsi");
|
||||||
new LocTextKey("sebserver.exam.proctoring.form.secret");
|
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 =
|
private final static LocTextKey SEB_PROCTORING_FORM_SDKKEY =
|
||||||
new LocTextKey("sebserver.exam.proctoring.form.sdkkey");
|
new LocTextKey("sebserver.exam.proctoring.form.sdkkey");
|
||||||
private final static LocTextKey SEB_PROCTORING_FORM_SDKSECRET =
|
private final static LocTextKey SEB_PROCTORING_FORM_SDKSECRET =
|
||||||
|
@ -86,6 +105,13 @@ public class ProctoringSettingsPopup {
|
||||||
private final static LocTextKey SEB_PROCTORING_FORM_FEATURES =
|
private final static LocTextKey SEB_PROCTORING_FORM_FEATURES =
|
||||||
new LocTextKey("sebserver.exam.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) {
|
Function<PageAction, PageAction> settingsFunction(final PageService pageService, final boolean modifyGrant) {
|
||||||
|
|
||||||
return action -> {
|
return action -> {
|
||||||
|
@ -102,32 +128,90 @@ public class ProctoringSettingsPopup {
|
||||||
.setDialogWidth(860)
|
.setDialogWidth(860)
|
||||||
.setDialogHeight(600);
|
.setDialogHeight(600);
|
||||||
|
|
||||||
final SEBProctoringPropertiesForm bindFormContext = new SEBProctoringPropertiesForm(
|
final ResetButtonHandler resetButtonHandler = new ResetButtonHandler();
|
||||||
pageService,
|
|
||||||
pageContext);
|
|
||||||
|
|
||||||
final Predicate<FormHandle<?>> doBind = formHandle -> doSaveSettings(
|
|
||||||
pageService,
|
|
||||||
pageContext,
|
|
||||||
formHandle);
|
|
||||||
|
|
||||||
if (modifyGrant) {
|
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,
|
SEB_PROCTORING_FORM_TITLE,
|
||||||
doBind,
|
actionComposer,
|
||||||
Utils.EMPTY_EXECUTION,
|
Utils.EMPTY_EXECUTION,
|
||||||
bindFormContext);
|
bindFormContext);
|
||||||
} else {
|
} else {
|
||||||
dialog.open(
|
dialog.open(
|
||||||
SEB_PROCTORING_FORM_TITLE,
|
SEB_PROCTORING_FORM_TITLE,
|
||||||
pageContext,
|
pageContext,
|
||||||
pc -> bindFormContext.compose(pc.getParent()));
|
pc -> new SEBProctoringPropertiesForm(
|
||||||
|
pageService,
|
||||||
|
pageContext,
|
||||||
|
resetButtonHandler));
|
||||||
}
|
}
|
||||||
|
|
||||||
return action;
|
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(
|
private boolean doSaveSettings(
|
||||||
final PageService pageService,
|
final PageService pageService,
|
||||||
final PageContext pageContext,
|
final PageContext pageContext,
|
||||||
|
@ -168,8 +252,14 @@ public class ProctoringSettingsPopup {
|
||||||
false,
|
false,
|
||||||
form.getFieldValue(ProctoringServiceSettings.ATTR_APP_KEY),
|
form.getFieldValue(ProctoringServiceSettings.ATTR_APP_KEY),
|
||||||
form.getFieldValue(ProctoringServiceSettings.ATTR_APP_SECRET),
|
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_KEY),
|
||||||
form.getFieldValue(ProctoringServiceSettings.ATTR_SDK_SECRET),
|
form.getFieldValue(ProctoringServiceSettings.ATTR_SDK_SECRET),
|
||||||
|
|
||||||
BooleanUtils.toBoolean(form.getFieldValue(
|
BooleanUtils.toBoolean(form.getFieldValue(
|
||||||
ProctoringServiceSettings.ATTR_USE_ZOOM_APP_CLIENT_COLLECTING_ROOM)));
|
ProctoringServiceSettings.ATTR_USE_ZOOM_APP_CLIENT_COLLECTING_ROOM)));
|
||||||
|
|
||||||
|
@ -217,23 +307,22 @@ public class ProctoringSettingsPopup {
|
||||||
|
|
||||||
private final PageService pageService;
|
private final PageService pageService;
|
||||||
private final PageContext pageContext;
|
private final PageContext pageContext;
|
||||||
|
private final ResetButtonHandler resetButtonHandler;
|
||||||
|
|
||||||
protected SEBProctoringPropertiesForm(
|
protected SEBProctoringPropertiesForm(
|
||||||
final PageService pageService,
|
final PageService pageService,
|
||||||
final PageContext pageContext) {
|
final PageContext pageContext,
|
||||||
|
final ResetButtonHandler resetButtonHandler) {
|
||||||
|
|
||||||
this.pageService = pageService;
|
this.pageService = pageService;
|
||||||
this.pageContext = pageContext;
|
this.pageContext = pageContext;
|
||||||
|
this.resetButtonHandler = resetButtonHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Supplier<FormHandle<?>> compose(final Composite parent) {
|
public Supplier<FormHandle<?>> compose(final Composite parent) {
|
||||||
final RestService restService = this.pageService.getRestService();
|
final RestService restService = this.pageService.getRestService();
|
||||||
final ResourceService resourceService = this.pageService.getResourceService();
|
|
||||||
final EntityKey entityKey = this.pageContext.getEntityKey();
|
final EntityKey entityKey = this.pageContext.getEntityKey();
|
||||||
final boolean isReadonly = BooleanUtils.toBoolean(
|
|
||||||
this.pageContext.getAttribute(PageContext.AttributeKeys.FORCE_READ_ONLY));
|
|
||||||
|
|
||||||
final Composite content = this.pageService
|
final Composite content = this.pageService
|
||||||
.getWidgetFactory()
|
.getWidgetFactory()
|
||||||
|
@ -248,12 +337,170 @@ public class ProctoringSettingsPopup {
|
||||||
.call()
|
.call()
|
||||||
.getOrThrow();
|
.getOrThrow();
|
||||||
|
|
||||||
final PageContext formContext = this.pageContext
|
this.resetButtonHandler.enable(proctoringSettings.serviceInUse);
|
||||||
|
final FormHandleAnchor formHandleAnchor = new FormHandleAnchor();
|
||||||
|
formHandleAnchor.formContext = this.pageContext
|
||||||
.copyOf(content)
|
.copyOf(content)
|
||||||
.clearEntityKeys();
|
.clearEntityKeys();
|
||||||
|
|
||||||
final FormHandle<ProctoringServiceSettings> formHandle = this.pageService.formBuilder(
|
buildFormAccordingToService(
|
||||||
formContext)
|
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)
|
.withDefaultSpanInput(5)
|
||||||
.withEmptyCellSeparation(true)
|
.withEmptyCellSeparation(true)
|
||||||
.withDefaultSpanEmptyCell(1)
|
.withDefaultSpanEmptyCell(1)
|
||||||
|
@ -270,49 +517,13 @@ public class ProctoringSettingsPopup {
|
||||||
.addField(FormBuilder.checkbox(
|
.addField(FormBuilder.checkbox(
|
||||||
ProctoringServiceSettings.ATTR_ENABLE_PROCTORING,
|
ProctoringServiceSettings.ATTR_ENABLE_PROCTORING,
|
||||||
SEB_PROCTORING_FORM_ENABLE,
|
SEB_PROCTORING_FORM_ENABLE,
|
||||||
String.valueOf(proctoringSettings.enableProctoring)))
|
String.valueOf(proctoringSettings.enableProctoring)));
|
||||||
|
return formBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
.addField(FormBuilder.singleSelection(
|
private FormHandle<ProctoringServiceSettings> buildFooter(final ProctoringServiceSettings proctoringSettings,
|
||||||
ProctoringServiceSettings.ATTR_SERVER_TYPE,
|
final ResourceService resourceService, final FormBuilder formBuilder) {
|
||||||
SEB_PROCTORING_FORM_TYPE,
|
return formBuilder.withDefaultSpanInput(1)
|
||||||
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)
|
|
||||||
.addField(FormBuilder.text(
|
.addField(FormBuilder.text(
|
||||||
ProctoringServiceSettings.ATTR_COLLECTING_ROOM_SIZE,
|
ProctoringServiceSettings.ATTR_COLLECTING_ROOM_SIZE,
|
||||||
SEB_PROCTORING_FORM_ROOM_SIZE,
|
SEB_PROCTORING_FORM_ROOM_SIZE,
|
||||||
|
@ -337,14 +548,14 @@ public class ProctoringSettingsPopup {
|
||||||
resourceService::examProctoringFeaturesResources))
|
resourceService::examProctoringFeaturesResources))
|
||||||
|
|
||||||
.build();
|
.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;
|
package ch.ethz.seb.sebserver.gui.service.page.impl;
|
||||||
|
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
@ -17,6 +18,8 @@ import org.eclipse.swt.SWT;
|
||||||
import org.eclipse.swt.graphics.Rectangle;
|
import org.eclipse.swt.graphics.Rectangle;
|
||||||
import org.eclipse.swt.layout.GridData;
|
import org.eclipse.swt.layout.GridData;
|
||||||
import org.eclipse.swt.layout.GridLayout;
|
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.Button;
|
||||||
import org.eclipse.swt.widgets.Composite;
|
import org.eclipse.swt.widgets.Composite;
|
||||||
import org.eclipse.swt.widgets.Dialog;
|
import org.eclipse.swt.widgets.Dialog;
|
||||||
|
@ -110,9 +113,53 @@ public class ModalInputDialog<T> extends Dialog {
|
||||||
open(title, predicate, cancelCallback, contentComposer);
|
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(
|
public void open(
|
||||||
final LocTextKey title,
|
final LocTextKey title,
|
||||||
final Predicate<T> callback,
|
final Predicate<T> okCallback,
|
||||||
final Runnable cancelCallback,
|
final Runnable cancelCallback,
|
||||||
final ModalInputDialogComposer<T> contentComposer) {
|
final ModalInputDialogComposer<T> contentComposer) {
|
||||||
|
|
||||||
|
@ -142,7 +189,7 @@ public class ModalInputDialog<T> extends Dialog {
|
||||||
ok.addListener(SWT.Selection, event -> {
|
ok.addListener(SWT.Selection, event -> {
|
||||||
if (valueSupplier != null) {
|
if (valueSupplier != null) {
|
||||||
final T result = valueSupplier.get();
|
final T result = valueSupplier.get();
|
||||||
if (callback.test(result)) {
|
if (okCallback.test(result)) {
|
||||||
this.shell.close();
|
this.shell.close();
|
||||||
}
|
}
|
||||||
} else {
|
} 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;
|
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) {
|
public TableBuilder<ROW> withContentChangeListener(final Consumer<Integer> contentChangeListener) {
|
||||||
this.contentChangeListener = contentChangeListener;
|
this.contentChangeListener = contentChangeListener;
|
||||||
return this;
|
return this;
|
||||||
|
|
|
@ -147,7 +147,8 @@ public class WidgetFactory {
|
||||||
NOTIFICATION("notification.png"),
|
NOTIFICATION("notification.png"),
|
||||||
VERIFY("verify.png"),
|
VERIFY("verify.png"),
|
||||||
SHIELD("shield.png"),
|
SHIELD("shield.png"),
|
||||||
NO_SHIELD("no_shield.png");
|
NO_SHIELD("no_shield.png"),
|
||||||
|
BACK("back.png");
|
||||||
|
|
||||||
public String fileName;
|
public String fileName;
|
||||||
private ImageData image = null;
|
private ImageData image = null;
|
||||||
|
|
|
@ -116,6 +116,8 @@ public interface ExamAdminService {
|
||||||
* @return ExamProctoringService instance */
|
* @return ExamProctoringService instance */
|
||||||
Result<ExamProctoringService> getExamProctoringService(final Long examId);
|
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
|
/** 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.
|
* 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 static final Logger log = LoggerFactory.getLogger(ExamAdminServiceImpl.class);
|
||||||
|
|
||||||
private final ExamDAO examDAO;
|
private final ExamDAO examDAO;
|
||||||
private final ProctoringAdminService proctoringServiceSettingsService;
|
private final ProctoringAdminService proctoringAdminService;
|
||||||
private final AdditionalAttributesDAO additionalAttributesDAO;
|
private final AdditionalAttributesDAO additionalAttributesDAO;
|
||||||
private final ConfigurationNodeDAO configurationNodeDAO;
|
private final ConfigurationNodeDAO configurationNodeDAO;
|
||||||
private final ExamConfigurationMapDAO examConfigurationMapDAO;
|
private final ExamConfigurationMapDAO examConfigurationMapDAO;
|
||||||
|
@ -70,7 +70,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
|
||||||
|
|
||||||
protected ExamAdminServiceImpl(
|
protected ExamAdminServiceImpl(
|
||||||
final ExamDAO examDAO,
|
final ExamDAO examDAO,
|
||||||
final ProctoringAdminService proctoringServiceSettingsService,
|
final ProctoringAdminService proctoringAdminService,
|
||||||
final AdditionalAttributesDAO additionalAttributesDAO,
|
final AdditionalAttributesDAO additionalAttributesDAO,
|
||||||
final ConfigurationNodeDAO configurationNodeDAO,
|
final ConfigurationNodeDAO configurationNodeDAO,
|
||||||
final ExamConfigurationMapDAO examConfigurationMapDAO,
|
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) {
|
final @Value("${sebserver.webservice.api.admin.exam.app.signature.key.numerical.threshold:2}") int defaultNumericalTrustThreshold) {
|
||||||
|
|
||||||
this.examDAO = examDAO;
|
this.examDAO = examDAO;
|
||||||
this.proctoringServiceSettingsService = proctoringServiceSettingsService;
|
this.proctoringAdminService = proctoringAdminService;
|
||||||
this.additionalAttributesDAO = additionalAttributesDAO;
|
this.additionalAttributesDAO = additionalAttributesDAO;
|
||||||
this.configurationNodeDAO = configurationNodeDAO;
|
this.configurationNodeDAO = configurationNodeDAO;
|
||||||
this.examConfigurationMapDAO = examConfigurationMapDAO;
|
this.examConfigurationMapDAO = examConfigurationMapDAO;
|
||||||
|
@ -200,7 +200,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<ProctoringServiceSettings> getProctoringServiceSettings(final Long examId) {
|
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
|
@Override
|
||||||
|
@ -209,7 +209,7 @@ public class ExamAdminServiceImpl implements ExamAdminService {
|
||||||
final Long examId,
|
final Long examId,
|
||||||
final ProctoringServiceSettings proctoringServiceSettings) {
|
final ProctoringServiceSettings proctoringServiceSettings) {
|
||||||
|
|
||||||
return this.proctoringServiceSettingsService
|
return this.proctoringAdminService
|
||||||
.saveProctoringServiceSettings(
|
.saveProctoringServiceSettings(
|
||||||
new EntityKey(examId, EntityType.EXAM),
|
new EntityKey(examId, EntityType.EXAM),
|
||||||
proctoringServiceSettings)
|
proctoringServiceSettings)
|
||||||
|
@ -239,10 +239,30 @@ public class ExamAdminServiceImpl implements ExamAdminService {
|
||||||
@Override
|
@Override
|
||||||
public Result<ExamProctoringService> getExamProctoringService(final Long examId) {
|
public Result<ExamProctoringService> getExamProctoringService(final Long examId) {
|
||||||
return getProctoringServiceSettings(examId)
|
return getProctoringServiceSettings(examId)
|
||||||
.flatMap(settings -> this.proctoringServiceSettingsService
|
.flatMap(settings -> this.proctoringAdminService
|
||||||
.getExamProctoringService(settings.serverType));
|
.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) {
|
private Result<Exam> initAdditionalAttributesForMoodleExams(final Exam exam) {
|
||||||
return Result.tryCatch(() -> {
|
return Result.tryCatch(() -> {
|
||||||
final LmsAPITemplate lmsTemplate = this.lmsAPIService
|
final LmsAPITemplate lmsTemplate = this.lmsAPIService
|
||||||
|
|
|
@ -91,6 +91,9 @@ public class ProctoringAdminServiceImpl implements ProctoringAdminService {
|
||||||
: false,
|
: false,
|
||||||
getString(mapping, ProctoringServiceSettings.ATTR_APP_KEY),
|
getString(mapping, ProctoringServiceSettings.ATTR_APP_KEY),
|
||||||
getString(mapping, ProctoringServiceSettings.ATTR_APP_SECRET),
|
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_KEY),
|
||||||
getString(mapping, ProctoringServiceSettings.ATTR_SDK_SECRET),
|
getString(mapping, ProctoringServiceSettings.ATTR_SDK_SECRET),
|
||||||
getBoolean(mapping,
|
getBoolean(mapping,
|
||||||
|
@ -139,19 +142,42 @@ public class ProctoringAdminServiceImpl implements ProctoringAdminService {
|
||||||
ProctoringServiceSettings.ATTR_COLLECTING_ROOM_SIZE,
|
ProctoringServiceSettings.ATTR_COLLECTING_ROOM_SIZE,
|
||||||
String.valueOf(proctoringServiceSettings.collectingRoomSize));
|
String.valueOf(proctoringServiceSettings.collectingRoomSize));
|
||||||
|
|
||||||
this.additionalAttributesDAO.saveAdditionalAttribute(
|
if (StringUtils.isNotBlank(proctoringServiceSettings.appKey)) {
|
||||||
parentEntityKey.entityType,
|
this.additionalAttributesDAO.saveAdditionalAttribute(
|
||||||
entityId,
|
parentEntityKey.entityType,
|
||||||
ProctoringServiceSettings.ATTR_APP_KEY,
|
entityId,
|
||||||
StringUtils.trim(proctoringServiceSettings.appKey));
|
ProctoringServiceSettings.ATTR_APP_KEY,
|
||||||
|
StringUtils.trim(proctoringServiceSettings.appKey));
|
||||||
|
|
||||||
this.additionalAttributesDAO.saveAdditionalAttribute(
|
this.additionalAttributesDAO.saveAdditionalAttribute(
|
||||||
parentEntityKey.entityType,
|
parentEntityKey.entityType,
|
||||||
entityId,
|
entityId,
|
||||||
ProctoringServiceSettings.ATTR_APP_SECRET,
|
ProctoringServiceSettings.ATTR_APP_SECRET,
|
||||||
this.cryptor.encrypt(Utils.trim(proctoringServiceSettings.appSecret))
|
this.cryptor.encrypt(Utils.trim(proctoringServiceSettings.appSecret))
|
||||||
.getOrThrow()
|
.getOrThrow()
|
||||||
.toString());
|
.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)) {
|
if (StringUtils.isNotBlank(proctoringServiceSettings.sdkKey)) {
|
||||||
this.additionalAttributesDAO.saveAdditionalAttribute(
|
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.ClientConnection;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom;
|
import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
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) */
|
/** Defines functionality to deal with proctoring rooms in a generic way (independent from meeting service) */
|
||||||
public interface ExamProctoringRoomService {
|
public interface ExamProctoringRoomService {
|
||||||
|
|
||||||
|
ProctoringAdminService getProctoringAdminService();
|
||||||
|
|
||||||
/** Get all existing default proctoring rooms of an exam.
|
/** Get all existing default proctoring rooms of an exam.
|
||||||
*
|
*
|
||||||
* @param examId The exam identifier
|
* @param examId The exam identifier
|
||||||
|
@ -136,4 +139,6 @@ public interface ExamProctoringRoomService {
|
||||||
* @return Result refer to void or to an error when happened */
|
* @return Result refer to void or to an error when happened */
|
||||||
Result<Void> notifyRoomOpened(Long examId, String roomName);
|
Result<Void> notifyRoomOpened(Long examId, String roomName);
|
||||||
|
|
||||||
|
Result<Exam> cleanupAllRooms(Exam exam);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,6 +84,11 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
|
||||||
this.sendBroadcastReset = sendBroadcastReset;
|
this.sendBroadcastReset = sendBroadcastReset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ProctoringAdminService getProctoringAdminService() {
|
||||||
|
return this.proctoringAdminService;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<Collection<RemoteProctoringRoom>> getProctoringCollectingRooms(final Long examId) {
|
public Result<Collection<RemoteProctoringRoom>> getProctoringCollectingRooms(final Long examId) {
|
||||||
return this.remoteProctoringRoomDAO.getCollectingRooms(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) {
|
private void assignToCollectingRoom(final ClientConnectionRecord cc) {
|
||||||
synchronized (RESERVE_ROOM_LOCK) {
|
synchronized (RESERVE_ROOM_LOCK) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -15,7 +15,9 @@ import java.util.Base64;
|
||||||
import java.util.Base64.Encoder;
|
import java.util.Base64.Encoder;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@ -36,6 +38,10 @@ import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
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.stereotype.Service;
|
||||||
import org.springframework.web.client.RestClientResponseException;
|
import org.springframework.web.client.RestClientResponseException;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
@ -86,15 +92,12 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
|
|
||||||
private static final String TOKEN_ENCODE_ALG = "HmacSHA256";
|
private static final String TOKEN_ENCODE_ALG = "HmacSHA256";
|
||||||
|
|
||||||
@Deprecated
|
|
||||||
private static final String ZOOM_ACCESS_TOKEN_HEADER =
|
private static final String ZOOM_ACCESS_TOKEN_HEADER =
|
||||||
"{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
|
"{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
|
||||||
|
|
||||||
@Deprecated
|
@Deprecated
|
||||||
private static final String ZOOM_API_ACCESS_TOKEN_PAYLOAD =
|
private static final String ZOOM_API_ACCESS_TOKEN_PAYLOAD =
|
||||||
"{\"iss\":\"%s\",\"exp\":%s}";
|
"{\"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(
|
private static final Map<String, String> SEB_API_NAME_INSTRUCTION_NAME_MAPPING = Utils.immutableMapOf(Arrays.asList(
|
||||||
new Tuple<>(
|
new Tuple<>(
|
||||||
|
@ -125,7 +128,6 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
private final Cryptor cryptor;
|
private final Cryptor cryptor;
|
||||||
private final AsyncService asyncService;
|
private final AsyncService asyncService;
|
||||||
private final JSONMapper jsonMapper;
|
private final JSONMapper jsonMapper;
|
||||||
private final ZoomRestTemplate zoomRestTemplate;
|
|
||||||
private final RemoteProctoringRoomDAO remoteProctoringRoomDAO;
|
private final RemoteProctoringRoomDAO remoteProctoringRoomDAO;
|
||||||
private final AuthorizationService authorizationService;
|
private final AuthorizationService authorizationService;
|
||||||
private final SEBClientInstructionService sebInstructionService;
|
private final SEBClientInstructionService sebInstructionService;
|
||||||
|
@ -149,7 +151,6 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
this.cryptor = cryptor;
|
this.cryptor = cryptor;
|
||||||
this.asyncService = asyncService;
|
this.asyncService = asyncService;
|
||||||
this.jsonMapper = jsonMapper;
|
this.jsonMapper = jsonMapper;
|
||||||
this.zoomRestTemplate = new ZoomRestTemplate(this);
|
|
||||||
this.remoteProctoringRoomDAO = remoteProctoringRoomDAO;
|
this.remoteProctoringRoomDAO = remoteProctoringRoomDAO;
|
||||||
this.authorizationService = authorizationService;
|
this.authorizationService = authorizationService;
|
||||||
this.sebInstructionService = sebInstructionService;
|
this.sebInstructionService = sebInstructionService;
|
||||||
|
@ -173,16 +174,26 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
|
|
||||||
try {
|
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,
|
proctoringSettings.appKey,
|
||||||
this.cryptor
|
this.cryptor.encrypt(proctoringSettings.appSecret).getOrThrow(),
|
||||||
.encrypt(proctoringSettings.appSecret)
|
proctoringSettings.accountId,
|
||||||
.getOrThrow());
|
proctoringSettings.clientId,
|
||||||
|
this.cryptor.encrypt(proctoringSettings.clientSecret).getOrThrow(),
|
||||||
|
proctoringSettings.sdkKey,
|
||||||
|
this.cryptor.encrypt(proctoringSettings.sdkSecret).getOrThrow(),
|
||||||
|
proctoringSettings.useZoomAppClientForCollectingRoom);
|
||||||
|
|
||||||
final ResponseEntity<String> result = this.zoomRestTemplate
|
final ZoomRestTemplate newRestTemplate = createNewRestTemplate(proctoringServiceSettings);
|
||||||
.testServiceConnection(
|
|
||||||
proctoringSettings.serverURL,
|
final ResponseEntity<String> result = newRestTemplate.testServiceConnection();
|
||||||
credentials);
|
|
||||||
|
|
||||||
if (result.getStatusCode() != HttpStatus.OK) {
|
if (result.getStatusCode() != HttpStatus.OK) {
|
||||||
throw new APIMessageException(Arrays.asList(
|
throw new APIMessageException(Arrays.asList(
|
||||||
|
@ -415,13 +426,8 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
roomData.getAdditionalRoomData(),
|
roomData.getAdditionalRoomData(),
|
||||||
AdditionalZoomRoomData.class);
|
AdditionalZoomRoomData.class);
|
||||||
|
|
||||||
final ClientCredentials credentials = new ClientCredentials(
|
|
||||||
proctoringSettings.appKey,
|
|
||||||
proctoringSettings.appSecret);
|
|
||||||
|
|
||||||
this.deleteAdHocMeeting(
|
this.deleteAdHocMeeting(
|
||||||
proctoringSettings,
|
proctoringSettings,
|
||||||
credentials,
|
|
||||||
additionalZoomRoomData.meeting_id,
|
additionalZoomRoomData.meeting_id,
|
||||||
additionalZoomRoomData.user_id)
|
additionalZoomRoomData.user_id)
|
||||||
.getOrThrow();
|
.getOrThrow();
|
||||||
|
@ -521,15 +527,10 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
final ProctoringServiceSettings proctoringSettings) {
|
final ProctoringServiceSettings proctoringSettings) {
|
||||||
|
|
||||||
return Result.tryCatch(() -> {
|
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
|
// First create a new user/host for the new room
|
||||||
final ResponseEntity<String> createUser = this.zoomRestTemplate.createUser(
|
final ResponseEntity<String> createUser = zoomRestTemplate.createUser(roomName);
|
||||||
proctoringSettings.serverURL,
|
|
||||||
credentials,
|
|
||||||
roomName);
|
|
||||||
|
|
||||||
final int statusCodeValue = createUser.getStatusCodeValue();
|
final int statusCodeValue = createUser.getStatusCodeValue();
|
||||||
if (statusCodeValue >= 400) {
|
if (statusCodeValue >= 400) {
|
||||||
|
@ -540,16 +541,11 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
createUser.getBody(),
|
createUser.getBody(),
|
||||||
UserResponse.class);
|
UserResponse.class);
|
||||||
|
|
||||||
this.zoomRestTemplate.applyUserSettings(
|
zoomRestTemplate.applyUserSettings(userResponse.id);
|
||||||
proctoringSettings.serverURL,
|
|
||||||
credentials,
|
|
||||||
userResponse.id);
|
|
||||||
|
|
||||||
// Then create new meeting with the ad-hoc user/host
|
// Then create new meeting with the ad-hoc user/host
|
||||||
final CharSequence meetingPwd = UUID.randomUUID().toString().subSequence(0, 9);
|
final CharSequence meetingPwd = UUID.randomUUID().toString().subSequence(0, 9);
|
||||||
final ResponseEntity<String> createMeeting = this.zoomRestTemplate.createMeeting(
|
final ResponseEntity<String> createMeeting = zoomRestTemplate.createMeeting(
|
||||||
proctoringSettings.serverURL,
|
|
||||||
credentials,
|
|
||||||
userResponse.id,
|
userResponse.id,
|
||||||
subject,
|
subject,
|
||||||
duration,
|
duration,
|
||||||
|
@ -580,69 +576,18 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
|
|
||||||
private Result<Void> deleteAdHocMeeting(
|
private Result<Void> deleteAdHocMeeting(
|
||||||
final ProctoringServiceSettings proctoringSettings,
|
final ProctoringServiceSettings proctoringSettings,
|
||||||
final ClientCredentials credentials,
|
|
||||||
final Long meetingId,
|
final Long meetingId,
|
||||||
final String userId) {
|
final String userId) {
|
||||||
|
|
||||||
return Result.tryCatch(() -> {
|
return Result.tryCatch(() -> {
|
||||||
|
|
||||||
this.zoomRestTemplate.deleteMeeting(proctoringSettings.serverURL, credentials, meetingId);
|
final ZoomRestTemplate zoomRestTemplate = getZoomRestTemplate(proctoringSettings);
|
||||||
this.zoomRestTemplate.deleteUser(proctoringSettings.serverURL, credentials, userId);
|
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(
|
private String createSDKJWT(
|
||||||
final ClientCredentials sdkCredentials,
|
final ClientCredentials sdkCredentials,
|
||||||
final Long expTime,
|
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) {
|
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
|
// 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;
|
return nowPlusOneDayInSeconds - 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
private final static class ZoomRestTemplate {
|
private final LinkedHashMap<Long, ZoomRestTemplate> restTemplatesCache = new LinkedHashMap<>();
|
||||||
|
|
||||||
private static final int LIZENSED_USER = 2;
|
private synchronized ZoomRestTemplate getZoomRestTemplate(final ProctoringServiceSettings proctoringSettings) {
|
||||||
private static final String API_TEST_ENDPOINT = "v2/users";
|
if (!this.restTemplatesCache.containsKey(proctoringSettings.examId)) {
|
||||||
private static final String API_CREATE_USER_ENDPOINT = "v2/users";
|
this.restTemplatesCache.put(
|
||||||
private static final String API_APPLY_USER_SETTINGS_ENDPOINT = "v2/users/{userId}/settings";
|
proctoringSettings.examId,
|
||||||
private static final String API_DELETE_USER_ENDPOINT = "v2/users/{userid}?action=delete";
|
createNewRestTemplate(proctoringSettings));
|
||||||
private static final String API_USER_CUST_CREATE = "custCreate";
|
} else {
|
||||||
private static final String API_ZOOM_ROOM_USER = "SEBProctoringRoomUser";
|
final ZoomRestTemplate zoomRestTemplate = this.restTemplatesCache.get(proctoringSettings.examId);
|
||||||
private static final String API_CREATE_MEETING_ENDPOINT = "v2/users/{userid}/meetings";
|
if (!zoomRestTemplate.isValid(proctoringSettings)) {
|
||||||
private static final String API_DELETE_MEETING_ENDPOINT = "v2/meetings/{meetingid}";
|
this.restTemplatesCache.remove(proctoringSettings.examId);
|
||||||
private static final String API_END_MEETING_ENDPOINT = "v2/meetings/{meetingid}/status";
|
this.restTemplatesCache.put(
|
||||||
|
proctoringSettings.examId,
|
||||||
|
createNewRestTemplate(proctoringSettings));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private final ZoomProctoringService zoomProctoringService;
|
if (this.restTemplatesCache.size() > 5) {
|
||||||
private final RestTemplate restTemplate;
|
final Long toRemove = this.restTemplatesCache.keySet().iterator().next();
|
||||||
private final CircuitBreaker<ResponseEntity<String>> circuitBreaker;
|
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.zoomProctoringService = zoomProctoringService;
|
||||||
this.restTemplate = new RestTemplate(zoomProctoringService.clientHttpRequestFactoryService
|
|
||||||
.getClientHttpRequestFactory()
|
|
||||||
.getOrThrow());
|
|
||||||
|
|
||||||
this.circuitBreaker = zoomProctoringService.asyncService.createCircuitBreaker(
|
this.circuitBreaker = zoomProctoringService.asyncService.createCircuitBreaker(
|
||||||
2,
|
2,
|
||||||
10 * Constants.SECOND_IN_MILLIS,
|
10 * Constants.SECOND_IN_MILLIS,
|
||||||
10 * Constants.SECOND_IN_MILLIS);
|
10 * Constants.SECOND_IN_MILLIS);
|
||||||
|
this.proctoringSettings = proctoringSettings;
|
||||||
|
initConnection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResponseEntity<String> testServiceConnection(
|
protected abstract void initConnection();
|
||||||
final String zoomServerUrl,
|
|
||||||
final ClientCredentials credentials) {
|
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 {
|
try {
|
||||||
|
|
||||||
final String url = UriComponentsBuilder
|
final String url = UriComponentsBuilder
|
||||||
.fromUriString(zoomServerUrl)
|
.fromUriString(this.proctoringSettings.serverURL)
|
||||||
.path(API_TEST_ENDPOINT)
|
.path(API_TEST_ENDPOINT)
|
||||||
.queryParam("status", "active")
|
.queryParam("status", "active")
|
||||||
.queryParam("page_size", "10")
|
.queryParam("page_size", "10")
|
||||||
|
@ -840,7 +778,7 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
.queryParam("data_type", "Json")
|
.queryParam("data_type", "Json")
|
||||||
.build()
|
.build()
|
||||||
.toUriString();
|
.toUriString();
|
||||||
return exchange(url, HttpMethod.GET, credentials);
|
return exchange(url, HttpMethod.GET);
|
||||||
|
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
log.error("Failed to test zoom service connection: ", e);
|
log.error("Failed to test zoom service connection: ", e);
|
||||||
|
@ -848,17 +786,15 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResponseEntity<String> createUser(
|
public ResponseEntity<String> createUser(final String roomName) {
|
||||||
final String zoomServerUrl,
|
|
||||||
final ClientCredentials credentials,
|
|
||||||
final String roomName) {
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
final String url = UriComponentsBuilder
|
final String url = UriComponentsBuilder
|
||||||
.fromUriString(zoomServerUrl)
|
.fromUriString(this.proctoringSettings.serverURL)
|
||||||
.path(API_CREATE_USER_ENDPOINT)
|
.path(API_CREATE_USER_ENDPOINT)
|
||||||
.toUriString();
|
.toUriString();
|
||||||
final String host = new URL(zoomServerUrl).getHost();
|
final String host = new URL(this.proctoringSettings.serverURL).getHost();
|
||||||
final CreateUserRequest createUserRequest = new CreateUserRequest(
|
final CreateUserRequest createUserRequest = new CreateUserRequest(
|
||||||
API_USER_CUST_CREATE,
|
API_USER_CUST_CREATE,
|
||||||
new CreateUserRequest.UserInfo(
|
new CreateUserRequest.UserInfo(
|
||||||
|
@ -868,7 +804,7 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
API_ZOOM_ROOM_USER));
|
API_ZOOM_ROOM_USER));
|
||||||
|
|
||||||
final String body = this.zoomProctoringService.jsonMapper.writeValueAsString(createUserRequest);
|
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);
|
headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
|
||||||
return exchange(url, HttpMethod.POST, body, headers);
|
return exchange(url, HttpMethod.POST, body, headers);
|
||||||
|
|
||||||
|
@ -878,13 +814,10 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResponseEntity<String> applyUserSettings(
|
public ResponseEntity<String> applyUserSettings(final String userId) {
|
||||||
final String zoomServerUrl,
|
|
||||||
final ClientCredentials credentials,
|
|
||||||
final String userId) {
|
|
||||||
try {
|
try {
|
||||||
final String url = UriComponentsBuilder
|
final String url = UriComponentsBuilder
|
||||||
.fromUriString(zoomServerUrl)
|
.fromUriString(this.proctoringSettings.serverURL)
|
||||||
.path(API_APPLY_USER_SETTINGS_ENDPOINT)
|
.path(API_APPLY_USER_SETTINGS_ENDPOINT)
|
||||||
.buildAndExpand(userId)
|
.buildAndExpand(userId)
|
||||||
.normalize()
|
.normalize()
|
||||||
|
@ -892,7 +825,7 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
|
|
||||||
final ApplyUserSettingsRequest applySettingsRequest = new ApplyUserSettingsRequest();
|
final ApplyUserSettingsRequest applySettingsRequest = new ApplyUserSettingsRequest();
|
||||||
final String body = this.zoomProctoringService.jsonMapper.writeValueAsString(applySettingsRequest);
|
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);
|
headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
|
||||||
final ResponseEntity<String> exchange = exchange(url, HttpMethod.PATCH, body, headers);
|
final ResponseEntity<String> exchange = exchange(url, HttpMethod.PATCH, body, headers);
|
||||||
|
@ -904,8 +837,6 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResponseEntity<String> createMeeting(
|
public ResponseEntity<String> createMeeting(
|
||||||
final String zoomServerUrl,
|
|
||||||
final ClientCredentials credentials,
|
|
||||||
final String userId,
|
final String userId,
|
||||||
final String topic,
|
final String topic,
|
||||||
final int duration,
|
final int duration,
|
||||||
|
@ -915,7 +846,7 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
final String url = UriComponentsBuilder
|
final String url = UriComponentsBuilder
|
||||||
.fromUriString(zoomServerUrl)
|
.fromUriString(this.proctoringSettings.serverURL)
|
||||||
.path(API_CREATE_MEETING_ENDPOINT)
|
.path(API_CREATE_MEETING_ENDPOINT)
|
||||||
.buildAndExpand(userId)
|
.buildAndExpand(userId)
|
||||||
.toUriString();
|
.toUriString();
|
||||||
|
@ -927,7 +858,7 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
waitingRoom);
|
waitingRoom);
|
||||||
|
|
||||||
final String body = this.zoomProctoringService.jsonMapper.writeValueAsString(createRoomRequest);
|
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);
|
headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
|
||||||
return exchange(url, HttpMethod.POST, body, headers);
|
return exchange(url, HttpMethod.POST, body, headers);
|
||||||
|
|
||||||
|
@ -937,21 +868,18 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResponseEntity<String> deleteMeeting(
|
public ResponseEntity<String> deleteMeeting(final Long meetingId) {
|
||||||
final String zoomServerUrl,
|
|
||||||
final ClientCredentials credentials,
|
|
||||||
final Long meetingId) {
|
|
||||||
|
|
||||||
// try to set set meeting status to ended first
|
// try to set set meeting status to ended first
|
||||||
try {
|
try {
|
||||||
|
|
||||||
final String url = UriComponentsBuilder
|
final String url = UriComponentsBuilder
|
||||||
.fromUriString(zoomServerUrl)
|
.fromUriString(this.proctoringSettings.serverURL)
|
||||||
.path(API_END_MEETING_ENDPOINT)
|
.path(API_END_MEETING_ENDPOINT)
|
||||||
.buildAndExpand(meetingId)
|
.buildAndExpand(meetingId)
|
||||||
.toUriString();
|
.toUriString();
|
||||||
|
|
||||||
final HttpHeaders headers = getHeaders(credentials);
|
final HttpHeaders headers = getHeaders();
|
||||||
headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
|
headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
|
||||||
|
|
||||||
final ResponseEntity<String> exchange = exchange(
|
final ResponseEntity<String> exchange = exchange(
|
||||||
|
@ -976,12 +904,12 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
final String url = UriComponentsBuilder
|
final String url = UriComponentsBuilder
|
||||||
.fromUriString(zoomServerUrl)
|
.fromUriString(this.proctoringSettings.serverURL)
|
||||||
.path(API_DELETE_MEETING_ENDPOINT)
|
.path(API_DELETE_MEETING_ENDPOINT)
|
||||||
.buildAndExpand(meetingId)
|
.buildAndExpand(meetingId)
|
||||||
.toUriString();
|
.toUriString();
|
||||||
|
|
||||||
return exchange(url, HttpMethod.DELETE, credentials);
|
return exchange(url, HttpMethod.DELETE);
|
||||||
|
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
log.warn("Failed to delete Zoom ad-hoc meeting: {} cause: {} / {}",
|
log.warn("Failed to delete Zoom ad-hoc meeting: {} cause: {} / {}",
|
||||||
|
@ -992,20 +920,17 @@ public class ZoomProctoringService implements ExamProctoringService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResponseEntity<String> deleteUser(
|
public ResponseEntity<String> deleteUser(final String userId) {
|
||||||
final String zoomServerUrl,
|
|
||||||
final ClientCredentials credentials,
|
|
||||||
final String userId) {
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final String url = UriComponentsBuilder
|
final String url = UriComponentsBuilder
|
||||||
.fromUriString(zoomServerUrl)
|
.fromUriString(this.proctoringSettings.serverURL)
|
||||||
.path(API_DELETE_USER_ENDPOINT)
|
.path(API_DELETE_USER_ENDPOINT)
|
||||||
.buildAndExpand(userId)
|
.buildAndExpand(userId)
|
||||||
.normalize()
|
.normalize()
|
||||||
.toUriString();
|
.toUriString();
|
||||||
|
|
||||||
return exchange(url, HttpMethod.DELETE, credentials);
|
return exchange(url, HttpMethod.DELETE);
|
||||||
|
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
log.error("Failed to delete Zoom ad-hoc user with id: {} cause: {} / {}",
|
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(
|
private ResponseEntity<String> exchange(
|
||||||
final String url,
|
final String url,
|
||||||
final HttpMethod method,
|
final HttpMethod method) {
|
||||||
final ClientCredentials credentials) {
|
|
||||||
|
|
||||||
return exchange(url, method, null, getHeaders(credentials));
|
return exchange(url, method, null, getHeaders());
|
||||||
}
|
}
|
||||||
|
|
||||||
private ResponseEntity<String> exchange(
|
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)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
static class Settings {
|
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 mute_upon_entry = false;
|
||||||
@JsonProperty final boolean join_before_host;
|
@JsonProperty final boolean join_before_host;
|
||||||
@JsonProperty final int jbh_time = 0;
|
@JsonProperty final int jbh_time = 0;
|
||||||
|
|
|
@ -25,7 +25,6 @@ public class ProctoringSettingsValidator
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
//if (value.enableProctoring) {
|
|
||||||
if (value.serverType == ProctoringServerType.JITSI_MEET || value.serverType == ProctoringServerType.ZOOM) {
|
if (value.serverType == ProctoringServerType.JITSI_MEET || value.serverType == ProctoringServerType.ZOOM) {
|
||||||
boolean passed = true;
|
boolean passed = true;
|
||||||
|
|
||||||
|
@ -37,25 +36,44 @@ public class ProctoringSettingsValidator
|
||||||
passed = false;
|
passed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (StringUtils.isBlank(value.appKey)) {
|
if (value.serverType == ProctoringServerType.JITSI_MEET) {
|
||||||
context.disableDefaultConstraintViolation();
|
if (StringUtils.isBlank(value.appKey)) {
|
||||||
context
|
context.disableDefaultConstraintViolation();
|
||||||
.buildConstraintViolationWithTemplate("proctoringSettings:appKey:notNull")
|
context
|
||||||
.addPropertyNode("appKey").addConstraintViolation();
|
.buildConstraintViolationWithTemplate("proctoringSettings:appKey:notNull")
|
||||||
passed = false;
|
.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)) {
|
if (value.serverType == ProctoringServerType.ZOOM) {
|
||||||
context.disableDefaultConstraintViolation();
|
if (StringUtils.isBlank(value.sdkKey)) {
|
||||||
context
|
context.disableDefaultConstraintViolation();
|
||||||
.buildConstraintViolationWithTemplate("proctoringSettings:appSecret:notNull")
|
context
|
||||||
.addPropertyNode("appSecret").addConstraintViolation();
|
.buildConstraintViolationWithTemplate("proctoringSettings:sdkKey:notNull")
|
||||||
passed = false;
|
.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 passed;
|
||||||
}
|
}
|
||||||
//}
|
|
||||||
|
|
||||||
return true;
|
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.institution.SecurityKeyService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
|
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.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.session.ExamSessionService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationService;
|
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 ExamSessionService examSessionService;
|
||||||
private final SEBRestrictionService sebRestrictionService;
|
private final SEBRestrictionService sebRestrictionService;
|
||||||
private final SecurityKeyService securityKeyService;
|
private final SecurityKeyService securityKeyService;
|
||||||
|
private final ExamProctoringRoomService examProctoringRoomService;
|
||||||
|
|
||||||
public ExamAdministrationController(
|
public ExamAdministrationController(
|
||||||
final AuthorizationService authorization,
|
final AuthorizationService authorization,
|
||||||
|
@ -101,7 +103,8 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
||||||
final ExamTemplateService examTemplateService,
|
final ExamTemplateService examTemplateService,
|
||||||
final ExamSessionService examSessionService,
|
final ExamSessionService examSessionService,
|
||||||
final SEBRestrictionService sebRestrictionService,
|
final SEBRestrictionService sebRestrictionService,
|
||||||
final SecurityKeyService securityKeyService) {
|
final SecurityKeyService securityKeyService,
|
||||||
|
final ExamProctoringRoomService examProctoringRoomService) {
|
||||||
|
|
||||||
super(authorization,
|
super(authorization,
|
||||||
bulkActionService,
|
bulkActionService,
|
||||||
|
@ -118,6 +121,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
||||||
this.examSessionService = examSessionService;
|
this.examSessionService = examSessionService;
|
||||||
this.sebRestrictionService = sebRestrictionService;
|
this.sebRestrictionService = sebRestrictionService;
|
||||||
this.securityKeyService = securityKeyService;
|
this.securityKeyService = securityKeyService;
|
||||||
|
this.examProctoringRoomService = examProctoringRoomService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -486,6 +490,29 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
|
||||||
.getOrThrow();
|
.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
|
// **** Proctoring
|
||||||
// ****************************************************************************
|
// ****************************************************************************
|
||||||
|
|
||||||
|
|
|
@ -483,6 +483,7 @@ sebserver.quizdiscovery.quiz.details.additional.time_limit.toolitp=The time limi
|
||||||
################################
|
################################
|
||||||
|
|
||||||
sebserver.exam.list.actions=
|
sebserver.exam.list.actions=
|
||||||
|
sebserver.exam.security.actions=
|
||||||
sebserver.exam.list.title=Exam
|
sebserver.exam.list.title=Exam
|
||||||
sebserver.exam.list.title.subtitle=
|
sebserver.exam.list.title.subtitle=
|
||||||
sebserver.exam.list.column.institution=Institution
|
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.url.tooltip=The proctoring server URL
|
||||||
sebserver.exam.proctoring.form.collectingRoomSize=Collecting Room Size
|
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.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.appkey.jitsi=App Key
|
||||||
sebserver.exam.proctoring.form.secret=App Secret
|
sebserver.exam.proctoring.form.appkey.jitsi.tooltip=The application key of the proctoring service server
|
||||||
sebserver.exam.proctoring.form.secret.tooltip=The secret used to access the proctoring service
|
sebserver.exam.proctoring.form.secret.jitsi=App Secret
|
||||||
sebserver.exam.proctoring.form.sdkkey=SDK Key (Zoom - MacOS/iOS)
|
sebserver.exam.proctoring.form.secret.jitsi.tooltip=The secret used to access the proctoring service
|
||||||
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.appkey.zoom=App Key (Deprecated)
|
||||||
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.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=Enabled Features
|
||||||
sebserver.exam.proctoring.form.features.TOWN_HALL=Town-Hall Room
|
sebserver.exam.proctoring.form.features.TOWN_HALL=Town-Hall Room
|
||||||
sebserver.exam.proctoring.form.features.ONE_TO_ONE=One to One 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.features.RESET_BROADCAST_ON_LAVE=Reset broadcast on leave
|
||||||
sebserver.exam.proctoring.form.useZoomAppClient=Use Zoom App-Client
|
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.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=Jitsi Meet Server
|
||||||
sebserver.exam.proctoring.type.servertype.JITSI_MEET.tooltip=Use a Jitsi Meet server for proctoring
|
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.edit=App Signature Key
|
||||||
sebserver.exam.signaturekey.action.save=Save Settings
|
sebserver.exam.signaturekey.action.save=Save Settings
|
||||||
sebserver.exam.signaturekey.action.cancel=Cancel and back to Exam
|
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.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.showGrant=Show Security Grant
|
||||||
sebserver.exam.signaturekey.action.deleteGrant=Delete Security Grant
|
sebserver.exam.signaturekey.action.deleteGrant=Delete Security Grant
|
||||||
sebserver.exam.signaturekey.title=App Signature Key Overview
|
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,
|
false,
|
||||||
"apiKey",
|
"apiKey",
|
||||||
"apiSecret",
|
"apiSecret",
|
||||||
|
"accountId",
|
||||||
|
"clientId",
|
||||||
|
"clientSecret",
|
||||||
"sdkKey",
|
"sdkKey",
|
||||||
"sdkSecret",
|
"sdkSecret",
|
||||||
false);
|
false);
|
||||||
|
@ -3721,6 +3724,7 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest {
|
||||||
EnumSet.allOf(ProctoringFeature.class),
|
EnumSet.allOf(ProctoringFeature.class),
|
||||||
true,
|
true,
|
||||||
"appKey", "appSecret",
|
"appKey", "appSecret",
|
||||||
|
"accountId", "clientId", "clientSecret",
|
||||||
"sdkKey", "sdkSecret",
|
"sdkKey", "sdkSecret",
|
||||||
false);
|
false);
|
||||||
|
|
||||||
|
|
|
@ -78,7 +78,9 @@ public class ExamProctoringRoomServiceTest extends AdministrationAPIIntegrationT
|
||||||
2L,
|
2L,
|
||||||
new ProctoringServiceSettings(
|
new ProctoringServiceSettings(
|
||||||
2L, true, ProctoringServerType.JITSI_MEET, "", 1, null, false,
|
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());
|
assertTrue(this.examAdminService.isProctoringEnabled(2L).get());
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ public class ExamJITSIProctoringServiceTest {
|
||||||
|
|
||||||
final ProctoringServiceSettings proctoringServiceSettings = new ProctoringServiceSettings(
|
final ProctoringServiceSettings proctoringServiceSettings = new ProctoringServiceSettings(
|
||||||
1L, true, ProctoringServerType.JITSI_MEET, "URL?",
|
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
|
final Result<Boolean> testExamProctoring = jitsiProctoringService
|
||||||
.testExamProctoring(proctoringServiceSettings);
|
.testExamProctoring(proctoringServiceSettings);
|
||||||
|
@ -59,7 +59,9 @@ public class ExamJITSIProctoringServiceTest {
|
||||||
|
|
||||||
final ProctoringServiceSettings proctoringServiceSettings = new ProctoringServiceSettings(
|
final ProctoringServiceSettings proctoringServiceSettings = new ProctoringServiceSettings(
|
||||||
1L, true, ProctoringServerType.JITSI_MEET, "http://jitsi.ch",
|
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
|
final Result<ProctoringRoomConnection> proctorRoomConnection = jitsiProctoringService
|
||||||
.getProctorRoomConnection(proctoringServiceSettings, "TestRoom", "Test-User");
|
.getProctorRoomConnection(proctoringServiceSettings, "TestRoom", "Test-User");
|
||||||
|
|
Loading…
Reference in a new issue