From a44f78213cacaed9c7656eae386ee415c672017b Mon Sep 17 00:00:00 2001 From: frcroth Date: Wed, 13 Nov 2024 15:28:20 +0100 Subject: [PATCH 01/17] Adapt create org route in backend to accept terms of service version --- app/controllers/AuthenticationController.scala | 13 +++++++++---- app/controllers/OrganizationController.scala | 6 +----- app/models/organization/OrganizationService.scala | 10 ++++++++++ 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/app/controllers/AuthenticationController.scala b/app/controllers/AuthenticationController.scala index f6609755e09..2a25e793a88 100755 --- a/app/controllers/AuthenticationController.scala +++ b/app/controllers/AuthenticationController.scala @@ -621,6 +621,8 @@ class AuthenticationController @Inject()( dataStoreToken <- bearerTokenAuthenticatorService.createAndInitDataStoreTokenForUser(user) _ <- organizationService .createOrganizationDirectory(organization._id, dataStoreToken) ?~> "organization.folderCreation.failed" + _ <- Fox.runOptional(signUpData.acceptedTermsOfService)(version => + organizationService.acceptTermsOfService(organization._id, version)) } yield { Mailer ! Send(defaultMails .newOrganizationMail(organization.name, email, request.headers.get("Host").getOrElse(""))) @@ -730,7 +732,8 @@ trait AuthForms { firstName: String, lastName: String, password: String, - inviteToken: Option[String]) + inviteToken: Option[String], + acceptedTermsOfService: Option[Int]) def signUpForm(implicit messages: Messages): Form[SignUpData] = Form( @@ -745,8 +748,9 @@ trait AuthForms { "firstName" -> nonEmptyText, "lastName" -> nonEmptyText, "inviteToken" -> optional(nonEmptyText), - )((organization, organizationName, email, password, firstName, lastName, inviteToken) => - SignUpData(organization, organizationName, email, firstName, lastName, password._1, inviteToken))( + "acceptTermsOfService" -> optional(number) + )((organization, organizationName, email, password, firstName, lastName, inviteToken, acceptTos) => + SignUpData(organization, organizationName, email, firstName, lastName, password._1, inviteToken, acceptTos))( signUpData => Some( (signUpData.organization, @@ -755,7 +759,8 @@ trait AuthForms { ("", ""), signUpData.firstName, signUpData.lastName, - signUpData.inviteToken)))) + signUpData.inviteToken, + signUpData.acceptedTermsOfService)))) // Sign in case class SignInData(email: String, password: String) diff --git a/app/controllers/OrganizationController.scala b/app/controllers/OrganizationController.scala index 19ae573e35a..f1bd61c9c4e 100755 --- a/app/controllers/OrganizationController.scala +++ b/app/controllers/OrganizationController.scala @@ -3,7 +3,6 @@ package controllers import org.apache.pekko.actor.ActorSystem import play.silhouette.api.Silhouette import com.scalableminds.util.accesscontext.{DBAccessContext, GlobalAccessContext} -import com.scalableminds.util.time.Instant import com.scalableminds.util.tools.{Fox, FoxImplicits} import mail.{DefaultMails, Send} @@ -141,10 +140,7 @@ class OrganizationController @Inject()( def acceptTermsOfService(version: Int): Action[AnyContent] = sil.SecuredAction.async { implicit request => for { _ <- bool2Fox(request.identity.isOrganizationOwner) ?~> "termsOfService.onlyOrganizationOwner" - _ <- bool2Fox(conf.WebKnossos.TermsOfService.enabled) ?~> "termsOfService.notEnabled" - requiredVersion = conf.WebKnossos.TermsOfService.version - _ <- bool2Fox(version == requiredVersion) ?~> Messages("termsOfService.versionMismatch", requiredVersion, version) - _ <- organizationDAO.acceptTermsOfService(request.identity._organization, version, Instant.now) + _ <- organizationService.acceptTermsOfService(request.identity._organization, version) } yield Ok } diff --git a/app/models/organization/OrganizationService.scala b/app/models/organization/OrganizationService.scala index 6e105e18c2c..3fd81773d04 100644 --- a/app/models/organization/OrganizationService.scala +++ b/app/models/organization/OrganizationService.scala @@ -1,6 +1,7 @@ package models.organization import com.scalableminds.util.accesscontext.{DBAccessContext, GlobalAccessContext} +import com.scalableminds.util.time.Instant import com.scalableminds.util.tools.{Fox, FoxImplicits, TextUtils} import com.scalableminds.webknossos.datastore.rpc.RPC import com.typesafe.scalalogging.LazyLogging @@ -10,6 +11,7 @@ import models.dataset.{DataStore, DataStoreDAO} import models.folder.{Folder, FolderDAO, FolderService} import models.team.{PricingPlan, Team, TeamDAO} import models.user.{Invite, MultiUserDAO, User, UserDAO, UserService} +import play.api.i18n.{Messages, MessagesProvider} import play.api.libs.json.{JsArray, JsObject, Json} import utils.{ObjectId, WkConf} @@ -165,4 +167,12 @@ class OrganizationService @Inject()(organizationDAO: OrganizationDAO, def newUserMailRecipient(organization: Organization)(implicit ctx: DBAccessContext): Fox[String] = fallbackOnOwnerEmail(organization.newUserMailingList, organization) + def acceptTermsOfService(organizationId: String, version: Int)(implicit ctx: DBAccessContext, mp: MessagesProvider): Fox[Unit] = + for { + _ <- bool2Fox(conf.WebKnossos.TermsOfService.enabled) ?~> "termsOfService.notEnabled" + requiredVersion = conf.WebKnossos.TermsOfService.version + _ <- bool2Fox(version == requiredVersion) ?~> Messages("termsOfService.versionMismatch", requiredVersion, version) + _ <- organizationDAO.acceptTermsOfService(organizationId, version, Instant.now) + } yield () + } From 94716a7bda84996792ed28fd6b32d5aed9ee01a4 Mon Sep 17 00:00:00 2001 From: frcroth Date: Wed, 13 Nov 2024 15:35:07 +0100 Subject: [PATCH 02/17] fix inconsistency --- app/controllers/AuthenticationController.scala | 2 +- conf/application.conf | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/AuthenticationController.scala b/app/controllers/AuthenticationController.scala index 2a25e793a88..03d8fa7a6ae 100755 --- a/app/controllers/AuthenticationController.scala +++ b/app/controllers/AuthenticationController.scala @@ -748,7 +748,7 @@ trait AuthForms { "firstName" -> nonEmptyText, "lastName" -> nonEmptyText, "inviteToken" -> optional(nonEmptyText), - "acceptTermsOfService" -> optional(number) + "acceptedTermsOfService" -> optional(number) )((organization, organizationName, email, password, firstName, lastName, inviteToken, acceptTos) => SignUpData(organization, organizationName, email, firstName, lastName, password._1, inviteToken, acceptTos))( signUpData => diff --git a/conf/application.conf b/conf/application.conf index 0ae8b6f25dd..eb5ed4d566b 100644 --- a/conf/application.conf +++ b/conf/application.conf @@ -116,7 +116,7 @@ webKnossos { Please add the information of the operator to comply with GDPR. """ termsOfService { - enabled = false + enabled = true # The URL will be embedded into an iFrame url = "https://webknossos.org/terms-of-service" acceptanceDeadline = "2023-01-01T00:00:00Z" @@ -146,7 +146,7 @@ features { discussionBoard = "https://forum.image.sc/tag/webknossos" discussionBoardRequiresAdmin = false hideNavbarLogin = false - isWkorgInstance = false + isWkorgInstance = true recommendWkorgInstance = true taskReopenAllowedInSeconds = 30 allowDeleteDatasets = true From 5607fcc8404100af1f7dd893ec30ede2b61b0d6a Mon Sep 17 00:00:00 2001 From: frcroth Date: Wed, 13 Nov 2024 15:49:09 +0100 Subject: [PATCH 03/17] Fix formatting --- app/models/organization/OrganizationService.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/organization/OrganizationService.scala b/app/models/organization/OrganizationService.scala index 3fd81773d04..0760d67cb3b 100644 --- a/app/models/organization/OrganizationService.scala +++ b/app/models/organization/OrganizationService.scala @@ -167,7 +167,8 @@ class OrganizationService @Inject()(organizationDAO: OrganizationDAO, def newUserMailRecipient(organization: Organization)(implicit ctx: DBAccessContext): Fox[String] = fallbackOnOwnerEmail(organization.newUserMailingList, organization) - def acceptTermsOfService(organizationId: String, version: Int)(implicit ctx: DBAccessContext, mp: MessagesProvider): Fox[Unit] = + def acceptTermsOfService(organizationId: String, version: Int)(implicit ctx: DBAccessContext, + mp: MessagesProvider): Fox[Unit] = for { _ <- bool2Fox(conf.WebKnossos.TermsOfService.enabled) ?~> "termsOfService.notEnabled" requiredVersion = conf.WebKnossos.TermsOfService.version From 05eed1c5e8eb10790e840ac45db26919a64d9e3e Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Thu, 14 Nov 2024 20:28:20 +0100 Subject: [PATCH 04/17] start to implement frontend --- .../admin/auth/registration_form_wkorg.tsx | 22 +++++++++++ frontend/javascripts/messages.tsx | 2 + .../javascripts/test/misc/node_types.spec.ts | 37 +++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 frontend/javascripts/test/misc/node_types.spec.ts diff --git a/frontend/javascripts/admin/auth/registration_form_wkorg.tsx b/frontend/javascripts/admin/auth/registration_form_wkorg.tsx index b18395a1388..1eba8dc7df7 100644 --- a/frontend/javascripts/admin/auth/registration_form_wkorg.tsx +++ b/frontend/javascripts/admin/auth/registration_form_wkorg.tsx @@ -176,6 +176,28 @@ function RegistrationFormWKOrg(props: Props) { . + + {/* WIP! see terms_of_services.tsx */} + + value + ? Promise.resolve() + : Promise.reject(new Error(messages["auth.tos_check_required"])), + }, + ]} + > + + I agree to the{" "} + + terms of service + + . + + { + const serverNode: ServerNode = { + id: "1", + name: "Server1", + status: "active", + }; + + t.is(serverNode.id, "1"); + t.is(serverNode.name, "Server1"); + t.is(serverNode.status, "active"); +}); + +test("MutableNode has the right fields", (t) => { + const mutableNode: MutableNode = { + id: "2", + value: "some value", + updatedAt: new Date(), + }; + + t.is(mutableNode.id, "2"); + t.is(mutableNode.value, "some value"); + t.true(mutableNode.updatedAt instanceof Date); +}); From ae61dc64e3d2da3c9566772d0c74e659814721f9 Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Thu, 14 Nov 2024 23:24:02 +0100 Subject: [PATCH 05/17] delete old tos modal, add checkboxes to forms and add style --- .../javascripts/admin/api/terms_of_service.ts | 14 -- .../admin/auth/registration_form_generic.tsx | 33 +++ .../admin/auth/registration_form_wkorg.tsx | 26 ++- .../components/terms_of_services_check.tsx | 214 ------------------ frontend/javascripts/router.tsx | 2 - frontend/stylesheets/main.less | 5 + 6 files changed, 54 insertions(+), 240 deletions(-) delete mode 100644 frontend/javascripts/components/terms_of_services_check.tsx diff --git a/frontend/javascripts/admin/api/terms_of_service.ts b/frontend/javascripts/admin/api/terms_of_service.ts index c1cf868c5a8..8d989704f98 100644 --- a/frontend/javascripts/admin/api/terms_of_service.ts +++ b/frontend/javascripts/admin/api/terms_of_service.ts @@ -7,17 +7,3 @@ export function getTermsOfService(): Promise<{ }> { return Request.receiveJSON("/api/termsOfService"); } - -export type AcceptanceInfo = { - acceptanceDeadline: number; - acceptanceDeadlinePassed: boolean; - acceptanceNeeded: boolean; -}; - -export async function requiresTermsOfServiceAcceptance(): Promise { - return await Request.receiveJSON("/api/termsOfService/acceptanceNeeded"); -} - -export function acceptTermsOfService(version: number): Promise { - return Request.receiveJSON(`/api/termsOfService/accept?version=${version}`, { method: "POST" }); -} diff --git a/frontend/javascripts/admin/auth/registration_form_generic.tsx b/frontend/javascripts/admin/auth/registration_form_generic.tsx index a4686ce5829..f820e2b7da5 100644 --- a/frontend/javascripts/admin/auth/registration_form_generic.tsx +++ b/frontend/javascripts/admin/auth/registration_form_generic.tsx @@ -9,6 +9,8 @@ import Store from "oxalis/throttled_store"; import messages from "messages"; import { setHasOrganizationsAction } from "oxalis/model/actions/ui_actions"; import { setActiveOrganizationAction } from "oxalis/model/actions/organization_actions"; +import { useFetch } from "libs/react_helpers"; +import { getTermsOfService } from "admin/api/terms_of_service"; const FormItem = Form.Item; const { Password } = Input; @@ -26,6 +28,8 @@ type Props = { function RegistrationFormGeneric(props: Props) { const [form] = Form.useForm(); + const terms = useFetch(getTermsOfService, null, []); + const onFinish = async (formValues: Record) => { await Request.sendJSONReceiveJSON( props.organizationIdToCreate != null @@ -276,6 +280,7 @@ function RegistrationFormGeneric(props: Props) { {props.hidePrivacyStatement ? null : ( )} + {terms != null && !terms.enabled ? null : ( + + value + ? Promise.resolve() + : Promise.reject(new Error(messages["auth.tos_check_required"])), + }, + ]} + > + + I agree to the{" "} + {terms == null ? ( + "terms of service" + ) : ( + + terms of service + + )} + . + + + )} +