Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Accept terms of Service at signup #8193

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions app/controllers/AuthenticationController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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("")))
Expand Down Expand Up @@ -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(
Expand All @@ -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))(
"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,
Expand All @@ -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)
Expand Down
6 changes: 1 addition & 5 deletions app/controllers/OrganizationController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand Down Expand Up @@ -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
}

Expand Down
11 changes: 11 additions & 0 deletions app/models/organization/OrganizationService.scala
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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}

Expand Down Expand Up @@ -165,4 +167,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,
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 ()

}
4 changes: 2 additions & 2 deletions conf/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
14 changes: 0 additions & 14 deletions frontend/javascripts/admin/api/terms_of_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AcceptanceInfo> {
return await Request.receiveJSON("/api/termsOfService/acceptanceNeeded");
}

export function acceptTermsOfService(version: number): Promise<unknown> {
return Request.receiveJSON(`/api/termsOfService/accept?version=${version}`, { method: "POST" });
}
77 changes: 55 additions & 22 deletions frontend/javascripts/admin/auth/registration_form_generic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,6 +28,8 @@ type Props = {
function RegistrationFormGeneric(props: Props) {
const [form] = Form.useForm();

const terms = useFetch(getTermsOfService, null, []);

const onFinish = async (formValues: Record<string, any>) => {
await Request.sendJSONReceiveJSON(
props.organizationIdToCreate != null
Expand Down Expand Up @@ -274,28 +278,57 @@ function RegistrationFormGeneric(props: Props) {
</FormItem>
</Col>
</Row>
{props.hidePrivacyStatement ? null : (
<FormItem
name="privacy_check"
valuePropName="checked"
rules={[
{
validator: (_, value) =>
value
? Promise.resolve()
: Promise.reject(new Error(messages["auth.privacy_check_required"])),
},
]}
>
<Checkbox>
I agree to storage and processing of my personal data as described in the{" "}
<a target="_blank" href="/privacy" rel="noopener noreferrer">
privacy statement
</a>
.
</Checkbox>
</FormItem>
)}
<div className="registration-form-checkboxes">
{props.hidePrivacyStatement ? null : (
<FormItem
name="privacy_check"
valuePropName="checked"
rules={[
{
validator: (_, value) =>
value
? Promise.resolve()
: Promise.reject(new Error(messages["auth.privacy_check_required"])),
},
]}
>
<Checkbox>
I agree to storage and processing of my personal data as described in the{" "}
<a target="_blank" href="/privacy" rel="noopener noreferrer">
privacy statement
</a>
.
</Checkbox>
</FormItem>
)}
{terms != null && !terms.enabled ? null : (
<FormItem
name="tos_check"
valuePropName="checked"
rules={[
{
validator: (_, value) =>
value
? Promise.resolve()
: Promise.reject(new Error(messages["auth.tos_check_required"])),
},
]}
>
<Checkbox disabled={terms == null}>
I agree to the{" "}
{terms == null ? (
"terms of service"
) : (
<a target="_blank" href={terms.url} rel="noopener noreferrer">
terms of service
</a>
)}
.
</Checkbox>
</FormItem>
)}
</div>

<FormItem>
<Button
size="large"
Expand Down
79 changes: 54 additions & 25 deletions frontend/javascripts/admin/auth/registration_form_wkorg.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import Request from "libs/request";
import Store from "oxalis/throttled_store";
import messages from "messages";
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;
Expand All @@ -30,6 +32,7 @@ function generateOrganizationId() {
function RegistrationFormWKOrg(props: Props) {
const [form] = Form.useForm();
const organizationId = useRef(generateOrganizationId());
const terms = useFetch(getTermsOfService, null, []);

async function onFinish(formValues: Record<string, any>) {
await Request.sendJSONReceiveJSON("/api/auth/createOrganizationWithAdmin", {
Expand All @@ -43,6 +46,7 @@ function RegistrationFormWKOrg(props: Props) {
},
organization: organizationId.current,
organizationName: `${formValues.firstName.trim()} ${formValues.lastName.trim()} Lab`,
acceptedTermsOfService: terms?.version,
},
});
const [user, organization] = await loginUser({
Expand Down Expand Up @@ -155,32 +159,57 @@ function RegistrationFormWKOrg(props: Props) {
placeholder="Password"
/>
</FormItem>
<div className="registration-form-checkboxes">
<FormItem
name="privacy_check"
valuePropName="checked"
rules={[
{
validator: (_, value) =>
value
? Promise.resolve()
: Promise.reject(new Error(messages["auth.privacy_check_required"])),
},
]}
>
<Checkbox>
I agree to storage and processing of my personal data as described in the{" "}
<a target="_blank" href="/privacy" rel="noopener noreferrer">
privacy statement
</a>
.
</Checkbox>
</FormItem>

<FormItem
name="privacy_check"
valuePropName="checked"
rules={[
{
validator: (_, value) =>
value
? Promise.resolve()
: Promise.reject(new Error(messages["auth.privacy_check_required"])),
},
]}
>
<Checkbox>
I agree to storage and processing of my personal data as described in the{" "}
<a target="_blank" href="/privacy" rel="noopener noreferrer">
privacy statement
</a>
.
</Checkbox>
</FormItem>
<FormItem
style={{
marginBottom: 10,
}}
>
{terms != null && !terms.enabled ? null : (
<FormItem
name="tos_check"
valuePropName="checked"
rules={[
{
validator: (_, value) =>
value
? Promise.resolve()
: Promise.reject(new Error(messages["auth.tos_check_required"])),
},
]}
>
<Checkbox disabled={terms == null}>
I agree to the{" "}
{terms == null ? (
"terms of service"
) : (
<a target="_blank" href={terms.url} rel="noopener noreferrer">
terms of service
</a>
)}
.
</Checkbox>
</FormItem>
)}
</div>

<FormItem>
<Button
size="large"
type="primary"
Expand Down
Loading