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

Add Self-service Password Change #2863

Merged
merged 15 commits into from
Jun 25, 2024
Merged
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
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 {
Copy link
Member

Choose a reason for hiding this comment

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

There is an existing Link component which should have the same style?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I stole this from UserEmail FWIW

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the Link component does not have the underline or the white text colour, it just looks like plain grey text

Copy link
Member

Choose a reason for hiding this comment

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

Fundamentally we're missing a good implementation of the Text Link component: https://www.figma.com/design/rTaQE2nIUSLav4Tg3nozq7/Compound-Web-Components?node-id=645-3494&t=OH2MjoxKqsJTB1vF-4

I'll leave the duplication for now, and will see to implement that in Compound

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
Loading