From bdab4af7d2b1f7d496a22b2695bb5180c64bd513 Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Wed, 4 Jun 2025 09:45:21 +0200 Subject: [PATCH 01/20] allow user to change email and set email in user list to read-only --- .../admin/auth/change_email_view.tsx | 166 ++++++++++++++++++ .../javascripts/admin/user/user_list_view.tsx | 55 +----- frontend/javascripts/navbar.tsx | 1 + frontend/javascripts/router.tsx | 6 + 4 files changed, 174 insertions(+), 54 deletions(-) create mode 100644 frontend/javascripts/admin/auth/change_email_view.tsx diff --git a/frontend/javascripts/admin/auth/change_email_view.tsx b/frontend/javascripts/admin/auth/change_email_view.tsx new file mode 100644 index 00000000000..1f2ba3b8744 --- /dev/null +++ b/frontend/javascripts/admin/auth/change_email_view.tsx @@ -0,0 +1,166 @@ +import { LockOutlined, MailOutlined } from "@ant-design/icons"; +import { updateUser } from "admin/rest_api"; +import { Alert, Button, Col, Form, Input, Row } from "antd"; +import { useWkSelector } from "libs/react_hooks"; +import Request from "libs/request"; +import Toast from "libs/toast"; +import { type RouteComponentProps, withRouter } from "react-router-dom"; +const FormItem = Form.Item; + +function ChangeEmailView() { + const [form] = Form.useForm(); + const activeUser = useWkSelector((state) => state.activeUser); + + async function changeEmail(newEmail: string) { + const newUser = Object.assign({}, activeUser, { + email: newEmail, + }); + return updateUser(newUser); + } + + function onFinish() { + const newEmail = form.getFieldValue("newEmail"); + changeEmail(newEmail) + .then(() => { + Toast.success("Email address changed successfully. You will be logged out."); + return Request.receiveJSON("/api/auth/logout"); + }) + .then(() => { + form.resetFields(); + // Redirect to login page after successful email change + window.location.href = "/auth/login"; + }) + .catch((error) => { + Toast.error( + "An unexpected error occurred while changing the email address: " + error.message, + ); + }); + } + + function checkEmailsAreMatching(value: string, otherEmailFieldKey: string[]) { + const otherFieldValue = form.getFieldValue(otherEmailFieldKey); + + if (value && otherFieldValue) { + if (value !== otherFieldValue) { + return Promise.reject(new Error("Email addresses do not match")); + } else if (form.getFieldError(otherEmailFieldKey).length > 0) { + form.validateFields([otherEmailFieldKey]); + } + } + + return Promise.resolve(); + } + + return ( + + +

Change Email

+ +
+ + + } + placeholder="Your Password" + /> + + checkEmailsAreMatching(value, ["confirmNewEmail"]), + }, + ]} + > + + } + placeholder="New Email Address" + /> + + checkEmailsAreMatching(value, ["newEmail"]), + }, + ]} + > + + } + placeholder="Confirm New Email Address" + /> + + + + +
+ +
+ ); +} + +export default withRouter(ChangeEmailView); diff --git a/frontend/javascripts/admin/user/user_list_view.tsx b/frontend/javascripts/admin/user/user_list_view.tsx index e617fab0d7f..a91a2d4fd46 100644 --- a/frontend/javascripts/admin/user/user_list_view.tsx +++ b/frontend/javascripts/admin/user/user_list_view.tsx @@ -17,7 +17,7 @@ import { getEditableUsers, updateUser } from "admin/rest_api"; import { renderTeamRolesAndPermissionsForUser } from "admin/team/team_list_view"; import ExperienceModalView from "admin/user/experience_modal_view"; import PermissionsAndTeamsModalView from "admin/user/permissions_and_teams_modal_view"; -import { Alert, App, Button, Col, Input, Modal, Row, Spin, Table, Tag, Tooltip } from "antd"; +import { Alert, App, Button, Col, Input, Row, Spin, Table, Tag, Tooltip } from "antd"; import LinkButton from "components/link_button"; import dayjs from "dayjs"; import Persistence from "libs/persistence"; @@ -25,7 +25,6 @@ import Toast from "libs/toast"; import * as Utils from "libs/utils"; import { location } from "libs/window"; import _ from "lodash"; -import messages from "messages"; import React, { type Key, useEffect, useState } from "react"; import { connect } from "react-redux"; import type { RouteComponentProps } from "react-router-dom"; @@ -34,9 +33,6 @@ import type { APIOrganization, APITeamMembership, APIUser, ExperienceMap } from import { enforceActiveOrganization } from "viewer/model/accessors/organization_accessors"; import { enforceActiveUser } from "viewer/model/accessors/user_accessor"; import type { WebknossosState } from "viewer/store"; -import EditableTextLabel from "viewer/view/components/editable_text_label"; -import { logoutUserAction } from "../../viewer/model/actions/user_actions"; -import Store from "../../viewer/store"; const { Column } = Table; const { Search } = Input; @@ -122,28 +118,6 @@ function UserListView({ activeUser, activeOrganization }: Props) { activateUser(user, false); } - async function changeEmail(selectedUser: APIUser, newEmail: string) { - const newUserPromises = users.map((user) => { - if (selectedUser.id === user.id) { - const newUser = Object.assign({}, user, { - email: newEmail, - }); - return updateUser(newUser); - } - - return Promise.resolve(user); - }); - Promise.all(newUserPromises).then( - (newUsers) => { - setUsers(newUsers); - setSelectedUserIds([selectedUser.id]); - Toast.success(messages["users.change_email_confirmation"]); - if (activeUser.email === selectedUser.email) Store.dispatch(logoutUserAction()); - }, - () => {}, // Do nothing, change did not succeed - ); - } - function handleUsersChange(updatedUsers: Array): void { setUsers(updatedUsers); setIsExperienceModalOpen(false); @@ -423,33 +397,6 @@ function UserListView({ activeUser, activeOrganization }: Props) { key="email" width={320} sorter={Utils.localeCompareBy((user) => user.email)} - render={(__, user: APIUser) => - activeUser.isAdmin ? ( - { - if (newEmail !== user.email) { - Modal.confirm({ - title: messages["users.change_email_title"], - content: messages["users.change_email"]({ - newEmail, - }), - onOk: () => changeEmail(user, newEmail), - }); - } - }} - /> - ) : ( - user.email - ) - } /> Change Password, }, + { key: "changeEmail", label: Change Email }, { key: "token", label: Auth Token }, { key: "theme", diff --git a/frontend/javascripts/router.tsx b/frontend/javascripts/router.tsx index 6176b7aa097..0cd29bb55fd 100644 --- a/frontend/javascripts/router.tsx +++ b/frontend/javascripts/router.tsx @@ -1,5 +1,6 @@ import AcceptInviteView from "admin/auth/accept_invite_view"; import AuthTokenView from "admin/auth/auth_token_view"; +import ChangeEmailView from "admin/auth/change_email_view"; import ChangePasswordView from "admin/auth/change_password_view"; import FinishResetPasswordView from "admin/auth/finish_reset_password_view"; import LoginView from "admin/auth/login_view"; @@ -644,6 +645,11 @@ class ReactRouter extends React.Component { path="/auth/token" component={AuthTokenView} /> + Date: Wed, 4 Jun 2025 11:48:15 +0200 Subject: [PATCH 02/20] send verification email --- conf/application.conf | 4 ++-- frontend/javascripts/admin/auth/change_email_view.tsx | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/conf/application.conf b/conf/application.conf index aac6419d24e..f4bde86b6b3 100644 --- a/conf/application.conf +++ b/conf/application.conf @@ -83,8 +83,8 @@ webKnossos { inviteExpiry = 14 days ssoKey = "" emailVerification { - activated = false - required = false + activated = true + required = true gracePeriod = 7 days # time period in which users do not need to verify their email address linkExpiry = 30 days } diff --git a/frontend/javascripts/admin/auth/change_email_view.tsx b/frontend/javascripts/admin/auth/change_email_view.tsx index 1f2ba3b8744..318f4f51809 100644 --- a/frontend/javascripts/admin/auth/change_email_view.tsx +++ b/frontend/javascripts/admin/auth/change_email_view.tsx @@ -5,6 +5,7 @@ import { useWkSelector } from "libs/react_hooks"; import Request from "libs/request"; import Toast from "libs/toast"; import { type RouteComponentProps, withRouter } from "react-router-dom"; +import { handleResendVerificationEmail } from "./verify_email_view"; const FormItem = Form.Item; function ChangeEmailView() { @@ -22,6 +23,7 @@ function ChangeEmailView() { const newEmail = form.getFieldValue("newEmail"); changeEmail(newEmail) .then(() => { + handleResendVerificationEmail(); Toast.success("Email address changed successfully. You will be logged out."); return Request.receiveJSON("/api/auth/logout"); }) From 3562d51649d258ffb67f5611213731851f792f25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Wed, 4 Jun 2025 13:22:20 +0200 Subject: [PATCH 03/20] enable users to edit their own email address --- app/controllers/UserController.scala | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/app/controllers/UserController.scala b/app/controllers/UserController.scala index 7074b9203ef..91a738ee5ff 100755 --- a/app/controllers/UserController.scala +++ b/app/controllers/UserController.scala @@ -195,16 +195,15 @@ class UserController @Inject()(userService: UserService, Fox.successful(()) }) - private def checkAdminOnlyUpdates(user: User, - isActive: Boolean, - isAdmin: Boolean, - isDatasetManager: Boolean, - oldEmail: String, - email: String)(issuingUser: User): Boolean = - if (isActive && user.isAdmin == isAdmin && oldEmail == email && isDatasetManager == user.isDatasetManager) + private def checkAdminOnlyUpdates(user: User, isActive: Boolean, isAdmin: Boolean, isDatasetManager: Boolean)( + issuingUser: User): Boolean = + if (isActive && user.isAdmin == isAdmin && isDatasetManager == user.isDatasetManager) true else issuingUser.isAdminOf(user) + private def checkAdminOrSelfUpdates(user: User, oldEmail: String, email: String)(issuingUser: User): Boolean = + if (oldEmail == email) true else issuingUser.isAdminOf(user) || issuingUser._id == user._id + private def checkNoSelfDeactivate(user: User, isActive: Boolean)(issuingUser: User): Boolean = issuingUser._id != user._id || isActive || user.isDeactivated @@ -232,6 +231,7 @@ class UserController @Inject()(userService: UserService, count <- userDAO.countIdentitiesForMultiUser(user._multiUser) issuingMultiUser <- multiUserDAO.findOne(issuingUser._multiUser) _ <- Fox.fromBool(count <= 1 || issuingMultiUser.isSuperUser) ?~> "user.email.onlySuperUserCanChange" + // TODOM: @fm3 should we keep this check as now we can have guest users? } yield () private def preventZeroAdmins(user: User, isAdmin: Boolean) = @@ -277,8 +277,9 @@ class UserController @Inject()(userService: UserService, experiences = experiencesOpt.getOrElse(oldExperience) lastTaskTypeId = if (lastTaskTypeIdOpt.isEmpty) user.lastTaskTypeId.map(_.id) else lastTaskTypeIdOpt _ <- Fox.assertTrue(userService.isEditableBy(user, request.identity)) ?~> "notAllowed" ~> FORBIDDEN - _ <- Fox.fromBool(checkAdminOnlyUpdates(user, isActive, isAdmin, isDatasetManager, oldEmail, email)( - issuingUser)) ?~> "notAllowed" ~> FORBIDDEN + _ <- Fox + .fromBool(checkAdminOnlyUpdates(user, isActive, isAdmin, isDatasetManager)(issuingUser)) ?~> "notAllowed" ~> FORBIDDEN + _ <- Fox.fromBool(checkAdminOrSelfUpdates(user, oldEmail, email)(issuingUser)) ?~> "notAllowed" ~> FORBIDDEN _ <- Fox.fromBool(checkNoSelfDeactivate(user, isActive)(issuingUser)) ?~> "user.noSelfDeactivate" ~> FORBIDDEN _ <- checkNoDeactivateWithRemainingTask(user, isActive) _ <- checkNoActivateBeyondLimit(user, isActive) From 07fabe09390a5015c4036c2187b90e3978c33769 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Wed, 4 Jun 2025 13:50:24 +0200 Subject: [PATCH 04/20] allow to edit own email addres for non admins --- app/controllers/UserController.scala | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/app/controllers/UserController.scala b/app/controllers/UserController.scala index 91a738ee5ff..d4b5618daab 100755 --- a/app/controllers/UserController.scala +++ b/app/controllers/UserController.scala @@ -195,6 +195,15 @@ class UserController @Inject()(userService: UserService, Fox.successful(()) }) + private def checkTeamManagerOnlyUpdates(user: User, + experiences: Map[String, Int], + oldExperiences: Map[String, Int], + teams: List[TeamMembership], + oldTeams: List[TeamMembership])(issuingUser: User): Fox[Boolean] = + if (experiences == oldExperiences && teams == oldTeams) + Fox.successful(true) + else userService.isEditableBy(user, issuingUser) + private def checkAdminOnlyUpdates(user: User, isActive: Boolean, isAdmin: Boolean, isDatasetManager: Boolean)( issuingUser: User): Boolean = if (isActive && user.isAdmin == isAdmin && isDatasetManager == user.isDatasetManager) @@ -264,6 +273,7 @@ class UserController @Inject()(userService: UserService, lastTaskTypeIdOpt) => for { user <- userDAO.findOne(userId) ?~> "user.notFound" ~> NOT_FOUND + // properties for team managers dataset manager and admins only: experiences, teams oldExperience <- userService.experiencesFor(user._id) oldAssignedMemberships <- userService.teamMembershipsFor(user._id) firstName = firstNameOpt.getOrElse(user.firstName) @@ -276,7 +286,13 @@ class UserController @Inject()(userService: UserService, assignedMemberships = assignedMembershipsOpt.getOrElse(oldAssignedMemberships) experiences = experiencesOpt.getOrElse(oldExperience) lastTaskTypeId = if (lastTaskTypeIdOpt.isEmpty) user.lastTaskTypeId.map(_.id) else lastTaskTypeIdOpt - _ <- Fox.assertTrue(userService.isEditableBy(user, request.identity)) ?~> "notAllowed" ~> FORBIDDEN + _ <- Fox + .runIf(user._id != issuingUser._id)(Fox.assertTrue(userService.isEditableBy(user, request.identity))) ?~> "notAllowed" ~> FORBIDDEN + _ <- checkTeamManagerOnlyUpdates(user, + experiences, + oldExperience, + assignedMemberships, + oldAssignedMemberships)(issuingUser) ?~> "notAllowed" ~> FORBIDDEN _ <- Fox .fromBool(checkAdminOnlyUpdates(user, isActive, isAdmin, isDatasetManager)(issuingUser)) ?~> "notAllowed" ~> FORBIDDEN _ <- Fox.fromBool(checkAdminOrSelfUpdates(user, oldEmail, email)(issuingUser)) ?~> "notAllowed" ~> FORBIDDEN From 209bf8b90cc5056911640e7e1954a63fefad28fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Wed, 4 Jun 2025 17:21:18 +0200 Subject: [PATCH 05/20] add specific column tracking when the last email change was for more accurate email verification grace period --- .../user/EmailVerificationService.scala | 2 +- app/models/user/MultiUser.scala | 5 +++- .../135-extra-column-for-email-changed.sql | 24 +++++++++++++++++++ .../135-extra-column-for-email-changed.sql | 23 ++++++++++++++++++ tools/postgres/schema.sql | 3 ++- 5 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 conf/evolutions/135-extra-column-for-email-changed.sql create mode 100644 conf/evolutions/reversions/135-extra-column-for-email-changed.sql diff --git a/app/models/user/EmailVerificationService.scala b/app/models/user/EmailVerificationService.scala index c15f965ba5f..5e5c839a43c 100644 --- a/app/models/user/EmailVerificationService.scala +++ b/app/models/user/EmailVerificationService.scala @@ -78,7 +78,7 @@ class EmailVerificationService @Inject()(conf: WkConf, ): Fox[Boolean] = for { multiUser: MultiUser <- multiUserDAO.findOne(user._multiUser) ?~> "user.notFound" - endOfGracePeriod: Instant = multiUser.created + conf.WebKnossos.User.EmailVerification.gracePeriod + endOfGracePeriod: Instant = multiUser.emailChangeDate + conf.WebKnossos.User.EmailVerification.gracePeriod overGracePeriod = endOfGracePeriod.isPast } yield !conf.WebKnossos.User.EmailVerification.required || multiUser.isEmailVerified || !overGracePeriod } diff --git a/app/models/user/MultiUser.scala b/app/models/user/MultiUser.scala index 0eb731d16d3..9c7fcdc9798 100644 --- a/app/models/user/MultiUser.scala +++ b/app/models/user/MultiUser.scala @@ -26,6 +26,7 @@ case class MultiUser( selectedTheme: Theme = Theme.auto, created: Instant = Instant.now, isEmailVerified: Boolean = false, + emailChangeDate: Instant = Instant.now, isDeleted: Boolean = false ) @@ -57,6 +58,7 @@ class MultiUserDAO @Inject()(sqlClient: SqlClient)(implicit ec: ExecutionContext theme, Instant.fromSql(r.created), r.isemailverified, + Instant.fromSql(r.emailchangedate), r.isdeleted ) } @@ -91,7 +93,8 @@ class MultiUserDAO @Inject()(sqlClient: SqlClient)(implicit ec: ExecutionContext _ <- run(q"""UPDATE webknossos.multiusers SET email = $email, - isEmailVerified = false + isEmailVerified = false, + emailChangeDate = NOW() WHERE _id = $multiUserId""".asUpdate) } yield () diff --git a/conf/evolutions/135-extra-column-for-email-changed.sql b/conf/evolutions/135-extra-column-for-email-changed.sql new file mode 100644 index 00000000000..ded6a5fa594 --- /dev/null +++ b/conf/evolutions/135-extra-column-for-email-changed.sql @@ -0,0 +1,24 @@ +START TRANSACTION; + +do $$ begin ASSERT (select schemaVersion from webknossos.releaseInformation) = 134, 'Previous schema version mismatch'; end; $$ LANGUAGE plpgsql; + +DROP VIEW IF EXISTS webknossos.userInfos; +DROP VIEW IF EXISTS webknossos.multiUsers_; + +ALTER TABLE webknossos.multiUsers ADD COLUMN emailChangeDate TIMESTAMPTZ NOT NULL DEFAULT NOW(); +UPDATE webknossos.multiUsers SET emailChangeDate = created; + +CREATE VIEW webknossos.multiUsers_ AS SELECT * FROM webknossos.multiUsers WHERE NOT isDeleted; +CREATE VIEW webknossos.userInfos AS +SELECT +u._id AS _user, m.email, u.firstName, u.lastname, o.name AS organization_name, +u.isDeactivated, u.isDatasetManager, u.isAdmin, m.isSuperUser, +u._organization, o._id AS organization_id, u.created AS user_created, +m.created AS multiuser_created, u._multiUser, m._lastLoggedInIdentity, u.lastActivity, m.isEmailVerified +FROM webknossos.users_ u +JOIN webknossos.organizations_ o ON u._organization = o._id +JOIN webknossos.multiUsers_ m on u._multiUser = m._id; + +UPDATE webknossos.releaseInformation SET schemaVersion = 135; + +COMMIT TRANSACTION; diff --git a/conf/evolutions/reversions/135-extra-column-for-email-changed.sql b/conf/evolutions/reversions/135-extra-column-for-email-changed.sql new file mode 100644 index 00000000000..7f9217d9e64 --- /dev/null +++ b/conf/evolutions/reversions/135-extra-column-for-email-changed.sql @@ -0,0 +1,23 @@ +START TRANSACTION; + +do $$ begin ASSERT (select schemaVersion from webknossos.releaseInformation) = 135, 'Previous schema version mismatch'; end; $$ LANGUAGE plpgsql; + +DROP VIEW IF EXISTS webknossos.userInfos; +DROP VIEW IF EXISTS webknossos.multiUsers_; + +ALTER TABLE webknossos.multiUsers DROP COLUMN emailChangeDate; + +CREATE VIEW webknossos.multiUsers_ AS SELECT * FROM webknossos.multiUsers WHERE NOT isDeleted; +CREATE VIEW webknossos.userInfos AS +SELECT +u._id AS _user, m.email, u.firstName, u.lastname, o.name AS organization_name, +u.isDeactivated, u.isDatasetManager, u.isAdmin, m.isSuperUser, +u._organization, o._id AS organization_id, u.created AS user_created, +m.created AS multiuser_created, u._multiUser, m._lastLoggedInIdentity, u.lastActivity, m.isEmailVerified +FROM webknossos.users_ u +JOIN webknossos.organizations_ o ON u._organization = o._id +JOIN webknossos.multiUsers_ m on u._multiUser = m._id; + +UPDATE webknossos.releaseInformation SET schemaVersion = 134; + +COMMIT TRANSACTION; diff --git a/tools/postgres/schema.sql b/tools/postgres/schema.sql index 267488b6ca6..15ea810ce8a 100644 --- a/tools/postgres/schema.sql +++ b/tools/postgres/schema.sql @@ -21,7 +21,7 @@ CREATE TABLE webknossos.releaseInformation ( schemaVersion BIGINT NOT NULL ); -INSERT INTO webknossos.releaseInformation(schemaVersion) values(134); +INSERT INTO webknossos.releaseInformation(schemaVersion) values(135); COMMIT TRANSACTION; @@ -459,6 +459,7 @@ CREATE TABLE webknossos.multiUsers( selectedTheme webknossos.THEME NOT NULL DEFAULT 'auto', _lastLoggedInIdentity TEXT CONSTRAINT _lastLoggedInIdentity_objectId CHECK (_lastLoggedInIdentity ~ '^[0-9a-f]{24}$') DEFAULT NULL, isEmailVerified BOOLEAN NOT NULL DEFAULT FALSE, + emailChangeDate TIMESTAMPTZ NOT NULL DEFAULT NOW(), isDeleted BOOLEAN NOT NULL DEFAULT FALSE, CONSTRAINT nuxInfoIsJsonObject CHECK(jsonb_typeof(novelUserExperienceInfos) = 'object') ); From 58bcc29aafe515d10723464ce5ac3a9c0afa034a Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Fri, 6 Jun 2025 09:41:41 +0200 Subject: [PATCH 06/20] clean up frontend code --- .../admin/auth/change_email_view.tsx | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/frontend/javascripts/admin/auth/change_email_view.tsx b/frontend/javascripts/admin/auth/change_email_view.tsx index 318f4f51809..30e24648dca 100644 --- a/frontend/javascripts/admin/auth/change_email_view.tsx +++ b/frontend/javascripts/admin/auth/change_email_view.tsx @@ -5,9 +5,16 @@ import { useWkSelector } from "libs/react_hooks"; import Request from "libs/request"; import Toast from "libs/toast"; import { type RouteComponentProps, withRouter } from "react-router-dom"; +import { logoutUserAction } from "viewer/model/actions/user_actions"; +import { Store } from "viewer/singletons"; import { handleResendVerificationEmail } from "./verify_email_view"; + const FormItem = Form.Item; +const NEW_EMAIL_FIELD_KEY = "newEmail"; +const CONFIRM_NEW_EMAIL_FIELD_KEY = "confirmNewEmail"; +const PASSWORD_FIELD_KEY = "password"; + function ChangeEmailView() { const [form] = Form.useForm(); const activeUser = useWkSelector((state) => state.activeUser); @@ -20,17 +27,14 @@ function ChangeEmailView() { } function onFinish() { - const newEmail = form.getFieldValue("newEmail"); + const newEmail = form.getFieldValue(NEW_EMAIL_FIELD_KEY); changeEmail(newEmail) - .then(() => { + .then(async () => { handleResendVerificationEmail(); Toast.success("Email address changed successfully. You will be logged out."); - return Request.receiveJSON("/api/auth/logout"); - }) - .then(() => { - form.resetFields(); - // Redirect to login page after successful email change + await Request.receiveJSON("/api/auth/logout"); window.location.href = "/auth/login"; + Store.dispatch(logoutUserAction()); }) .catch((error) => { Toast.error( @@ -73,7 +77,7 @@ function ChangeEmailView() { />
checkEmailsAreMatching(value, ["confirmNewEmail"]), + validator: (_, value: string) => + checkEmailsAreMatching(value, [CONFIRM_NEW_EMAIL_FIELD_KEY]), }, ]} > @@ -122,7 +127,7 @@ function ChangeEmailView() { checkEmailsAreMatching(value, ["newEmail"]), + validator: (_, value: string) => + checkEmailsAreMatching(value, [NEW_EMAIL_FIELD_KEY]), }, ]} > From 74fe1f29adcf4edf2c667ea885bcdc5204d72c66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Sat, 14 Jun 2025 11:29:17 +0200 Subject: [PATCH 07/20] add password verification upon email update --- app/controllers/UserController.scala | 50 +++++++++++++++++-- .../admin/auth/change_email_view.tsx | 12 +++-- 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/app/controllers/UserController.scala b/app/controllers/UserController.scala index d4b5618daab..39163693616 100755 --- a/app/controllers/UserController.scala +++ b/app/controllers/UserController.scala @@ -3,7 +3,6 @@ package controllers import play.silhouette.api.Silhouette import com.scalableminds.util.accesscontext.{DBAccessContext, GlobalAccessContext} import com.scalableminds.util.tools.{Fox, FoxImplicits} - import models.annotation.{AnnotationDAO, AnnotationService, AnnotationType} import models.organization.OrganizationService import models.team._ @@ -16,6 +15,10 @@ import com.scalableminds.util.objectid.ObjectId import javax.inject.Inject import models.user.Theme.Theme +import net.liftweb.common.{Box, Failure, Full} +import play.silhouette.api.exceptions.ProviderException +import play.silhouette.api.util.Credentials +import play.silhouette.impl.providers.CredentialsProvider import security.WkEnv import scala.concurrent.ExecutionContext @@ -23,6 +26,7 @@ import scala.concurrent.ExecutionContext class UserController @Inject()(userService: UserService, userDAO: UserDAO, multiUserDAO: MultiUserDAO, + credentialsProvider: CredentialsProvider, organizationService: OrganizationService, annotationDAO: AnnotationDAO, teamMembershipService: TeamMembershipService, @@ -175,6 +179,7 @@ class UserController @Inject()(userService: UserService, ((__ \ "firstName").readNullable[String] and (__ \ "lastName").readNullable[String] and (__ \ "email").readNullable[String] and + (__ \ "password").readNullable[String] and (__ \ "isActive").readNullable[Boolean] and (__ \ "isAdmin").readNullable[Boolean] and (__ \ "isDatasetManager").readNullable[Boolean] and @@ -204,15 +209,49 @@ class UserController @Inject()(userService: UserService, Fox.successful(true) else userService.isEditableBy(user, issuingUser) + private def checkPasswordIfEmailChangedByNonAdmin( + user: User, + passwordOpt: Option[String], + oldEmail: String, + email: String)(issuingUser: User)(implicit m: MessagesProvider): Fox[Unit] = { + + val isAdminAndNotSameUser = issuingUser.isAdminOf(user) && user._id != issuingUser._id + + if (oldEmail == email || isAdminAndNotSameUser) { + Fox.successful(()) + } else if (user._id == issuingUser._id) { + passwordOpt match { + case Some(password) => + val credentials = Credentials(user._id.id, password) + Fox.fromFutureBox( + credentialsProvider + .authenticate(credentials) + .flatMap { loginInfo => + userService.retrieve(loginInfo).map { + case Some(user) => { + println("found user", user) + Full(()) + } + case None => Failure(Messages("error.noUser")) + } + } + .recover { + case _: ProviderException => + Failure(Messages("error.invalidCredentials")) + }) + case None => Fox.failure(Messages("error.passwordsDontMatch")) + } + } else { + Fox.failure(Messages("notAllowed")) + } + } + private def checkAdminOnlyUpdates(user: User, isActive: Boolean, isAdmin: Boolean, isDatasetManager: Boolean)( issuingUser: User): Boolean = if (isActive && user.isAdmin == isAdmin && isDatasetManager == user.isDatasetManager) true else issuingUser.isAdminOf(user) - private def checkAdminOrSelfUpdates(user: User, oldEmail: String, email: String)(issuingUser: User): Boolean = - if (oldEmail == email) true else issuingUser.isAdminOf(user) || issuingUser._id == user._id - private def checkNoSelfDeactivate(user: User, isActive: Boolean)(issuingUser: User): Boolean = issuingUser._id != user._id || isActive || user.isDeactivated @@ -265,6 +304,7 @@ class UserController @Inject()(userService: UserService, case (firstNameOpt, lastNameOpt, emailOpt, + passwordOpt, isActiveOpt, isAdminOpt, isDatasetManagerOpt, @@ -295,7 +335,7 @@ class UserController @Inject()(userService: UserService, oldAssignedMemberships)(issuingUser) ?~> "notAllowed" ~> FORBIDDEN _ <- Fox .fromBool(checkAdminOnlyUpdates(user, isActive, isAdmin, isDatasetManager)(issuingUser)) ?~> "notAllowed" ~> FORBIDDEN - _ <- Fox.fromBool(checkAdminOrSelfUpdates(user, oldEmail, email)(issuingUser)) ?~> "notAllowed" ~> FORBIDDEN + _ <- checkPasswordIfEmailChangedByNonAdmin(user, passwordOpt, oldEmail, email)(issuingUser) _ <- Fox.fromBool(checkNoSelfDeactivate(user, isActive)(issuingUser)) ?~> "user.noSelfDeactivate" ~> FORBIDDEN _ <- checkNoDeactivateWithRemainingTask(user, isActive) _ <- checkNoActivateBeyondLimit(user, isActive) diff --git a/frontend/javascripts/admin/auth/change_email_view.tsx b/frontend/javascripts/admin/auth/change_email_view.tsx index 318f4f51809..934eac0c365 100644 --- a/frontend/javascripts/admin/auth/change_email_view.tsx +++ b/frontend/javascripts/admin/auth/change_email_view.tsx @@ -12,16 +12,18 @@ function ChangeEmailView() { const [form] = Form.useForm(); const activeUser = useWkSelector((state) => state.activeUser); - async function changeEmail(newEmail: string) { + async function changeEmail(newEmail: string, password: string) { const newUser = Object.assign({}, activeUser, { email: newEmail, + password, }); return updateUser(newUser); } function onFinish() { const newEmail = form.getFieldValue("newEmail"); - changeEmail(newEmail) + const password = form.getFieldValue("password"); + changeEmail(newEmail, password) .then(() => { handleResendVerificationEmail(); Toast.success("Email address changed successfully. You will be logged out."); @@ -33,9 +35,9 @@ function ChangeEmailView() { window.location.href = "/auth/login"; }) .catch((error) => { - Toast.error( - "An unexpected error occurred while changing the email address: " + error.message, - ); + const errorMsg = "An unexpected error occurred while changing the email address."; + Toast.error(errorMsg); + console.error(errorMsg, error); }); } From 04151f4a5f6587e27506a540a7c146bda232cf22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Wed, 18 Jun 2025 19:03:04 +0200 Subject: [PATCH 08/20] disallow admins updating emails of other users --- app/controllers/UserController.scala | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/app/controllers/UserController.scala b/app/controllers/UserController.scala index 39163693616..23df14fea08 100755 --- a/app/controllers/UserController.scala +++ b/app/controllers/UserController.scala @@ -209,15 +209,12 @@ class UserController @Inject()(userService: UserService, Fox.successful(true) else userService.isEditableBy(user, issuingUser) - private def checkPasswordIfEmailChangedByNonAdmin( + private def checkPasswordIfEmailChanged( user: User, passwordOpt: Option[String], oldEmail: String, email: String)(issuingUser: User)(implicit m: MessagesProvider): Fox[Unit] = { - - val isAdminAndNotSameUser = issuingUser.isAdminOf(user) && user._id != issuingUser._id - - if (oldEmail == email || isAdminAndNotSameUser) { + if (oldEmail == email) { Fox.successful(()) } else if (user._id == issuingUser._id) { passwordOpt match { @@ -335,7 +332,7 @@ class UserController @Inject()(userService: UserService, oldAssignedMemberships)(issuingUser) ?~> "notAllowed" ~> FORBIDDEN _ <- Fox .fromBool(checkAdminOnlyUpdates(user, isActive, isAdmin, isDatasetManager)(issuingUser)) ?~> "notAllowed" ~> FORBIDDEN - _ <- checkPasswordIfEmailChangedByNonAdmin(user, passwordOpt, oldEmail, email)(issuingUser) + _ <- checkPasswordIfEmailChanged(user, passwordOpt, oldEmail, email)(issuingUser) _ <- Fox.fromBool(checkNoSelfDeactivate(user, isActive)(issuingUser)) ?~> "user.noSelfDeactivate" ~> FORBIDDEN _ <- checkNoDeactivateWithRemainingTask(user, isActive) _ <- checkNoActivateBeyondLimit(user, isActive) From 8d7ed7b79aa000d19c98a3e8ca9172719bebbab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Wed, 18 Jun 2025 19:16:06 +0200 Subject: [PATCH 09/20] add entry to MIGRATIONS.unreleased.md --- MIGRATIONS.unreleased.md | 1 + 1 file changed, 1 insertion(+) diff --git a/MIGRATIONS.unreleased.md b/MIGRATIONS.unreleased.md index 6b99ddd532b..9c207f903c5 100644 --- a/MIGRATIONS.unreleased.md +++ b/MIGRATIONS.unreleased.md @@ -10,3 +10,4 @@ User-facing changes are documented in the [changelog](CHANGELOG.released.md). ### Postgres Evolutions: - [134-dataset-layer-attachments.sql](conf/evolutions/134-dataset-layer-attachments.sql) +- [135-extra-column-for-email-changed.sql](conf/evolutions/135-extra-column-for-email-changed.sql) From 8be7adb51224892e2cf2e74fe71d66714b173e51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Wed, 18 Jun 2025 19:18:17 +0200 Subject: [PATCH 10/20] format backend --- app/controllers/UserController.scala | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/controllers/UserController.scala b/app/controllers/UserController.scala index 23df14fea08..92962d02c0c 100755 --- a/app/controllers/UserController.scala +++ b/app/controllers/UserController.scala @@ -209,11 +209,8 @@ class UserController @Inject()(userService: UserService, Fox.successful(true) else userService.isEditableBy(user, issuingUser) - private def checkPasswordIfEmailChanged( - user: User, - passwordOpt: Option[String], - oldEmail: String, - email: String)(issuingUser: User)(implicit m: MessagesProvider): Fox[Unit] = { + private def checkPasswordIfEmailChanged(user: User, passwordOpt: Option[String], oldEmail: String, email: String)( + issuingUser: User)(implicit m: MessagesProvider): Fox[Unit] = if (oldEmail == email) { Fox.successful(()) } else if (user._id == issuingUser._id) { @@ -241,7 +238,6 @@ class UserController @Inject()(userService: UserService, } else { Fox.failure(Messages("notAllowed")) } - } private def checkAdminOnlyUpdates(user: User, isActive: Boolean, isAdmin: Boolean, isDatasetManager: Boolean)( issuingUser: User): Boolean = From 9ec548d659d3b444ab520d0c1bbb39dcb732c6ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Wed, 18 Jun 2025 21:56:09 +0200 Subject: [PATCH 11/20] remove logging used for testing --- app/controllers/UserController.scala | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/controllers/UserController.scala b/app/controllers/UserController.scala index 92962d02c0c..904bf609978 100755 --- a/app/controllers/UserController.scala +++ b/app/controllers/UserController.scala @@ -222,11 +222,8 @@ class UserController @Inject()(userService: UserService, .authenticate(credentials) .flatMap { loginInfo => userService.retrieve(loginInfo).map { - case Some(user) => { - println("found user", user) - Full(()) - } - case None => Failure(Messages("error.noUser")) + case Some(user) => Full(()) + case None => Failure(Messages("error.noUser")) } } .recover { From 2a1166d8b0970980ad5b5d07ea65472b661d5d08 Mon Sep 17 00:00:00 2001 From: Florian M Date: Thu, 19 Jun 2025 11:47:47 +0200 Subject: [PATCH 12/20] unused import --- app/controllers/UserController.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/UserController.scala b/app/controllers/UserController.scala index 904bf609978..77fab076666 100755 --- a/app/controllers/UserController.scala +++ b/app/controllers/UserController.scala @@ -15,7 +15,7 @@ import com.scalableminds.util.objectid.ObjectId import javax.inject.Inject import models.user.Theme.Theme -import net.liftweb.common.{Box, Failure, Full} +import net.liftweb.common.{Failure, Full} import play.silhouette.api.exceptions.ProviderException import play.silhouette.api.util.Credentials import play.silhouette.impl.providers.CredentialsProvider From 7ff58c02e95379816b71dcc7c5e765e6d19ab5c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Mon, 23 Jun 2025 12:22:17 +0200 Subject: [PATCH 13/20] add missing emailChangeDate column to multi user e2e test csv data --- test/db/multiusers.csv | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/db/multiusers.csv b/test/db/multiusers.csv index ca766a02423..f6dd42f3ebe 100644 --- a/test/db/multiusers.csv +++ b/test/db/multiusers.csv @@ -1,7 +1,7 @@ -_id,email,passwordInfo_hasher,passwordInfo_password,isSuperUser,novelUserExperienceInfos,created,selectedTheme,_lastLoggedInIdentity,isEmailVerified,isDeleted -'8fb0c6a674d0af7b003b23ea','user_A@scalableminds.com','SCrypt','$2a$10$6xBhZRx30Hzb8ZK1ygpslekVHsWXKLbLfxPz.U1yM0r5lMR06ELeW',t,'{}','2016-04-11T12:57:49.000Z','auto',,t,f -'8fb0c6a674d0af7b003b23eb','user_B@scalableminds.com','SCrypt','$2a$10$6xBhZRx30Hzb8ZK1ygpslekVHsWXKLbLfxPz.U1yM0r5lMR06ELeW',t,'{}','2016-04-11T12:57:49.000Z','auto',,t,f -'8fb0c6a674d0af7b003b23ec','user_C@scalableminds.com','SCrypt','$2a$10$6xBhZRx30Hzb8ZK1ygpslekVHsWXKLbLfxPz.U1yM0r5lMR06ELeW',t,'{}','2016-04-11T12:57:49.000Z','auto',,t,f -'8fb0c6a674d0af7b003b23ed','user_D@scalableminds.com','SCrypt','$2a$10$6xBhZRx30Hzb8ZK1ygpslekVHsWXKLbLfxPz.U1yM0r5lMR06ELeW',t,'{}','2016-04-11T12:57:49.000Z','light',,t,f -'8fb0c6a674d0af7b003b23ee','user_E@scalableminds.com','SCrypt','$2a$10$6xBhZRx30Hzb8ZK1ygpslekVHsWXKLbLfxPz.U1yM0r5lMR06ELeW',t,'{}','2016-04-11T12:57:49.000Z','dark',,t,f -'8fb0c6a674d0af7b003b23ef','user_F@scalableminds.com','SCrypt','$2a$10$6xBhZRx30Hzb8ZK1ygpslekVHsWXKLbLfxPz.U1yM0r5lMR06ELeW',t,'{}','2016-04-11T12:57:49.000Z','dark',,t,f +_id,email,passwordInfo_hasher,passwordInfo_password,isSuperUser,novelUserExperienceInfos,created,selectedTheme,_lastLoggedInIdentity,isEmailVerified,emailChangeDate,isDeleted +'8fb0c6a674d0af7b003b23ea','user_A@scalableminds.com','SCrypt','$2a$10$6xBhZRx30Hzb8ZK1ygpslekVHsWXKLbLfxPz.U1yM0r5lMR06ELeW',t,'{}','2016-04-11T12:57:49.000Z','auto',,t,2016-04-11T12:57:49.000Z,f +'8fb0c6a674d0af7b003b23eb','user_B@scalableminds.com','SCrypt','$2a$10$6xBhZRx30Hzb8ZK1ygpslekVHsWXKLbLfxPz.U1yM0r5lMR06ELeW',t,'{}','2016-04-11T12:57:49.000Z','auto',,t,2016-04-11T12:57:49.000Z,f +'8fb0c6a674d0af7b003b23ec','user_C@scalableminds.com','SCrypt','$2a$10$6xBhZRx30Hzb8ZK1ygpslekVHsWXKLbLfxPz.U1yM0r5lMR06ELeW',t,'{}','2016-04-11T12:57:49.000Z','auto',,t,2016-04-11T12:57:49.000Z,f +'8fb0c6a674d0af7b003b23ed','user_D@scalableminds.com','SCrypt','$2a$10$6xBhZRx30Hzb8ZK1ygpslekVHsWXKLbLfxPz.U1yM0r5lMR06ELeW',t,'{}','2016-04-11T12:57:49.000Z','light',,t,2016-04-11T12:57:49.000Z,f +'8fb0c6a674d0af7b003b23ee','user_E@scalableminds.com','SCrypt','$2a$10$6xBhZRx30Hzb8ZK1ygpslekVHsWXKLbLfxPz.U1yM0r5lMR06ELeW',t,'{}','2016-04-11T12:57:49.000Z','dark',,t,2016-04-11T12:57:49.000Z,f +'8fb0c6a674d0af7b003b23ef','user_F@scalableminds.com','SCrypt','$2a$10$6xBhZRx30Hzb8ZK1ygpslekVHsWXKLbLfxPz.U1yM0r5lMR06ELeW',t,'{}','2016-04-11T12:57:49.000Z','dark',,t,2016-04-11T12:57:49.000Z,f From c45795f14dfdfb7cce7a38a73c10ba7ab3177c35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Mon, 23 Jun 2025 12:23:17 +0200 Subject: [PATCH 14/20] apply pr feedback --- app/controllers/UserController.scala | 26 +++++++------------------- app/models/user/UserService.scala | 5 ++++- conf/messages | 1 + 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/app/controllers/UserController.scala b/app/controllers/UserController.scala index 904bf609978..a6a001abd9a 100755 --- a/app/controllers/UserController.scala +++ b/app/controllers/UserController.scala @@ -210,7 +210,7 @@ class UserController @Inject()(userService: UserService, else userService.isEditableBy(user, issuingUser) private def checkPasswordIfEmailChanged(user: User, passwordOpt: Option[String], oldEmail: String, email: String)( - issuingUser: User)(implicit m: MessagesProvider): Fox[Unit] = + issuingUser: User): Fox[Unit] = if (oldEmail == email) { Fox.successful(()) } else if (user._id == issuingUser._id) { @@ -222,18 +222,18 @@ class UserController @Inject()(userService: UserService, .authenticate(credentials) .flatMap { loginInfo => userService.retrieve(loginInfo).map { - case Some(user) => Full(()) - case None => Failure(Messages("error.noUser")) + case Some(_) => Full(()) + case None => Failure("error.noUser") } } .recover { case _: ProviderException => - Failure(Messages("error.invalidCredentials")) + Failure("user.email.change.passwordWrong") }) - case None => Fox.failure(Messages("error.passwordsDontMatch")) + case None => Fox.failure("user.email.change.passwordWrong") } } else { - Fox.failure(Messages("notAllowed")) + Fox.failure("notAllowed") } private def checkAdminOnlyUpdates(user: User, isActive: Boolean, isAdmin: Boolean, isDatasetManager: Boolean)( @@ -261,17 +261,6 @@ class UserController @Inject()(userService: UserService, } yield () } else Fox.successful(()) - private def checkSuperUserOnlyUpdates(user: User, oldEmail: String, email: String)(issuingUser: User)( - implicit ctx: DBAccessContext): Fox[Unit] = - if (oldEmail == email) Fox.successful(()) - else - for { - count <- userDAO.countIdentitiesForMultiUser(user._multiUser) - issuingMultiUser <- multiUserDAO.findOne(issuingUser._multiUser) - _ <- Fox.fromBool(count <= 1 || issuingMultiUser.isSuperUser) ?~> "user.email.onlySuperUserCanChange" - // TODOM: @fm3 should we keep this check as now we can have guest users? - } yield () - private def preventZeroAdmins(user: User, isAdmin: Boolean) = if (user.isAdmin && !isAdmin) { for { @@ -303,7 +292,7 @@ class UserController @Inject()(userService: UserService, lastTaskTypeIdOpt) => for { user <- userDAO.findOne(userId) ?~> "user.notFound" ~> NOT_FOUND - // properties for team managers dataset manager and admins only: experiences, teams + // properties that can be changed by team managers and admins only: experiences, team memberships oldExperience <- userService.experiencesFor(user._id) oldAssignedMemberships <- userService.teamMembershipsFor(user._id) firstName = firstNameOpt.getOrElse(user.firstName) @@ -329,7 +318,6 @@ class UserController @Inject()(userService: UserService, _ <- Fox.fromBool(checkNoSelfDeactivate(user, isActive)(issuingUser)) ?~> "user.noSelfDeactivate" ~> FORBIDDEN _ <- checkNoDeactivateWithRemainingTask(user, isActive) _ <- checkNoActivateBeyondLimit(user, isActive) - _ <- checkSuperUserOnlyUpdates(user, oldEmail, email)(issuingUser) _ <- preventZeroAdmins(user, isAdmin) _ <- preventZeroOwners(user, isActive) teams <- Fox.combined(assignedMemberships.map(t => diff --git a/app/models/user/UserService.scala b/app/models/user/UserService.scala index 28589717945..16007c31575 100755 --- a/app/models/user/UserService.scala +++ b/app/models/user/UserService.scala @@ -198,7 +198,10 @@ class UserService @Inject()(conf: WkConf, } for { oldEmail <- emailFor(user) - _ <- Fox.runIf(oldEmail != email)(multiUserDAO.updateEmail(user._multiUser, email)) + _ <- Fox.runIf(oldEmail != email)(for { + _ <- multiUserDAO.updateEmail(user._multiUser, email) + _ = logger.info(s"Email of MultiUser changed from $oldEmail to $email.") + } yield ()) _ <- userDAO.updateValues(user._id, firstName, lastName, diff --git a/conf/messages b/conf/messages index b7aab81dbd6..9040099b0ab 100644 --- a/conf/messages +++ b/conf/messages @@ -59,6 +59,7 @@ user.email.verification.keyInvalid=Verification key is invalid. user.email.verification.keyUsed=Verification key has already been used. user.email.verification.emailDoesNotMatch=This verification key is associated with a different email address. user.email.verification.linkExpired=The email verification link is expired. +user.email.change.passwordWrong=The password you entered is incorrect. user.firstName.invalid=Please check your first name for any special characters user.lastName.invalid=Please check your last name for any special characters user.configuration.invalid=Could not parse configuration From c9731b25269560e5d0aba15aee785f291f4f9410 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Tue, 24 Jun 2025 14:02:38 +0200 Subject: [PATCH 15/20] include multiuserid in in logging upon email changed by user --- app/models/user/UserService.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/user/UserService.scala b/app/models/user/UserService.scala index 16007c31575..d148dec0ec2 100755 --- a/app/models/user/UserService.scala +++ b/app/models/user/UserService.scala @@ -200,7 +200,7 @@ class UserService @Inject()(conf: WkConf, oldEmail <- emailFor(user) _ <- Fox.runIf(oldEmail != email)(for { _ <- multiUserDAO.updateEmail(user._multiUser, email) - _ = logger.info(s"Email of MultiUser changed from $oldEmail to $email.") + _ = logger.info(s"Email of MultiUser ${user._multiUser} changed from $oldEmail to $email") } yield ()) _ <- userDAO.updateValues(user._id, firstName, From 44772871d90f3ff0a9fee68aa897715aa6b6d61a Mon Sep 17 00:00:00 2001 From: MichaelBuessemeyer <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Tue, 24 Jun 2025 14:04:23 +0200 Subject: [PATCH 16/20] Update unreleased_changes/8671.md Co-authored-by: Daniel --- unreleased_changes/8671.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unreleased_changes/8671.md b/unreleased_changes/8671.md index e218e4efe09..b7776ecf62c 100644 --- a/unreleased_changes/8671.md +++ b/unreleased_changes/8671.md @@ -2,7 +2,7 @@ - A user can now update the email address of their account by themselves. ### Removed -- A administator of the organization can no longer update a users email address. This has to be done by the user themselves. +- An administrator of the organization can no longer update a users email address. This has to be done by the user themselves. ### Postgres Evolutions - [135-extra-column-for-email-changed.sql](conf/evolutions/135-extra-column-for-email-changed.sql) From ddc9f573092390bed177dfcf251d99bf6465d9a6 Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Tue, 15 Jul 2025 12:32:02 +0200 Subject: [PATCH 17/20] update migration number --- ...ail-changed.sql => 136-extra-column-for-email-changed.sql} | 4 ++-- ...ail-changed.sql => 136-extra-column-for-email-changed.sql} | 4 ++-- tools/postgres/schema.sql | 2 +- unreleased_changes/8671.md | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) rename conf/evolutions/{135-extra-column-for-email-changed.sql => 136-extra-column-for-email-changed.sql} (88%) rename conf/evolutions/reversions/{135-extra-column-for-email-changed.sql => 136-extra-column-for-email-changed.sql} (87%) diff --git a/conf/evolutions/135-extra-column-for-email-changed.sql b/conf/evolutions/136-extra-column-for-email-changed.sql similarity index 88% rename from conf/evolutions/135-extra-column-for-email-changed.sql rename to conf/evolutions/136-extra-column-for-email-changed.sql index ded6a5fa594..47a45697b15 100644 --- a/conf/evolutions/135-extra-column-for-email-changed.sql +++ b/conf/evolutions/136-extra-column-for-email-changed.sql @@ -1,6 +1,6 @@ START TRANSACTION; -do $$ begin ASSERT (select schemaVersion from webknossos.releaseInformation) = 134, 'Previous schema version mismatch'; end; $$ LANGUAGE plpgsql; +do $$ begin ASSERT (select schemaVersion from webknossos.releaseInformation) = 135, 'Previous schema version mismatch'; end; $$ LANGUAGE plpgsql; DROP VIEW IF EXISTS webknossos.userInfos; DROP VIEW IF EXISTS webknossos.multiUsers_; @@ -19,6 +19,6 @@ FROM webknossos.users_ u JOIN webknossos.organizations_ o ON u._organization = o._id JOIN webknossos.multiUsers_ m on u._multiUser = m._id; -UPDATE webknossos.releaseInformation SET schemaVersion = 135; +UPDATE webknossos.releaseInformation SET schemaVersion = 136; COMMIT TRANSACTION; diff --git a/conf/evolutions/reversions/135-extra-column-for-email-changed.sql b/conf/evolutions/reversions/136-extra-column-for-email-changed.sql similarity index 87% rename from conf/evolutions/reversions/135-extra-column-for-email-changed.sql rename to conf/evolutions/reversions/136-extra-column-for-email-changed.sql index 7f9217d9e64..234c206f818 100644 --- a/conf/evolutions/reversions/135-extra-column-for-email-changed.sql +++ b/conf/evolutions/reversions/136-extra-column-for-email-changed.sql @@ -1,6 +1,6 @@ START TRANSACTION; -do $$ begin ASSERT (select schemaVersion from webknossos.releaseInformation) = 135, 'Previous schema version mismatch'; end; $$ LANGUAGE plpgsql; +do $$ begin ASSERT (select schemaVersion from webknossos.releaseInformation) = 136, 'Previous schema version mismatch'; end; $$ LANGUAGE plpgsql; DROP VIEW IF EXISTS webknossos.userInfos; DROP VIEW IF EXISTS webknossos.multiUsers_; @@ -18,6 +18,6 @@ FROM webknossos.users_ u JOIN webknossos.organizations_ o ON u._organization = o._id JOIN webknossos.multiUsers_ m on u._multiUser = m._id; -UPDATE webknossos.releaseInformation SET schemaVersion = 134; +UPDATE webknossos.releaseInformation SET schemaVersion = 135; COMMIT TRANSACTION; diff --git a/tools/postgres/schema.sql b/tools/postgres/schema.sql index ceb3db05140..bf470e2dd2f 100644 --- a/tools/postgres/schema.sql +++ b/tools/postgres/schema.sql @@ -21,7 +21,7 @@ CREATE TABLE webknossos.releaseInformation ( schemaVersion BIGINT NOT NULL ); -INSERT INTO webknossos.releaseInformation(schemaVersion) values(135); +INSERT INTO webknossos.releaseInformation(schemaVersion) values(136); COMMIT TRANSACTION; diff --git a/unreleased_changes/8671.md b/unreleased_changes/8671.md index b7776ecf62c..6934d163e76 100644 --- a/unreleased_changes/8671.md +++ b/unreleased_changes/8671.md @@ -5,5 +5,5 @@ - An administrator of the organization can no longer update a users email address. This has to be done by the user themselves. ### Postgres Evolutions -- [135-extra-column-for-email-changed.sql](conf/evolutions/135-extra-column-for-email-changed.sql) +- [136-extra-column-for-email-changed.sql](conf/evolutions/136-extra-column-for-email-changed.sql) From 30e72abcb422eeeea0b137056b48ca54f34be2b2 Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Tue, 15 Jul 2025 12:32:34 +0200 Subject: [PATCH 18/20] update import --- app/controllers/UserController.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/UserController.scala b/app/controllers/UserController.scala index 5a92da080f1..43e827fb91b 100755 --- a/app/controllers/UserController.scala +++ b/app/controllers/UserController.scala @@ -15,7 +15,7 @@ import com.scalableminds.util.objectid.ObjectId import javax.inject.Inject import models.user.Theme.Theme -import net.liftweb.common.{Failure, Full} +import com.scalableminds.util.tools.{Failure, Full} import play.silhouette.api.exceptions.ProviderException import play.silhouette.api.util.Credentials import play.silhouette.impl.providers.CredentialsProvider From d0ca9c5db87117f2b6887d04b0d037f1dcf57882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20B=C3=BC=C3=9Femeyer?= <39529669+MichaelBuessemeyer@users.noreply.github.com> Date: Tue, 15 Jul 2025 13:29:55 +0200 Subject: [PATCH 19/20] remove unused import --- app/controllers/UserController.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/UserController.scala b/app/controllers/UserController.scala index 43e827fb91b..9249ece325b 100755 --- a/app/controllers/UserController.scala +++ b/app/controllers/UserController.scala @@ -1,7 +1,7 @@ package controllers import play.silhouette.api.Silhouette -import com.scalableminds.util.accesscontext.{DBAccessContext, GlobalAccessContext} +import com.scalableminds.util.accesscontext.GlobalAccessContext import com.scalableminds.util.tools.{Fox, FoxImplicits} import models.annotation.{AnnotationDAO, AnnotationService, AnnotationType} import models.organization.OrganizationService From 6b14824f2944c5b1149689ec6df7997d315f76b6 Mon Sep 17 00:00:00 2001 From: Charlie Meister Date: Tue, 15 Jul 2025 14:32:58 +0200 Subject: [PATCH 20/20] sort imports and call function rather than route directly --- frontend/javascripts/admin/auth/change_email_view.tsx | 7 +++---- frontend/javascripts/navbar.tsx | 4 ++-- frontend/javascripts/router.tsx | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/frontend/javascripts/admin/auth/change_email_view.tsx b/frontend/javascripts/admin/auth/change_email_view.tsx index 0909d0c77f8..ed9485655ec 100644 --- a/frontend/javascripts/admin/auth/change_email_view.tsx +++ b/frontend/javascripts/admin/auth/change_email_view.tsx @@ -1,8 +1,7 @@ import { LockOutlined, MailOutlined } from "@ant-design/icons"; -import { updateUser } from "admin/rest_api"; +import { logoutUser, updateUser } from "admin/rest_api"; import { Alert, Button, Col, Form, Input, Row } from "antd"; import { useWkSelector } from "libs/react_hooks"; -import Request from "libs/request"; import Toast from "libs/toast"; import { type RouteComponentProps, withRouter } from "react-router-dom"; import { logoutUserAction } from "viewer/model/actions/user_actions"; @@ -34,9 +33,9 @@ function ChangeEmailView() { .then(async () => { handleResendVerificationEmail(); Toast.success("Email address changed successfully. You will be logged out."); - await Request.receiveJSON("/api/auth/logout"); - window.location.href = "/auth/login"; + await logoutUser(); Store.dispatch(logoutUserAction()); + window.location.href = "/auth/login"; }) .catch((error) => { const errorMsg = "An unexpected error occurred while changing the email address."; diff --git a/frontend/javascripts/navbar.tsx b/frontend/javascripts/navbar.tsx index 9146e5808cf..9fa8857db5c 100644 --- a/frontend/javascripts/navbar.tsx +++ b/frontend/javascripts/navbar.tsx @@ -32,6 +32,7 @@ import { PricingPlanEnum } from "admin/organization/pricing_plan_utils"; import { getBuildInfo, getUsersOrganizations, + logoutUser, sendAnalyticsEvent, switchToOrganization, updateNovelUserExperienceInfos, @@ -42,7 +43,6 @@ import { PricingEnforcedSpan } from "components/pricing_enforcers"; import features from "features"; import { useFetch, useInterval } from "libs/react_helpers"; import { useWkSelector } from "libs/react_hooks"; -import Request from "libs/request"; import Toast from "libs/toast"; import * as Utils from "libs/utils"; import window, { location } from "libs/window"; @@ -790,7 +790,7 @@ function Navbar({ const handleLogout = async (event: React.SyntheticEvent) => { event.preventDefault(); - await Request.receiveJSON("/api/auth/logout"); + await logoutUser(); Store.dispatch(logoutUserAction()); // Hard navigation location.href = "/"; diff --git a/frontend/javascripts/router.tsx b/frontend/javascripts/router.tsx index e4885c9d4ad..8e717876112 100644 --- a/frontend/javascripts/router.tsx +++ b/frontend/javascripts/router.tsx @@ -1,4 +1,5 @@ import AcceptInviteView from "admin/auth/accept_invite_view"; +import ChangeEmailView from "admin/auth/change_email_view"; import FinishResetPasswordView from "admin/auth/finish_reset_password_view"; import LoginView from "admin/auth/login_view"; import RegistrationView from "admin/auth/registration_view"; @@ -15,7 +16,6 @@ import { getShortLink, getUnversionedAnnotationInformation, } from "admin/rest_api"; -import ChangeEmailView from "admin/auth/change_email_view"; import ScriptCreateView from "admin/scripts/script_create_view"; import ScriptListView from "admin/scripts/script_list_view"; import AvailableTasksReportView from "admin/statistic/available_tasks_report_view";