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