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