mirror of
https://github.com/element-hq/matrix-authentication-service.git
synced 2026-10-22 19:44:30 +00:00
Extract the password complexity feedback out of the double-password input component (#5978)
This commit is contained in:
7 files changed
+126
-124
No files matched your search
@@ -0,0 +1,49 @@
|
||||
// Copyright 2026 Element Creations Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import { Form, Progress } from "@vector-im/compound-web";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { usePasswordComplexity } from "../utils/usePasswordComplexity";
|
||||
|
||||
const TINTS = ["red", "red", "orange", "lime", "green"] as const;
|
||||
|
||||
/**
|
||||
* Strength meter, improvement hints and too-weak error for a new password.
|
||||
* Renders as a set of `Form.Field` children, so it must be used inside one.
|
||||
*/
|
||||
const PasswordComplexityFeedback: React.FC<{
|
||||
password: string;
|
||||
minimumPasswordComplexity: number;
|
||||
}> = ({ password, minimumPasswordComplexity }) => {
|
||||
const { t } = useTranslation();
|
||||
const complexity = usePasswordComplexity(password);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Progress
|
||||
size="sm"
|
||||
getValueLabel={() => complexity.scoreText}
|
||||
tint={password === "" ? undefined : TINTS[complexity.score]}
|
||||
max={4}
|
||||
value={complexity.score}
|
||||
/>
|
||||
|
||||
{complexity.improvementsText.map((suggestion) => (
|
||||
<Form.HelpMessage key={suggestion}>{suggestion}</Form.HelpMessage>
|
||||
))}
|
||||
|
||||
{complexity.score < minimumPasswordComplexity && (
|
||||
<Form.ErrorMessage match={() => true}>
|
||||
{t("frontend.password_strength.too_weak")}
|
||||
</Form.ErrorMessage>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PasswordComplexityFeedback;
|
||||
@@ -1,84 +1,27 @@
|
||||
// Copyright 2025, 2026 Element Creations Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import { Form, Progress } from "@vector-im/compound-web";
|
||||
import { useDeferredValue, useEffect, useRef, useState } from "react";
|
||||
import { Form } from "@vector-im/compound-web";
|
||||
import { useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { type FragmentType, graphql, useFragment } from "../gql";
|
||||
import type { PasswordComplexity } from "../utils/password_complexity";
|
||||
|
||||
const CONFIG_FRAGMENT = graphql(/* GraphQL */ `
|
||||
fragment PasswordCreationDoubleInput_siteConfig on SiteConfig {
|
||||
id
|
||||
minimumPasswordComplexity
|
||||
}
|
||||
`);
|
||||
|
||||
// This will load the password complexity module lazily,
|
||||
// so that it doesn't block the initial render and can be code-split
|
||||
const loadPromise = import("../utils/password_complexity").then(
|
||||
({ estimatePasswordComplexity }) => estimatePasswordComplexity,
|
||||
);
|
||||
|
||||
const usePasswordComplexity = (password: string): PasswordComplexity => {
|
||||
const { t } = useTranslation();
|
||||
const [result, setResult] = useState<PasswordComplexity>({
|
||||
score: 0,
|
||||
scoreText: t("frontend.password_strength.placeholder"),
|
||||
improvementsText: [],
|
||||
});
|
||||
const deferredPassword = useDeferredValue(password);
|
||||
|
||||
useEffect(() => {
|
||||
if (deferredPassword === "") {
|
||||
setResult({
|
||||
score: 0,
|
||||
scoreText: t("frontend.password_strength.placeholder"),
|
||||
improvementsText: [],
|
||||
});
|
||||
} else {
|
||||
loadPromise
|
||||
.then((estimatePasswordComplexity) =>
|
||||
estimatePasswordComplexity(deferredPassword, t),
|
||||
)
|
||||
.then((response) => setResult(response));
|
||||
}
|
||||
}, [deferredPassword, t]);
|
||||
|
||||
return result;
|
||||
};
|
||||
import PasswordComplexityFeedback from "./PasswordComplexityFeedback";
|
||||
|
||||
export default function PasswordCreationDoubleInput({
|
||||
siteConfig,
|
||||
forceShowNewPasswordInvalid,
|
||||
minimumPasswordComplexity,
|
||||
forceShowNewPasswordInvalid = false,
|
||||
}: {
|
||||
siteConfig: FragmentType<typeof CONFIG_FRAGMENT>;
|
||||
forceShowNewPasswordInvalid: boolean;
|
||||
minimumPasswordComplexity: number;
|
||||
forceShowNewPasswordInvalid?: boolean;
|
||||
}): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
const { minimumPasswordComplexity } = useFragment(
|
||||
CONFIG_FRAGMENT,
|
||||
siteConfig,
|
||||
);
|
||||
|
||||
const newPasswordRef = useRef<HTMLInputElement>(null);
|
||||
const newPasswordAgainRef = useRef<HTMLInputElement>(null);
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
|
||||
const passwordComplexity = usePasswordComplexity(newPassword);
|
||||
let passwordStrengthTint: "red" | "orange" | "lime" | "green" | undefined;
|
||||
if (newPassword === "") {
|
||||
passwordStrengthTint = undefined;
|
||||
} else {
|
||||
passwordStrengthTint = (["red", "red", "orange", "lime", "green"] as const)[
|
||||
passwordComplexity.score
|
||||
];
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Field name="new_password">
|
||||
@@ -89,7 +32,6 @@ export default function PasswordCreationDoubleInput({
|
||||
<Form.PasswordControl
|
||||
required
|
||||
autoComplete="new-password"
|
||||
ref={newPasswordRef}
|
||||
onBlur={() =>
|
||||
newPasswordAgainRef.current?.value &&
|
||||
newPasswordAgainRef.current?.reportValidity()
|
||||
@@ -97,24 +39,11 @@ export default function PasswordCreationDoubleInput({
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
/>
|
||||
|
||||
<Progress
|
||||
size="sm"
|
||||
getValueLabel={() => passwordComplexity.scoreText}
|
||||
tint={passwordStrengthTint}
|
||||
max={4}
|
||||
value={passwordComplexity.score}
|
||||
<PasswordComplexityFeedback
|
||||
password={newPassword}
|
||||
minimumPasswordComplexity={minimumPasswordComplexity}
|
||||
/>
|
||||
|
||||
{passwordComplexity.improvementsText.map((suggestion) => (
|
||||
<Form.HelpMessage key={suggestion}>{suggestion}</Form.HelpMessage>
|
||||
))}
|
||||
|
||||
{passwordComplexity.score < minimumPasswordComplexity && (
|
||||
<Form.ErrorMessage match={() => true}>
|
||||
{t("frontend.password_strength.too_weak")}
|
||||
</Form.ErrorMessage>
|
||||
)}
|
||||
|
||||
<Form.ErrorMessage match="valueMissing">
|
||||
{t("frontend.errors.field_required")}
|
||||
</Form.ErrorMessage>
|
||||
|
||||
+6
-12
@@ -25,7 +25,6 @@ type Documents = {
|
||||
"\n fragment Footer_siteConfig on SiteConfig {\n id\n imprint\n tosUri\n policyUri\n }\n": typeof types.Footer_SiteConfigFragmentDoc,
|
||||
"\n query Footer {\n siteConfig {\n id\n ...Footer_siteConfig\n }\n }\n": typeof types.FooterDocument,
|
||||
"\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n": typeof types.OAuth2Session_SessionFragmentDoc,
|
||||
"\n fragment PasswordCreationDoubleInput_siteConfig on SiteConfig {\n id\n minimumPasswordComplexity\n }\n": typeof types.PasswordCreationDoubleInput_SiteConfigFragmentDoc,
|
||||
"\n fragment EndBrowserSessionButton_session on BrowserSession {\n id\n userAgent {\n name\n os\n model\n deviceType\n }\n }\n": typeof types.EndBrowserSessionButton_SessionFragmentDoc,
|
||||
"\n mutation EndBrowserSession($id: ID!) {\n endBrowserSession(input: { browserSessionId: $id }) {\n status\n browserSession {\n id\n }\n }\n }\n": typeof types.EndBrowserSessionDocument,
|
||||
"\n fragment EndCompatSessionButton_session on CompatSession {\n id\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n": typeof types.EndCompatSessionButton_SessionFragmentDoc,
|
||||
@@ -62,11 +61,11 @@ type Documents = {
|
||||
"\n mutation DoVerifyEmail($id: ID!, $code: String!) {\n completeEmailAuthentication(input: { id: $id, code: $code }) {\n status\n }\n }\n": typeof types.DoVerifyEmailDocument,
|
||||
"\n mutation ResendEmailAuthenticationCode($id: ID!, $language: String!) {\n resendEmailAuthenticationCode(input: { id: $id, language: $language }) {\n status\n }\n }\n": typeof types.ResendEmailAuthenticationCodeDocument,
|
||||
"\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 types.ChangePasswordDocument,
|
||||
"\n query PasswordChange {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n\n siteConfig {\n ...PasswordCreationDoubleInput_siteConfig\n }\n }\n": typeof types.PasswordChangeDocument,
|
||||
"\n query PasswordChange {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n\n siteConfig {\n minimumPasswordComplexity\n }\n }\n": typeof types.PasswordChangeDocument,
|
||||
"\n mutation RecoverPassword($ticket: String!, $newPassword: String!) {\n setPasswordByRecovery(\n input: { ticket: $ticket, newPassword: $newPassword }\n ) {\n status\n }\n }\n": typeof types.RecoverPasswordDocument,
|
||||
"\n mutation ResendRecoveryEmail($ticket: String!) {\n resendRecoveryEmail(input: { ticket: $ticket }) {\n status\n progressUrl\n }\n }\n": typeof types.ResendRecoveryEmailDocument,
|
||||
"\n fragment RecoverPassword_userRecoveryTicket on UserRecoveryTicket {\n username\n email\n }\n": typeof types.RecoverPassword_UserRecoveryTicketFragmentDoc,
|
||||
"\n fragment RecoverPassword_siteConfig on SiteConfig {\n ...PasswordCreationDoubleInput_siteConfig\n }\n": typeof types.RecoverPassword_SiteConfigFragmentDoc,
|
||||
"\n fragment RecoverPassword_siteConfig on SiteConfig {\n minimumPasswordComplexity\n }\n": typeof types.RecoverPassword_SiteConfigFragmentDoc,
|
||||
"\n query PasswordRecovery($ticket: String!) {\n siteConfig {\n ...RecoverPassword_siteConfig\n }\n\n userRecoveryTicket(ticket: $ticket) {\n status\n ...RecoverPassword_userRecoveryTicket\n }\n }\n": typeof types.PasswordRecoveryDocument,
|
||||
"\n mutation AllowCrossSigningReset($userId: ID!) {\n allowUserCrossSigningReset(input: { userId: $userId }) {\n user {\n id\n }\n }\n }\n": typeof types.AllowCrossSigningResetDocument,
|
||||
"\n query SessionDetail($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": typeof types.SessionDetailDocument,
|
||||
@@ -82,7 +81,6 @@ const documents: Documents = {
|
||||
"\n fragment Footer_siteConfig on SiteConfig {\n id\n imprint\n tosUri\n policyUri\n }\n": types.Footer_SiteConfigFragmentDoc,
|
||||
"\n query Footer {\n siteConfig {\n id\n ...Footer_siteConfig\n }\n }\n": types.FooterDocument,
|
||||
"\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n": types.OAuth2Session_SessionFragmentDoc,
|
||||
"\n fragment PasswordCreationDoubleInput_siteConfig on SiteConfig {\n id\n minimumPasswordComplexity\n }\n": types.PasswordCreationDoubleInput_SiteConfigFragmentDoc,
|
||||
"\n fragment EndBrowserSessionButton_session on BrowserSession {\n id\n userAgent {\n name\n os\n model\n deviceType\n }\n }\n": types.EndBrowserSessionButton_SessionFragmentDoc,
|
||||
"\n mutation EndBrowserSession($id: ID!) {\n endBrowserSession(input: { browserSessionId: $id }) {\n status\n browserSession {\n id\n }\n }\n }\n": types.EndBrowserSessionDocument,
|
||||
"\n fragment EndCompatSessionButton_session on CompatSession {\n id\n userAgent {\n name\n os\n model\n deviceType\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n": types.EndCompatSessionButton_SessionFragmentDoc,
|
||||
@@ -119,11 +117,11 @@ const documents: Documents = {
|
||||
"\n mutation DoVerifyEmail($id: ID!, $code: String!) {\n completeEmailAuthentication(input: { id: $id, code: $code }) {\n status\n }\n }\n": types.DoVerifyEmailDocument,
|
||||
"\n mutation ResendEmailAuthenticationCode($id: ID!, $language: String!) {\n resendEmailAuthenticationCode(input: { id: $id, language: $language }) {\n status\n }\n }\n": types.ResendEmailAuthenticationCodeDocument,
|
||||
"\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 query PasswordChange {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n\n siteConfig {\n ...PasswordCreationDoubleInput_siteConfig\n }\n }\n": types.PasswordChangeDocument,
|
||||
"\n query PasswordChange {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n\n siteConfig {\n minimumPasswordComplexity\n }\n }\n": types.PasswordChangeDocument,
|
||||
"\n mutation RecoverPassword($ticket: String!, $newPassword: String!) {\n setPasswordByRecovery(\n input: { ticket: $ticket, newPassword: $newPassword }\n ) {\n status\n }\n }\n": types.RecoverPasswordDocument,
|
||||
"\n mutation ResendRecoveryEmail($ticket: String!) {\n resendRecoveryEmail(input: { ticket: $ticket }) {\n status\n progressUrl\n }\n }\n": types.ResendRecoveryEmailDocument,
|
||||
"\n fragment RecoverPassword_userRecoveryTicket on UserRecoveryTicket {\n username\n email\n }\n": types.RecoverPassword_UserRecoveryTicketFragmentDoc,
|
||||
"\n fragment RecoverPassword_siteConfig on SiteConfig {\n ...PasswordCreationDoubleInput_siteConfig\n }\n": types.RecoverPassword_SiteConfigFragmentDoc,
|
||||
"\n fragment RecoverPassword_siteConfig on SiteConfig {\n minimumPasswordComplexity\n }\n": types.RecoverPassword_SiteConfigFragmentDoc,
|
||||
"\n query PasswordRecovery($ticket: String!) {\n siteConfig {\n ...RecoverPassword_siteConfig\n }\n\n userRecoveryTicket(ticket: $ticket) {\n status\n ...RecoverPassword_userRecoveryTicket\n }\n }\n": types.PasswordRecoveryDocument,
|
||||
"\n mutation AllowCrossSigningReset($userId: ID!) {\n allowUserCrossSigningReset(input: { userId: $userId }) {\n user {\n id\n }\n }\n }\n": types.AllowCrossSigningResetDocument,
|
||||
"\n query SessionDetail($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.SessionDetailDocument,
|
||||
@@ -169,10 +167,6 @@ export function graphql(source: "\n query Footer {\n siteConfig {\n 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 fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n humanName\n\n ...EndOAuth2SessionButton_session\n\n userAgent {\n name\n model\n os\n deviceType\n }\n\n client {\n id\n clientId\n clientName\n applicationType\n logoUri\n }\n }\n"): typeof import('./graphql').OAuth2Session_SessionFragmentDoc;
|
||||
/**
|
||||
* 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 PasswordCreationDoubleInput_siteConfig on SiteConfig {\n id\n minimumPasswordComplexity\n }\n"): typeof import('./graphql').PasswordCreationDoubleInput_SiteConfigFragmentDoc;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -320,7 +314,7 @@ export function graphql(source: "\n mutation ChangePassword(\n $userId: 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 PasswordChange {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n\n siteConfig {\n ...PasswordCreationDoubleInput_siteConfig\n }\n }\n"): typeof import('./graphql').PasswordChangeDocument;
|
||||
export function graphql(source: "\n query PasswordChange {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n\n siteConfig {\n minimumPasswordComplexity\n }\n }\n"): typeof import('./graphql').PasswordChangeDocument;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -336,7 +330,7 @@ export function graphql(source: "\n fragment RecoverPassword_userRecoveryTicket
|
||||
/**
|
||||
* 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 RecoverPassword_siteConfig on SiteConfig {\n ...PasswordCreationDoubleInput_siteConfig\n }\n"): typeof import('./graphql').RecoverPassword_SiteConfigFragmentDoc;
|
||||
export function graphql(source: "\n fragment RecoverPassword_siteConfig on SiteConfig {\n minimumPasswordComplexity\n }\n"): typeof import('./graphql').RecoverPassword_SiteConfigFragmentDoc;
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
||||
@@ -224,8 +224,6 @@ export type OAuth2Session_SessionFragment = (
|
||||
& { ' $fragmentRefs'?: { 'EndOAuth2SessionButton_SessionFragment': EndOAuth2SessionButton_SessionFragment } }
|
||||
) & { ' $fragmentName'?: 'OAuth2Session_SessionFragment' };
|
||||
|
||||
export type PasswordCreationDoubleInput_SiteConfigFragment = { id: string, minimumPasswordComplexity: number } & { ' $fragmentName'?: 'PasswordCreationDoubleInput_SiteConfigFragment' };
|
||||
|
||||
export type EndBrowserSessionButton_SessionFragment = { id: string, userAgent: { name: string | null, os: string | null, model: string | null, deviceType: DeviceType } | null } & { ' $fragmentName'?: 'EndBrowserSessionButton_SessionFragment' };
|
||||
|
||||
export type EndBrowserSessionMutationVariables = Exact<{
|
||||
@@ -488,7 +486,7 @@ export type PasswordChangeQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
export type PasswordChangeQuery = { viewer:
|
||||
| { __typename: 'Anonymous', id: string }
|
||||
| { __typename: 'User', id: string }
|
||||
, siteConfig: { ' $fragmentRefs'?: { 'PasswordCreationDoubleInput_SiteConfigFragment': PasswordCreationDoubleInput_SiteConfigFragment } } };
|
||||
, siteConfig: { minimumPasswordComplexity: number } };
|
||||
|
||||
export type RecoverPasswordMutationVariables = Exact<{
|
||||
ticket: string;
|
||||
@@ -507,7 +505,7 @@ export type ResendRecoveryEmailMutation = { resendRecoveryEmail: { status: Resen
|
||||
|
||||
export type RecoverPassword_UserRecoveryTicketFragment = { username: string, email: string } & { ' $fragmentName'?: 'RecoverPassword_UserRecoveryTicketFragment' };
|
||||
|
||||
export type RecoverPassword_SiteConfigFragment = { ' $fragmentRefs'?: { 'PasswordCreationDoubleInput_SiteConfigFragment': PasswordCreationDoubleInput_SiteConfigFragment } } & { ' $fragmentName'?: 'RecoverPassword_SiteConfigFragment' };
|
||||
export type RecoverPassword_SiteConfigFragment = { minimumPasswordComplexity: number } & { ' $fragmentName'?: 'RecoverPassword_SiteConfigFragment' };
|
||||
|
||||
export type PasswordRecoveryQueryVariables = Exact<{
|
||||
ticket: string;
|
||||
@@ -914,20 +912,11 @@ export const RecoverPassword_UserRecoveryTicketFragmentDoc = new TypedDocumentSt
|
||||
email
|
||||
}
|
||||
`, {"fragmentName":"RecoverPassword_userRecoveryTicket"}) as unknown as TypedDocumentString<RecoverPassword_UserRecoveryTicketFragment, unknown>;
|
||||
export const PasswordCreationDoubleInput_SiteConfigFragmentDoc = new TypedDocumentString(`
|
||||
fragment PasswordCreationDoubleInput_siteConfig on SiteConfig {
|
||||
id
|
||||
minimumPasswordComplexity
|
||||
}
|
||||
`, {"fragmentName":"PasswordCreationDoubleInput_siteConfig"}) as unknown as TypedDocumentString<PasswordCreationDoubleInput_SiteConfigFragment, unknown>;
|
||||
export const RecoverPassword_SiteConfigFragmentDoc = new TypedDocumentString(`
|
||||
fragment RecoverPassword_siteConfig on SiteConfig {
|
||||
...PasswordCreationDoubleInput_siteConfig
|
||||
}
|
||||
fragment PasswordCreationDoubleInput_siteConfig on SiteConfig {
|
||||
id
|
||||
minimumPasswordComplexity
|
||||
}`, {"fragmentName":"RecoverPassword_siteConfig"}) as unknown as TypedDocumentString<RecoverPassword_SiteConfigFragment, unknown>;
|
||||
}
|
||||
`, {"fragmentName":"RecoverPassword_siteConfig"}) as unknown as TypedDocumentString<RecoverPassword_SiteConfigFragment, unknown>;
|
||||
export const DeactivateUserDocument = new TypedDocumentString(`
|
||||
mutation DeactivateUser($hsErase: Boolean!, $password: String) {
|
||||
deactivateUser(input: {hsErase: $hsErase, password: $password}) {
|
||||
@@ -1404,13 +1393,10 @@ export const PasswordChangeDocument = new TypedDocumentString(`
|
||||
}
|
||||
}
|
||||
siteConfig {
|
||||
...PasswordCreationDoubleInput_siteConfig
|
||||
minimumPasswordComplexity
|
||||
}
|
||||
}
|
||||
fragment PasswordCreationDoubleInput_siteConfig on SiteConfig {
|
||||
id
|
||||
minimumPasswordComplexity
|
||||
}`) as unknown as TypedDocumentString<PasswordChangeQuery, PasswordChangeQueryVariables>;
|
||||
`) as unknown as TypedDocumentString<PasswordChangeQuery, PasswordChangeQueryVariables>;
|
||||
export const RecoverPasswordDocument = new TypedDocumentString(`
|
||||
mutation RecoverPassword($ticket: String!, $newPassword: String!) {
|
||||
setPasswordByRecovery(input: {ticket: $ticket, newPassword: $newPassword}) {
|
||||
@@ -1436,16 +1422,12 @@ export const PasswordRecoveryDocument = new TypedDocumentString(`
|
||||
...RecoverPassword_userRecoveryTicket
|
||||
}
|
||||
}
|
||||
fragment PasswordCreationDoubleInput_siteConfig on SiteConfig {
|
||||
id
|
||||
minimumPasswordComplexity
|
||||
}
|
||||
fragment RecoverPassword_userRecoveryTicket on UserRecoveryTicket {
|
||||
fragment RecoverPassword_userRecoveryTicket on UserRecoveryTicket {
|
||||
username
|
||||
email
|
||||
}
|
||||
fragment RecoverPassword_siteConfig on SiteConfig {
|
||||
...PasswordCreationDoubleInput_siteConfig
|
||||
minimumPasswordComplexity
|
||||
}`) as unknown as TypedDocumentString<PasswordRecoveryQuery, PasswordRecoveryQueryVariables>;
|
||||
export const AllowCrossSigningResetDocument = new TypedDocumentString(`
|
||||
mutation AllowCrossSigningReset($userId: ID!) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Copyright 2025, 2026 Element Creations Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
@@ -52,7 +53,7 @@ const QUERY = graphql(/* GraphQL */ `
|
||||
}
|
||||
|
||||
siteConfig {
|
||||
...PasswordCreationDoubleInput_siteConfig
|
||||
minimumPasswordComplexity
|
||||
}
|
||||
}
|
||||
`);
|
||||
@@ -183,7 +184,7 @@ function ChangePassword(): React.ReactNode {
|
||||
<Separator />
|
||||
|
||||
<PasswordCreationDoubleInput
|
||||
siteConfig={siteConfig}
|
||||
minimumPasswordComplexity={siteConfig.minimumPasswordComplexity}
|
||||
forceShowNewPasswordInvalid={
|
||||
(mutation.data &&
|
||||
mutation.data.status === "INVALID_NEW_PASSWORD") ||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Copyright 2025, 2026 Element Creations Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
@@ -58,7 +59,7 @@ const FRAGMENT = graphql(/* GraphQL */ `
|
||||
|
||||
const SITE_CONFIG_FRAGMENT = graphql(/* GraphQL */ `
|
||||
fragment RecoverPassword_siteConfig on SiteConfig {
|
||||
...PasswordCreationDoubleInput_siteConfig
|
||||
minimumPasswordComplexity
|
||||
}
|
||||
`);
|
||||
|
||||
@@ -292,7 +293,7 @@ const EmailRecovery: React.FC<{
|
||||
/>
|
||||
|
||||
<PasswordCreationDoubleInput
|
||||
siteConfig={siteConfig}
|
||||
minimumPasswordComplexity={siteConfig.minimumPasswordComplexity}
|
||||
forceShowNewPasswordInvalid={
|
||||
mutation.data?.status === "INVALID_NEW_PASSWORD" || false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// Copyright 2026 Element Creations Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import { useDeferredValue, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import type { PasswordComplexity } from "./password_complexity";
|
||||
|
||||
// This will load the password complexity module lazily,
|
||||
// so that it doesn't block the initial render and can be code-split
|
||||
const loadPromise = import("./password_complexity").then(
|
||||
({ estimatePasswordComplexity }) => estimatePasswordComplexity,
|
||||
);
|
||||
|
||||
/** Score the given password, off the critical path and off the main thread. */
|
||||
export const usePasswordComplexity = (password: string): PasswordComplexity => {
|
||||
const { t } = useTranslation();
|
||||
const [result, setResult] = useState<PasswordComplexity>({
|
||||
score: 0,
|
||||
scoreText: t("frontend.password_strength.placeholder"),
|
||||
improvementsText: [],
|
||||
});
|
||||
const deferredPassword = useDeferredValue(password);
|
||||
|
||||
useEffect(() => {
|
||||
if (deferredPassword === "") {
|
||||
setResult({
|
||||
score: 0,
|
||||
scoreText: t("frontend.password_strength.placeholder"),
|
||||
improvementsText: [],
|
||||
});
|
||||
} else {
|
||||
loadPromise
|
||||
.then((estimatePasswordComplexity) =>
|
||||
estimatePasswordComplexity(deferredPassword, t),
|
||||
)
|
||||
.then((response) => setResult(response));
|
||||
}
|
||||
}, [deferredPassword, t]);
|
||||
|
||||
return result;
|
||||
};
|
||||
Reference in new issue
Block a user