1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-08-09 04:22:45 +03:00

frontend: refactor password change form to extract double-input password creation inputs as new component (#2994)

This commit is contained in:
reivilibre
2024-07-24 16:22:40 +01:00
committed by GitHub
parent 8147016735
commit 98aa3a8dbc
5 changed files with 195 additions and 140 deletions

View File

@@ -0,0 +1,164 @@
// 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 { Form, Progress } from "@vector-im/compound-web";
import { useDeferredValue, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { FragmentType, graphql, useFragment } from "../gql";
import {
PasswordComplexity,
estimatePasswordComplexity,
} from "../utils/password_complexity";
const CONFIG_FRAGMENT = graphql(/* GraphQL */ `
fragment PasswordCreationDoubleInput_siteConfig on SiteConfig {
id
minimumPasswordComplexity
}
`);
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 {
estimatePasswordComplexity(deferredPassword, t).then((response) =>
setResult(response),
);
}
}, [deferredPassword, t]);
return result;
};
export default function PasswordCreationDoubleInput({
siteConfig,
forceShowNewPasswordInvalid,
}: {
siteConfig: FragmentType<typeof CONFIG_FRAGMENT>;
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;
if (newPassword === "") {
passwordStrengthTint = undefined;
} else {
passwordStrengthTint = ["red", "red", "orange", "lime", "green"][
passwordComplexity.score
] as "red" | "orange" | "lime" | "green" | undefined;
}
return (
<>
<Form.Field name="new_password">
<Form.Label>
{t("frontend.password_change.new_password_label")}
</Form.Label>
<Form.PasswordControl
required
autoComplete="new-password"
ref={newPasswordRef}
onBlur={() =>
newPasswordAgainRef.current!.value &&
newPasswordAgainRef.current!.reportValidity()
}
onChange={(e) => setNewPassword(e.target.value)}
/>
<Progress
size="sm"
getValueLabel={() => passwordComplexity.scoreText}
tint={passwordStrengthTint}
max={4}
value={passwordComplexity.score}
/>
{passwordComplexity.improvementsText.map((suggestion) => (
<Form.HelpMessage>{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>
{forceShowNewPasswordInvalid && (
<Form.ErrorMessage>
{t(
"frontend.password_change.failure.description.invalid_new_password",
)}
</Form.ErrorMessage>
)}
</Form.Field>
<Form.Field name="new_password_again">
{/*
TODO This field has validation defects,
some caused by Radix-UI upstream bugs.
https://github.com/matrix-org/matrix-authentication-service/issues/2855
*/}
<Form.Label>
{t("frontend.password_change.new_password_again_label")}
</Form.Label>
<Form.PasswordControl
required
ref={newPasswordAgainRef}
autoComplete="new-password"
/>
<Form.ErrorMessage match="valueMissing">
{t("frontend.errors.field_required")}
</Form.ErrorMessage>
<Form.ErrorMessage match={(v, form) => v !== form.get("new_password")}>
{t("frontend.password_change.passwords_no_match")}
</Form.ErrorMessage>
<Form.SuccessMessage match="valid">
{t("frontend.password_change.passwords_match")}
</Form.SuccessMessage>
</Form.Field>
</>
);
}

View File

@@ -23,6 +23,7 @@ const documents = {
"\n query FooterQuery {\n siteConfig {\n id\n ...Footer_siteConfig\n }\n }\n": types.FooterQueryDocument,
"\n fragment OAuth2Session_session on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\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 mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n ...OAuth2Session_session\n }\n }\n }\n": types.EndOAuth2SessionDocument,
"\n fragment PasswordCreationDoubleInput_siteConfig on SiteConfig {\n id\n minimumPasswordComplexity\n }\n": types.PasswordCreationDoubleInput_SiteConfigFragmentDoc,
"\n fragment BrowserSession_detail on BrowserSession {\n id\n createdAt\n finishedAt\n userAgent {\n name\n model\n os\n }\n lastActiveIp\n lastActiveAt\n lastAuthentication {\n id\n createdAt\n }\n user {\n id\n username\n }\n }\n": types.BrowserSession_DetailFragmentDoc,
"\n fragment CompatSession_detail on CompatSession {\n id\n createdAt\n deviceId\n finishedAt\n lastActiveIp\n lastActiveAt\n userAgent {\n name\n os\n model\n }\n ssoLogin {\n id\n redirectUri\n }\n }\n": types.CompatSession_DetailFragmentDoc,
"\n fragment OAuth2Session_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n": types.OAuth2Session_DetailFragmentDoc,
@@ -52,7 +53,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 query PasswordChangeQuery {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n\n siteConfig {\n id\n minimumPasswordComplexity\n }\n }\n": types.PasswordChangeQueryDocument,
"\n query PasswordChangeQuery {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n\n siteConfig {\n ...PasswordCreationDoubleInput_siteConfig\n }\n }\n": types.PasswordChangeQueryDocument,
"\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,
};
@@ -111,6 +112,10 @@ export function graphql(source: "\n fragment OAuth2Session_session on Oauth2Ses
* 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 EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n ...OAuth2Session_session\n }\n }\n }\n"): (typeof documents)["\n mutation EndOAuth2Session($id: ID!) {\n endOauth2Session(input: { oauth2SessionId: $id }) {\n status\n oauth2Session {\n id\n ...OAuth2Session_session\n }\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 fragment PasswordCreationDoubleInput_siteConfig on SiteConfig {\n id\n minimumPasswordComplexity\n }\n"): (typeof documents)["\n fragment PasswordCreationDoubleInput_siteConfig on SiteConfig {\n id\n minimumPasswordComplexity\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -230,7 +235,7 @@ export function graphql(source: "\n query VerifyEmailQuery($id: ID!) {\n use
/**
* 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 PasswordChangeQuery {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n\n siteConfig {\n id\n minimumPasswordComplexity\n }\n }\n"): (typeof documents)["\n query PasswordChangeQuery {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n\n siteConfig {\n id\n minimumPasswordComplexity\n }\n }\n"];
export function graphql(source: "\n query PasswordChangeQuery {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n\n siteConfig {\n ...PasswordCreationDoubleInput_siteConfig\n }\n }\n"): (typeof documents)["\n query PasswordChangeQuery {\n viewer {\n __typename\n ... on Node {\n id\n }\n }\n\n siteConfig {\n ...PasswordCreationDoubleInput_siteConfig\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/

View File

@@ -1467,6 +1467,8 @@ export type EndOAuth2SessionMutation = { __typename?: 'Mutation', endOauth2Sessi
& { ' $fragmentRefs'?: { 'OAuth2Session_SessionFragment': OAuth2Session_SessionFragment } }
) | null } };
export type PasswordCreationDoubleInput_SiteConfigFragment = { __typename?: 'SiteConfig', id: string, minimumPasswordComplexity: number } & { ' $fragmentName'?: 'PasswordCreationDoubleInput_SiteConfigFragment' };
export type BrowserSession_DetailFragment = { __typename?: 'BrowserSession', id: string, createdAt: string, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, model?: string | null, os?: string | null } | null, lastAuthentication?: { __typename?: 'Authentication', id: string, createdAt: string } | null, user: { __typename?: 'User', id: string, username: string } } & { ' $fragmentName'?: 'BrowserSession_DetailFragment' };
export type CompatSession_DetailFragment = { __typename?: 'CompatSession', id: string, createdAt: string, deviceId: string, finishedAt?: string | null, lastActiveIp?: string | null, lastActiveAt?: string | null, userAgent?: { __typename?: 'UserAgent', name?: string | null, os?: string | null, model?: string | null } | null, ssoLogin?: { __typename?: 'CompatSsoLogin', id: string, redirectUri: string } | null } & { ' $fragmentName'?: 'CompatSession_DetailFragment' };
@@ -1678,7 +1680,10 @@ export type VerifyEmailQueryQuery = { __typename?: 'Query', userEmail?: (
export type PasswordChangeQueryQueryVariables = Exact<{ [key: string]: never; }>;
export type PasswordChangeQueryQuery = { __typename?: 'Query', viewer: { __typename: 'Anonymous', id: string } | { __typename: 'User', id: string }, siteConfig: { __typename?: 'SiteConfig', id: string, minimumPasswordComplexity: number } };
export type PasswordChangeQueryQuery = { __typename?: 'Query', viewer: { __typename: 'Anonymous', id: string } | { __typename: 'User', id: string }, siteConfig: (
{ __typename?: 'SiteConfig' }
& { ' $fragmentRefs'?: { 'PasswordCreationDoubleInput_SiteConfigFragment': PasswordCreationDoubleInput_SiteConfigFragment } }
) };
export type ChangePasswordMutationVariables = Exact<{
userId: Scalars['ID']['input'];
@@ -1702,6 +1707,7 @@ export const OAuth2Client_DetailFragmentDoc = {"kind":"Document","definitions":[
export const CompatSession_SessionFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CompatSession_session"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CompatSession"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"deviceId"}},{"kind":"Field","name":{"kind":"Name","value":"finishedAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastActiveIp"}},{"kind":"Field","name":{"kind":"Name","value":"lastActiveAt"}},{"kind":"Field","name":{"kind":"Name","value":"userAgent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"os"}},{"kind":"Field","name":{"kind":"Name","value":"model"}},{"kind":"Field","name":{"kind":"Name","value":"deviceType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"ssoLogin"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"redirectUri"}}]}}]}}]} as unknown as DocumentNode<CompatSession_SessionFragment, unknown>;
export const Footer_SiteConfigFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Footer_siteConfig"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SiteConfig"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"imprint"}},{"kind":"Field","name":{"kind":"Name","value":"tosUri"}},{"kind":"Field","name":{"kind":"Name","value":"policyUri"}}]}}]} as unknown as DocumentNode<Footer_SiteConfigFragment, unknown>;
export const OAuth2Session_SessionFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OAuth2Session_session"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Oauth2Session"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"scope"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"finishedAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastActiveIp"}},{"kind":"Field","name":{"kind":"Name","value":"lastActiveAt"}},{"kind":"Field","name":{"kind":"Name","value":"userAgent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"model"}},{"kind":"Field","name":{"kind":"Name","value":"os"}},{"kind":"Field","name":{"kind":"Name","value":"deviceType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"client"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"clientId"}},{"kind":"Field","name":{"kind":"Name","value":"clientName"}},{"kind":"Field","name":{"kind":"Name","value":"applicationType"}},{"kind":"Field","name":{"kind":"Name","value":"logoUri"}}]}}]}}]} as unknown as DocumentNode<OAuth2Session_SessionFragment, unknown>;
export const PasswordCreationDoubleInput_SiteConfigFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PasswordCreationDoubleInput_siteConfig"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SiteConfig"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"minimumPasswordComplexity"}}]}}]} as unknown as DocumentNode<PasswordCreationDoubleInput_SiteConfigFragment, unknown>;
export const BrowserSession_DetailFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BrowserSession_detail"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"BrowserSession"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"finishedAt"}},{"kind":"Field","name":{"kind":"Name","value":"userAgent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"model"}},{"kind":"Field","name":{"kind":"Name","value":"os"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lastActiveIp"}},{"kind":"Field","name":{"kind":"Name","value":"lastActiveAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastAuthentication"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}}]}}]} as unknown as DocumentNode<BrowserSession_DetailFragment, unknown>;
export const CompatSession_DetailFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"CompatSession_detail"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CompatSession"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"deviceId"}},{"kind":"Field","name":{"kind":"Name","value":"finishedAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastActiveIp"}},{"kind":"Field","name":{"kind":"Name","value":"lastActiveAt"}},{"kind":"Field","name":{"kind":"Name","value":"userAgent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"os"}},{"kind":"Field","name":{"kind":"Name","value":"model"}}]}},{"kind":"Field","name":{"kind":"Name","value":"ssoLogin"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"redirectUri"}}]}}]}}]} as unknown as DocumentNode<CompatSession_DetailFragment, unknown>;
export const OAuth2Session_DetailFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OAuth2Session_detail"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Oauth2Session"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"scope"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"finishedAt"}},{"kind":"Field","name":{"kind":"Name","value":"lastActiveIp"}},{"kind":"Field","name":{"kind":"Name","value":"lastActiveAt"}},{"kind":"Field","name":{"kind":"Name","value":"client"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"clientId"}},{"kind":"Field","name":{"kind":"Name","value":"clientName"}},{"kind":"Field","name":{"kind":"Name","value":"clientUri"}},{"kind":"Field","name":{"kind":"Name","value":"logoUri"}}]}}]}}]} as unknown as DocumentNode<OAuth2Session_DetailFragment, unknown>;
@@ -1735,6 +1741,6 @@ export const OAuth2ClientQueryDocument = {"kind":"Document","definitions":[{"kin
export const CurrentViewerQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CurrentViewerQuery"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"viewer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Node"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode<CurrentViewerQueryQuery, CurrentViewerQueryQueryVariables>;
export const DeviceRedirectQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"DeviceRedirectQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"deviceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"session"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"deviceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"deviceId"}}},{"kind":"Argument","name":{"kind":"Name","value":"userId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Node"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode<DeviceRedirectQueryQuery, DeviceRedirectQueryQueryVariables>;
export const VerifyEmailQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"VerifyEmailQuery"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userEmail"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"UserEmail_verifyEmail"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UserEmail_verifyEmail"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"UserEmail"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}}]}}]} as unknown as DocumentNode<VerifyEmailQueryQuery, VerifyEmailQueryQueryVariables>;
export const PasswordChangeQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PasswordChangeQuery"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"viewer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Node"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"siteConfig"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"minimumPasswordComplexity"}}]}}]}}]} as unknown as DocumentNode<PasswordChangeQueryQuery, PasswordChangeQueryQueryVariables>;
export const PasswordChangeQueryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PasswordChangeQuery"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"viewer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Node"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"siteConfig"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PasswordCreationDoubleInput_siteConfig"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PasswordCreationDoubleInput_siteConfig"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SiteConfig"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"minimumPasswordComplexity"}}]}}]} as unknown as DocumentNode<PasswordChangeQueryQuery, PasswordChangeQueryQueryVariables>;
export const ChangePasswordDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ChangePassword"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"oldPassword"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"newPassword"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setPassword"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"userId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userId"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"currentPassword"},"value":{"kind":"Variable","name":{"kind":"Name","value":"oldPassword"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"newPassword"},"value":{"kind":"Variable","name":{"kind":"Name","value":"newPassword"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]} as unknown as DocumentNode<ChangePasswordMutation, ChangePasswordMutationVariables>;
export const AllowCrossSigningResetDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"AllowCrossSigningReset"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"allowUserCrossSigningReset"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"userId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userId"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode<AllowCrossSigningResetMutation, AllowCrossSigningResetMutationVariables>;

View File

@@ -18,14 +18,8 @@ import {
useRouter,
} from "@tanstack/react-router";
import IconLockSolid from "@vector-im/compound-design-tokens/assets/web/icons/lock-solid";
import { Alert, Form, Progress, Separator } from "@vector-im/compound-web";
import {
FormEvent,
useDeferredValue,
useEffect,
useRef,
useState,
} from "react";
import { Alert, Form, Separator } from "@vector-im/compound-web";
import { FormEvent, useRef } from "react";
import { useTranslation } from "react-i18next";
import { useMutation, useQuery } from "urql";
@@ -34,12 +28,9 @@ import { ButtonLink } from "../components/ButtonLink";
import Layout from "../components/Layout";
import LoadingSpinner from "../components/LoadingSpinner";
import PageHeading from "../components/PageHeading";
import PasswordCreationDoubleInput from "../components/PasswordCreationDoubleInput";
import { graphql } from "../gql";
import { SetPasswordStatus } from "../gql/graphql";
import {
PasswordComplexity,
estimatePasswordComplexity,
} from "../utils/password_complexity";
const QUERY = graphql(/* GraphQL */ `
query PasswordChangeQuery {
@@ -51,8 +42,7 @@ const QUERY = graphql(/* GraphQL */ `
}
siteConfig {
id
minimumPasswordComplexity
...PasswordCreationDoubleInput_siteConfig
}
}
`);
@@ -79,32 +69,6 @@ export const Route = createLazyFileRoute("/password/change/")({
component: ChangePassword,
});
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 {
estimatePasswordComplexity(deferredPassword, t).then((response) =>
setResult(response),
);
}
}, [deferredPassword, t]);
return result;
};
function ChangePassword(): React.ReactNode {
const { t } = useTranslation();
const [queryResult] = useQuery({ query: QUERY });
@@ -112,13 +76,10 @@ function ChangePassword(): React.ReactNode {
if (queryResult.error) throw queryResult.error;
if (queryResult.data?.viewer.__typename !== "User") throw notFound();
const userId = queryResult.data.viewer.id;
const minPasswordComplexity =
queryResult.data.siteConfig.minimumPasswordComplexity;
const siteConfig = queryResult.data?.siteConfig;
if (!siteConfig) throw Error(); // This should never happen
const currentPasswordRef = useRef<HTMLInputElement>(null);
const newPasswordRef = useRef<HTMLInputElement>(null);
const newPasswordAgainRef = useRef<HTMLInputElement>(null);
const [newPassword, setNewPassword] = useState("");
const [result, changePassword] = useMutation(CHANGE_PASSWORD_MUTATION);
@@ -171,16 +132,6 @@ function ChangePassword(): React.ReactNode {
}
})();
const passwordComplexity = usePasswordComplexity(newPassword);
let passwordStrengthTint;
if (newPassword === "") {
passwordStrengthTint = undefined;
} else {
passwordStrengthTint = ["red", "red", "orange", "lime", "green"][
passwordComplexity.score
] as "red" | "orange" | "lime" | "green" | undefined;
}
return (
<Layout>
<BlockList>
@@ -248,85 +199,15 @@ function ChangePassword(): React.ReactNode {
<Separator />
<Form.Field name="new_password">
<Form.Label>
{t("frontend.password_change.new_password_label")}
</Form.Label>
<Form.PasswordControl
required
autoComplete="new-password"
ref={newPasswordRef}
onBlur={() =>
newPasswordAgainRef.current!.value &&
newPasswordAgainRef.current!.reportValidity()
}
onChange={(e) => setNewPassword(e.target.value)}
/>
<Progress
size="sm"
getValueLabel={() => passwordComplexity.scoreText}
tint={passwordStrengthTint}
max={4}
value={passwordComplexity.score}
/>
{passwordComplexity.improvementsText.map((suggestion) => (
<Form.HelpMessage>{suggestion}</Form.HelpMessage>
))}
{passwordComplexity.score < minPasswordComplexity && (
<Form.ErrorMessage match={() => true}>
{t("frontend.password_strength.too_weak")}
</Form.ErrorMessage>
)}
<Form.ErrorMessage match="valueMissing">
{t("frontend.errors.field_required")}
</Form.ErrorMessage>
{result.data &&
result.data.setPassword.status ===
SetPasswordStatus.InvalidNewPassword && (
<Form.ErrorMessage>
{t(
"frontend.password_change.failure.description.invalid_new_password",
)}
</Form.ErrorMessage>
)}
</Form.Field>
<Form.Field name="new_password_again">
{/*
TODO This field has validation defects,
some caused by Radix-UI upstream bugs.
https://github.com/matrix-org/matrix-authentication-service/issues/2855
*/}
<Form.Label>
{t("frontend.password_change.new_password_again_label")}
</Form.Label>
<Form.PasswordControl
required
ref={newPasswordAgainRef}
autoComplete="new-password"
/>
<Form.ErrorMessage match="valueMissing">
{t("frontend.errors.field_required")}
</Form.ErrorMessage>
<Form.ErrorMessage
match={(v, form) => v !== form.get("new_password")}
>
{t("frontend.password_change.passwords_no_match")}
</Form.ErrorMessage>
<Form.SuccessMessage match="valid">
{t("frontend.password_change.passwords_match")}
</Form.SuccessMessage>
</Form.Field>
<PasswordCreationDoubleInput
siteConfig={siteConfig}
forceShowNewPasswordInvalid={
(result.data &&
result.data.setPassword.status ===
SetPasswordStatus.InvalidNewPassword) ||
false
}
/>
<Form.Submit kind="primary" disabled={result.fetching}>
{!!result.fetching && <LoadingSpinner inline />}

View File

@@ -26,8 +26,7 @@ const QUERY = graphql(/* GraphQL */ `
}
siteConfig {
id
minimumPasswordComplexity
...PasswordCreationDoubleInput_siteConfig
}
}
`);