Skip to content
This repository has been archived by the owner on Sep 10, 2024. It is now read-only.

Commit

Permalink
Add Self-service Password Change (#2863)
Browse files Browse the repository at this point in the history
Co-authored-by: Quentin Gliech <quenting@element.io>
  • Loading branch information
reivilibre and sandhose authored Jun 25, 2024
1 parent 121966c commit aaa7cf3
Show file tree
Hide file tree
Showing 14 changed files with 591 additions and 7 deletions.
5 changes: 5 additions & 0 deletions crates/handlers/src/graphql/model/site_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub const SITE_CONFIG_ID: &str = "site_config";

#[derive(SimpleObject)]
#[graphql(complex)]
#[allow(clippy::struct_excessive_bools)]
pub struct SiteConfig {
/// The server name of the homeserver.
server_name: String,
Expand All @@ -40,6 +41,9 @@ pub struct SiteConfig {
/// Whether users can change their display name.
display_name_change_allowed: bool,

/// Whether passwords are enabled for login.
password_login_enabled: bool,

/// Whether passwords are enabled and users can change their own passwords.
password_change_allowed: bool,
}
Expand All @@ -63,6 +67,7 @@ impl SiteConfig {
imprint: data_model.imprint.clone(),
email_change_allowed: data_model.email_change_allowed,
display_name_change_allowed: data_model.displayname_change_allowed,
password_login_enabled: data_model.password_login_enabled,
password_change_allowed: data_model.password_change_allowed,
}
}
Expand Down
31 changes: 31 additions & 0 deletions frontend/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
"title": "Edit profile",
"username_label": "Username"
},
"password": {
"change": "Change password",
"change_disabled": "Password changes are disabled by the administrator.",
"label": "Password"
},
"title": "Your account"
},
"add_email_form": {
Expand Down Expand Up @@ -79,6 +84,9 @@
"subtitle": "An unexpected error occurred. Please try again.",
"title": "Something went wrong"
},
"errors": {
"field_required": "This field is required"
},
"last_active": {
"active_date": "Active {{relativeDate}}",
"active_now": "Active now",
Expand All @@ -103,6 +111,29 @@
"pagination_controls": {
"total": "Total: {{totalCount}}"
},
"password_change": {
"current_password_label": "Current password",
"failure": {
"description": {
"invalid_new_password": "The new password you chose is invalid; it may not meet the configured security policy.",
"no_current_password": "You don't have a current password.",
"password_changes_disabled": "Password changes are disabled.",
"unspecified": "This might be a temporary problem, so please try again later. If the problem persists, please contact your server administrator.",
"wrong_password": "The password you supplied as your current password is incorrect. Please try again."
},
"title": "Failed to update password"
},
"new_password_again_label": "Enter new password again",
"new_password_label": "New password",
"passwords_match": "Passwords match!",
"passwords_no_match": "Passwords don't match",
"subtitle": "Choose a new password for your account.",
"success": {
"description": "Your password has been updated successfully.",
"title": "Password updated"
},
"title": "Change your password"
},
"reset_cross_signing": {
"button": "Allow crypto identity reset",
"description": "If you are not signed in anywhere else, and have forgotten or lost all recovery options you’ll need to reset your crypto identity. This means you will lose your existing message history, other users will see that you have reset your identity and you will need to verify your existing devices again.",
Expand Down
18 changes: 16 additions & 2 deletions frontend/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@
"title": "Editer le profil",
"username_label": "Nom d’utilisateur"
},
"title": "Votre compte"
"title": "Votre compte",
"password": {
"label": "Mot de passe",
"change": "Changer le mot de passe"
}
},
"add_email_form": {
"email_denied_alert": {
Expand Down Expand Up @@ -88,6 +92,9 @@
"subtitle": "Une erreur inattendue s'est produite. Veuillez réessayer",
"title": "Un problème est survenu"
},
"errors": {
"field_required": "Ce champ est requis"
},
"error_boundary_title": "Un problème est survenu",
"last_active": {
"active_date": "Actif {{relativeDate}}",
Expand Down Expand Up @@ -117,6 +124,13 @@
"pagination_controls": {
"total": "Total : {{totalCount}}"
},
"password_change": {
"title": "Changer le mot de passe",
"subtitle": "Cela modifiera le mot de passe de votre compte.",
"current_password_label": "Mot de passe actuel",
"new_password_label": "Nouveau mot de passe",
"new_password_again_label": "Confirmer le mot de passe"
},
"reset_cross_signing": {
"button": "Autoriser le remplacement de l'identité cryptographique",
"description": "Si vous n'êtes connecté nulle part ailleurs et que vous avez oublié ou perdu toutes vos options de récupération, vous devez réinitialiser votre identité cryptographique. Cela signifie que vous perdrez votre historique de message, que les autres utilisateurs verront que vous avez réinitialisé votre identité et que vous devrez à nouveau vérifier vos appareils existants.",
Expand Down Expand Up @@ -229,4 +243,4 @@
"view_profile": "Voir les informations de votre profil et vos coordonnées"
}
}
}
}
4 changes: 4 additions & 0 deletions frontend/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1349,6 +1349,10 @@ type SiteConfig implements Node {
"""
displayNameChangeAllowed: Boolean!
"""
Whether passwords are enabled for login.
"""
passwordLoginEnabled: Boolean!
"""
Whether passwords are enabled and users can change their own passwords.
"""
passwordChangeAllowed: Boolean!
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/* Copyright 2024 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

.link {
display: inline-block;
text-decoration: underline;
color: var(--cpd-color-text-primary);
font-weight: var(--cpd-font-weight-medium);
border-radius: var(--cpd-radius-pill-effect);
padding-inline: 0.25rem;
}

.link:hover {
background: var(--cpd-color-gray-300);
}

.link:active {
background: var(--cpd-color-text-primary);
color: var(--cpd-color-text-on-solid-primary);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import { Link } from "@tanstack/react-router";
import { Form } from "@vector-im/compound-web";
import { useTranslation } from "react-i18next";

import { FragmentType, graphql, useFragment } from "../../gql";

import styles from "./AccountManagementPasswordPreview.module.css";

const CONFIG_FRAGMENT = graphql(/* GraphQL */ `
fragment PasswordChange_siteConfig on SiteConfig {
id
passwordChangeAllowed
}
`);

export default function AccountManagementPasswordPreview({
siteConfig,
}: {
siteConfig: FragmentType<typeof CONFIG_FRAGMENT>;
}): React.ReactElement {
const { t } = useTranslation();
const { passwordChangeAllowed } = useFragment(CONFIG_FRAGMENT, siteConfig);

return (
<Form.Root>
<Form.Field name="password_preview">
<Form.Label>{t("frontend.account.password.label")}</Form.Label>

<Form.TextControl
type="password"
readOnly
value="this looks like a password"
/>

<Form.HelpMessage>
{passwordChangeAllowed && (
<Link to="/password/change" className={styles.link}>
{t("frontend.account.password.change")}
</Link>
)}

{!passwordChangeAllowed &&
t("frontend.account.password.change_disabled")}
</Form.HelpMessage>
</Form.Field>
</Form.Root>
);
}
15 changes: 15 additions & 0 deletions frontend/src/components/AccountManagementPasswordPreview/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright 2024 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

export { default } from "./AccountManagementPasswordPreview";
14 changes: 12 additions & 2 deletions frontend/src/gql/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/
* Therefore it is highly recommended to use the babel or swc plugin for production.
*/
const documents = {
"\n fragment PasswordChange_siteConfig on SiteConfig {\n id\n passwordChangeAllowed\n }\n": types.PasswordChange_SiteConfigFragmentDoc,
"\n fragment BrowserSession_session on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent {\n raw\n name\n os\n model\n deviceType\n }\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n }\n": types.BrowserSession_SessionFragmentDoc,
"\n mutation EndBrowserSession($id: ID!) {\n endBrowserSession(input: { browserSessionId: $id }) {\n status\n browserSession {\n id\n ...BrowserSession_session\n }\n }\n }\n": types.EndBrowserSessionDocument,
"\n fragment OAuth2Client_detail on Oauth2Client {\n id\n clientId\n clientName\n clientUri\n logoUri\n tosUri\n policyUri\n redirectUris\n }\n": types.OAuth2Client_DetailFragmentDoc,
Expand Down Expand Up @@ -41,7 +42,7 @@ const documents = {
"\n fragment UserEmail_verifyEmail on UserEmail {\n id\n email\n }\n": types.UserEmail_VerifyEmailFragmentDoc,
"\n mutation VerifyEmail($id: ID!, $code: String!) {\n verifyEmail(input: { userEmailId: $id, code: $code }) {\n status\n\n user {\n id\n primaryEmail {\n id\n }\n }\n\n email {\n id\n ...UserEmail_email\n }\n }\n }\n": types.VerifyEmailDocument,
"\n mutation ResendVerificationEmail($id: ID!) {\n sendVerificationEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n primaryEmail {\n id\n }\n }\n\n email {\n id\n ...UserEmail_email\n }\n }\n }\n": types.ResendVerificationEmailDocument,
"\n query UserProfileQuery {\n viewer {\n __typename\n ... on User {\n id\n\n primaryEmail {\n id\n ...UserEmail_email\n }\n\n ...UserEmailList_user\n }\n }\n\n siteConfig {\n id\n emailChangeAllowed\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n }\n }\n": types.UserProfileQueryDocument,
"\n query UserProfileQuery {\n viewer {\n __typename\n ... on User {\n id\n\n primaryEmail {\n id\n ...UserEmail_email\n }\n\n ...UserEmailList_user\n }\n }\n\n siteConfig {\n id\n emailChangeAllowed\n passwordLoginEnabled\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n": types.UserProfileQueryDocument,
"\n query SessionDetailQuery($id: ID!) {\n viewerSession {\n ... on Node {\n id\n }\n }\n\n node(id: $id) {\n __typename\n id\n ...CompatSession_detail\n ...OAuth2Session_detail\n ...BrowserSession_detail\n }\n }\n": types.SessionDetailQueryDocument,
"\n query BrowserSessionList(\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n viewerSession {\n __typename\n ... on BrowserSession {\n id\n\n user {\n id\n\n browserSessions(\n first: $first\n after: $after\n last: $last\n before: $before\n state: ACTIVE\n ) {\n totalCount\n\n edges {\n cursor\n node {\n id\n ...BrowserSession_session\n }\n }\n\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n }\n }\n": types.BrowserSessionListDocument,
"\n query SessionsOverviewQuery {\n viewer {\n __typename\n\n ... on User {\n id\n ...BrowserSessionsOverview_user\n }\n }\n }\n": types.SessionsOverviewQueryDocument,
Expand All @@ -51,6 +52,7 @@ const documents = {
"\n query CurrentViewerQuery {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n }\n": types.CurrentViewerQueryDocument,
"\n query DeviceRedirectQuery($deviceId: String!, $userId: ID!) {\n session(deviceId: $deviceId, userId: $userId) {\n __typename\n ... on Node {\n id\n }\n }\n }\n": types.DeviceRedirectQueryDocument,
"\n query VerifyEmailQuery($id: ID!) {\n userEmail(id: $id) {\n ...UserEmail_verifyEmail\n }\n }\n": types.VerifyEmailQueryDocument,
"\n mutation ChangePassword(\n $userId: ID!\n $oldPassword: String!\n $newPassword: String!\n ) {\n setPassword(\n input: {\n userId: $userId\n currentPassword: $oldPassword\n newPassword: $newPassword\n }\n ) {\n status\n }\n }\n": types.ChangePasswordDocument,
"\n mutation AllowCrossSigningReset($userId: ID!) {\n allowUserCrossSigningReset(input: { userId: $userId }) {\n user {\n id\n }\n }\n }\n": types.AllowCrossSigningResetDocument,
};

Expand All @@ -68,6 +70,10 @@ const documents = {
*/
export function graphql(source: string): unknown;

/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment PasswordChange_siteConfig on SiteConfig {\n id\n passwordChangeAllowed\n }\n"): (typeof documents)["\n fragment PasswordChange_siteConfig on SiteConfig {\n id\n passwordChangeAllowed\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand Down Expand Up @@ -183,7 +189,7 @@ export function graphql(source: "\n mutation ResendVerificationEmail($id: ID!)
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query UserProfileQuery {\n viewer {\n __typename\n ... on User {\n id\n\n primaryEmail {\n id\n ...UserEmail_email\n }\n\n ...UserEmailList_user\n }\n }\n\n siteConfig {\n id\n emailChangeAllowed\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n }\n }\n"): (typeof documents)["\n query UserProfileQuery {\n viewer {\n __typename\n ... on User {\n id\n\n primaryEmail {\n id\n ...UserEmail_email\n }\n\n ...UserEmailList_user\n }\n }\n\n siteConfig {\n id\n emailChangeAllowed\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n }\n }\n"];
export function graphql(source: "\n query UserProfileQuery {\n viewer {\n __typename\n ... on User {\n id\n\n primaryEmail {\n id\n ...UserEmail_email\n }\n\n ...UserEmailList_user\n }\n }\n\n siteConfig {\n id\n emailChangeAllowed\n passwordLoginEnabled\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n"): (typeof documents)["\n query UserProfileQuery {\n viewer {\n __typename\n ... on User {\n id\n\n primaryEmail {\n id\n ...UserEmail_email\n }\n\n ...UserEmailList_user\n }\n }\n\n siteConfig {\n id\n emailChangeAllowed\n passwordLoginEnabled\n ...UserEmailList_siteConfig\n ...UserEmail_siteConfig\n ...PasswordChange_siteConfig\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand Down Expand Up @@ -220,6 +226,10 @@ export function graphql(source: "\n query DeviceRedirectQuery($deviceId: String
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query VerifyEmailQuery($id: ID!) {\n userEmail(id: $id) {\n ...UserEmail_verifyEmail\n }\n }\n"): (typeof documents)["\n query VerifyEmailQuery($id: ID!) {\n userEmail(id: $id) {\n ...UserEmail_verifyEmail\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation ChangePassword(\n $userId: ID!\n $oldPassword: String!\n $newPassword: String!\n ) {\n setPassword(\n input: {\n userId: $userId\n currentPassword: $oldPassword\n newPassword: $newPassword\n }\n ) {\n status\n }\n }\n"): (typeof documents)["\n mutation ChangePassword(\n $userId: ID!\n $oldPassword: String!\n $newPassword: String!\n ) {\n setPassword(\n input: {\n userId: $userId\n currentPassword: $oldPassword\n newPassword: $newPassword\n }\n ) {\n status\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand Down
Loading

0 comments on commit aaa7cf3

Please sign in to comment.