Skip to content

Allow users to change their own email address #8671

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

Closed
wants to merge 26 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
bdab4af
allow user to change email and set email in user list to read-only
knollengewaechs Jun 4, 2025
8b4ac8b
send verification email
knollengewaechs Jun 4, 2025
3562d51
enable users to edit their own email address
MichaelBuessemeyer Jun 4, 2025
07fabe0
allow to edit own email addres for non admins
MichaelBuessemeyer Jun 4, 2025
209bf8b
add specific column tracking when the last email change was for more…
MichaelBuessemeyer Jun 4, 2025
9774d5a
Merge branch 'master' into users-can-change-email
knollengewaechs Jun 6, 2025
58bcc29
clean up frontend code
knollengewaechs Jun 6, 2025
74fe1f2
add password verification upon email update
MichaelBuessemeyer Jun 14, 2025
5b866a0
Merge branch 'users-can-change-email' of github.com:scalableminds/web…
MichaelBuessemeyer Jun 14, 2025
04151f4
disallow admins updating emails of other users
MichaelBuessemeyer Jun 18, 2025
8d7ed7b
add entry to MIGRATIONS.unreleased.md
MichaelBuessemeyer Jun 18, 2025
8be7adb
format backend
MichaelBuessemeyer Jun 18, 2025
56098bc
Merge branch 'master' into users-can-change-email
MichaelBuessemeyer Jun 18, 2025
9ec548d
remove logging used for testing
MichaelBuessemeyer Jun 18, 2025
d443e02
Merge branch 'users-can-change-email' of github.com:scalableminds/web…
MichaelBuessemeyer Jun 18, 2025
cc94854
Merge branch 'master' into users-can-change-email
fm3 Jun 19, 2025
2a1166d
unused import
fm3 Jun 19, 2025
9dd940d
Merge branch 'users-can-change-email' of github.com:scalableminds/web…
fm3 Jun 19, 2025
7ff58c0
add missing emailChangeDate column to multi
MichaelBuessemeyer Jun 23, 2025
c45795f
apply pr feedback
MichaelBuessemeyer Jun 23, 2025
dae5355
Merge branch 'master' of github.com:scalableminds/webknossos into use…
MichaelBuessemeyer Jun 23, 2025
34bb221
Merge branch 'users-can-change-email' of github.com:scalableminds/web…
MichaelBuessemeyer Jun 23, 2025
c9731b2
include multiuserid in in logging upon email changed by user
MichaelBuessemeyer Jun 24, 2025
4477287
Update unreleased_changes/8671.md
MichaelBuessemeyer Jun 24, 2025
b4e3e55
merge master
knollengewaechs Jul 14, 2025
d540be7
Revert "merge master"
knollengewaechs Jul 14, 2025
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
4 changes: 2 additions & 2 deletions conf/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines -86 to 88
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for testing purposes. REMOVE BEFORE MERGING

linkExpiry = 30 days
}
Expand Down
168 changes: 168 additions & 0 deletions frontend/javascripts/admin/auth/change_email_view.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
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";
import { handleResendVerificationEmail } from "./verify_email_view";
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(() => {
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
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 (
<Row
justify="center"
align="middle"
style={{
padding: 50,
}}
>
<Col span={8}>
<h3>Change Email</h3>
<Alert
type="info"
message="You will be logged out after changing your email address."
showIcon
style={{
marginBottom: 24,
}}
/>
<Form onFinish={onFinish} form={form}>
<FormItem
name="password"
rules={[
{
required: true,
message: "Please enter your password for verification",
},
]}
>
<Input.Password
prefix={
<LockOutlined
style={{
fontSize: 13,
}}
/>
}
placeholder="Your Password"
/>
</FormItem>
<FormItem
hasFeedback
name="newEmail"
rules={[
{
required: true,
message: "Please enter your new email address",
},
{
type: "email",
message: "Please enter a valid email address",
},
{
validator: (_, value: string) => checkEmailsAreMatching(value, ["confirmNewEmail"]),
},
]}
>
<Input
prefix={
<MailOutlined
style={{
fontSize: 13,
}}
/>
}
placeholder="New Email Address"
/>
</FormItem>
<FormItem
hasFeedback
name="confirmNewEmail"
rules={[
{
required: true,
message: "Please confirm your new email address",
},
{
type: "email",
message: "Please enter a valid email address",
},
{
validator: (_, value: string) => checkEmailsAreMatching(value, ["newEmail"]),
},
]}
>
<Input
prefix={
<MailOutlined
style={{
fontSize: 13,
}}
/>
}
placeholder="Confirm New Email Address"
/>
</FormItem>
<FormItem>
<Button
type="primary"
htmlType="submit"
style={{
width: "100%",
}}
>
Change Email
</Button>
</FormItem>
</Form>
</Col>
</Row>
);
}

export default withRouter<RouteComponentProps, any>(ChangeEmailView);
55 changes: 1 addition & 54 deletions frontend/javascripts/admin/user/user_list_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,14 @@ 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";
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";
Expand All @@ -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;
Expand Down Expand Up @@ -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<APIUser>): void {
setUsers(updatedUsers);
setIsExperienceModalOpen(false);
Expand Down Expand Up @@ -423,33 +397,6 @@ function UserListView({ activeUser, activeOrganization }: Props) {
key="email"
width={320}
sorter={Utils.localeCompareBy<APIUser>((user) => user.email)}
render={(__, user: APIUser) =>
activeUser.isAdmin ? (
<EditableTextLabel
value={user.email}
label="Email"
rules={[
{
message: messages["auth.registration_email_invalid"],
type: "email",
},
]}
onChange={(newEmail) => {
if (newEmail !== user.email) {
Modal.confirm({
title: messages["users.change_email_title"],
content: messages["users.change_email"]({
newEmail,
}),
onOk: () => changeEmail(user, newEmail),
});
}
}}
/>
) : (
user.email
)
}
/>
<Column
title="Experiences"
Expand Down
1 change: 1 addition & 0 deletions frontend/javascripts/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,7 @@ function LoggedInAvatar({
key: "resetpassword",
label: <Link to="/auth/changePassword">Change Password</Link>,
},
{ key: "changeEmail", label: <Link to="/auth/changeEmail">Change Email</Link> },
{ key: "token", label: <Link to="/auth/token">Auth Token</Link> },
{
key: "theme",
Expand Down
6 changes: 6 additions & 0 deletions frontend/javascripts/router.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -644,6 +645,11 @@ class ReactRouter extends React.Component<Props> {
path="/auth/token"
component={AuthTokenView}
/>
<SecuredRouteWithErrorBoundary
isAuthenticated={isAuthenticated}
path="/auth/changeEmail"
component={ChangeEmailView}
/>
<SecuredRouteWithErrorBoundary
isAuthenticated={isAuthenticated}
path="/auth/changePassword"
Expand Down