You've already forked authentication-service
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:
164
frontend/src/components/PasswordCreationDoubleInput.tsx
Normal file
164
frontend/src/components/PasswordCreationDoubleInput.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -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.
|
||||
*/
|
||||
|
@@ -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>;
|
@@ -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 />}
|
||||
|
@@ -26,8 +26,7 @@ const QUERY = graphql(/* GraphQL */ `
|
||||
}
|
||||
|
||||
siteConfig {
|
||||
id
|
||||
minimumPasswordComplexity
|
||||
...PasswordCreationDoubleInput_siteConfig
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
Reference in New Issue
Block a user