diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index cb4ba59a80e..5abef6583c0 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -16,6 +16,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released ### Changed - Reading image files on datastore filesystem is now done asynchronously. [#8126](https://github.com/scalableminds/webknossos/pull/8126) - Improved error messages for starting jobs on datasets from other organizations. [#8181](https://github.com/scalableminds/webknossos/pull/8181) +- Terms of Service for Webknossos are now accepted at registration, not afterward. [#8193](https://github.com/scalableminds/webknossos/pull/8193) - Removed bounding box size restriction for inferral jobs for super users. [#8200](https://github.com/scalableminds/webknossos/pull/8200) ### Fixed diff --git a/app/controllers/AuthenticationController.scala b/app/controllers/AuthenticationController.scala index f6609755e09..f8c87168bd7 100755 --- a/app/controllers/AuthenticationController.scala +++ b/app/controllers/AuthenticationController.scala @@ -23,7 +23,7 @@ import org.apache.commons.codec.digest.{HmacAlgorithms, HmacUtils} import play.api.data.Form import play.api.data.Forms.{email, _} import play.api.data.validation.Constraints._ -import play.api.i18n.Messages +import play.api.i18n.{Messages, MessagesProvider} import play.api.libs.json._ import play.api.mvc.{Action, AnyContent, Cookie, PlayBodyParsers, Request, Result} import security.{ @@ -621,6 +621,8 @@ class AuthenticationController @Inject()( dataStoreToken <- bearerTokenAuthenticatorService.createAndInitDataStoreTokenForUser(user) _ <- organizationService .createOrganizationDirectory(organization._id, dataStoreToken) ?~> "organization.folderCreation.failed" + _ <- Fox.runIf(conf.WebKnossos.TermsOfService.enabled)( + acceptTermsOfServiceForUser(user, signUpData.acceptedTermsOfService)) } yield { Mailer ! Send(defaultMails .newOrganizationMail(organization.name, email, request.headers.get("Host").getOrElse(""))) @@ -637,6 +639,13 @@ class AuthenticationController @Inject()( ) } + private def acceptTermsOfServiceForUser(user: User, termsOfServiceVersion: Option[Int])( + implicit m: MessagesProvider): Fox[Unit] = + for { + acceptedVersion <- Fox.option2Fox(termsOfServiceVersion) ?~> "Terms of service must be accepted." + _ <- organizationService.acceptTermsOfService(user._organization, acceptedVersion)(DBAccessContext(Some(user)), m) + } yield () + case class CreateUserInOrganizationParameters(firstName: String, lastName: String, email: String, @@ -730,7 +739,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 +755,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))( + "acceptedTermsOfService" -> 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 +766,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..a5f29b74310 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} @@ -24,8 +26,7 @@ class OrganizationService @Inject()(organizationDAO: OrganizationDAO, folderService: FolderService, userService: UserService, rpc: RPC, - conf: WkConf, -)(implicit ec: ExecutionContext) + conf: WkConf)(implicit ec: ExecutionContext) extends FoxImplicits with LazyLogging { @@ -165,4 +166,13 @@ 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, + m: 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 () + } diff --git a/conf/application.conf b/conf/application.conf index 2cfdcccb0cd..751fcd1e5aa 100644 --- a/conf/application.conf +++ b/conf/application.conf @@ -117,7 +117,7 @@ webKnossos { """ termsOfService { enabled = false - # The URL will be embedded into an iFrame + # The URL will be linked to or embedded into an iFrame url = "https://webknossos.org/terms-of-service" acceptanceDeadline = "2023-01-01T00:00:00Z" version = 1 diff --git a/frontend/javascripts/admin/auth/registration_form_generic.tsx b/frontend/javascripts/admin/auth/registration_form_generic.tsx index a4686ce5829..dbb6c877a55 100644 --- a/frontend/javascripts/admin/auth/registration_form_generic.tsx +++ b/frontend/javascripts/admin/auth/registration_form_generic.tsx @@ -9,6 +9,9 @@ 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"; +import { TOSCheckFormItem } from "./tos_check_form_item"; const FormItem = Form.Item; const { Password } = Input; @@ -26,6 +29,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 @@ -274,28 +279,32 @@ function RegistrationFormGeneric(props: Props) { - {props.hidePrivacyStatement ? null : ( - - value - ? Promise.resolve() - : Promise.reject(new Error(messages["auth.privacy_check_required"])), - }, - ]} - > - - I agree to storage and processing of my personal data as described in the{" "} - - privacy statement - - . - - - )} +
+ {props.hidePrivacyStatement ? null : ( + + value + ? Promise.resolve() + : Promise.reject(new Error(messages["auth.privacy_check_required"])), + }, + ]} + > + + I agree to storage and processing of my personal data as described in the{" "} + + privacy statement + + . + + + )} + +
+