Skip to content

Update ChangePassword / PassphraseField to use Compound #29490

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions res/css/_common.pcss
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2024, 2025 New Vector Ltd.
Copyright 2019-2023 The Matrix.org Foundation C.I.C
Copyright 2017-2019 New Vector Ltd
Copyright 2017 Vector Creations Ltd
Expand Down Expand Up @@ -217,7 +217,7 @@ textarea {
}

input[type="text"]:focus,
input[type="password"]:focus,
:not(.mx_ChangePasswordForm input) > input[type="password"],
textarea:focus {
outline: none;
box-shadow: none;
Expand Down Expand Up @@ -592,6 +592,7 @@ legend {
*/
.mx_Dialog
button:not(
.mx_ChangePasswordForm button,
.mx_EncryptionUserSettingsTab button,
.mx_UserProfileSettings button,
.mx_ShareDialog button,
Expand Down Expand Up @@ -620,6 +621,7 @@ legend {

.mx_Dialog
button:not(
.mx_ChangePasswordForm button,
.mx_Dialog_nonDialogButton,
[class|="maplibregl"],
.mx_AccessibleButton,
Expand All @@ -634,6 +636,7 @@ legend {

.mx_Dialog
button:not(
.mx_ChangePasswordForm button,
.mx_Dialog_nonDialogButton,
[class|="maplibregl"],
.mx_AccessibleButton,
Expand All @@ -653,6 +656,7 @@ legend {
.mx_Dialog input[type="submit"].mx_Dialog_primary,
.mx_Dialog_buttons
button:not(
.mx_ChangePasswordForm button,
.mx_Dialog_nonDialogButton,
.mx_AccessibleButton,
.mx_UserProfileSettings button,
Expand All @@ -672,6 +676,7 @@ legend {
.mx_Dialog input[type="submit"].danger,
.mx_Dialog_buttons
button.danger:not(
.mx_ChangePasswordForm button,
.mx_Dialog_nonDialogButton,
.mx_AccessibleButton,
.mx_UserProfileSettings button,
Expand All @@ -694,6 +699,7 @@ legend {

.mx_Dialog
button:not(
.mx_ChangePasswordForm button,
.mx_Dialog_nonDialogButton,
[class|="maplibregl"],
.mx_AccessibleButton,
Expand Down
101 changes: 52 additions & 49 deletions src/components/views/auth/PassphraseField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,78 +6,88 @@
Please see LICENSE files in the repository root for full details.
*/

import React, { type ComponentProps, PureComponent, type RefCallback, type RefObject } from "react";
import React, { type RefCallback, type RefObject, useCallback, useMemo, useState } from "react";
import classNames from "classnames";

import type { ZxcvbnResult } from "@zxcvbn-ts/core";
import type { Score, ZxcvbnResult } from "@zxcvbn-ts/core";
import SdkConfig from "../../../SdkConfig";
import withValidation, { type IFieldState, type IValidationResult } from "../elements/Validation";
import withValidation, { type IValidationResult } from "../elements/Validation";
import { _t, _td, type TranslationKey } from "../../../languageHandler";
import Field, { type IInputProps } from "../elements/Field";
import { type IInputProps } from "../elements/Field";
import { MatrixClientPeg } from "../../../MatrixClientPeg";

Check failure on line 17 in src/components/views/auth/PassphraseField.tsx

View workflow job for this annotation

GitHub Actions / ESLint

There should be at least one empty line between import groups
import { Field, Label, PasswordInput, Progress } from "@vector-im/compound-web";

Check failure on line 18 in src/components/views/auth/PassphraseField.tsx

View workflow job for this annotation

GitHub Actions / ESLint

`@vector-im/compound-web` import should occur before type import of `@zxcvbn-ts/core`

const SCORE_TINT: Record<Score, "red" | "orange" | "lime" | "green"> ={
"0": "red",
"1": "red",
"2": "orange",
"3": "lime",
"4": "green"
};

interface IProps extends Omit<IInputProps, "onValidate" | "element"> {
autoFocus?: boolean;
id?: string;
className?: string;
minScore: 0 | 1 | 2 | 3 | 4;
value: string;
fieldRef?: RefCallback<Field> | RefObject<Field>;
fieldRef?: RefCallback<HTMLInputElement> | RefObject<HTMLInputElement>;
// Additional strings such as a username used to catch bad passwords
userInputs?: string[];

label: TranslationKey;
labelEnterPassword: TranslationKey;
labelStrongPassword: TranslationKey;
labelAllowedButUnsafe: TranslationKey;
tooltipAlignment?: ComponentProps<typeof Field>["tooltipAlignment"];
labelEnterPassword?: TranslationKey;
labelStrongPassword?: TranslationKey;
labelAllowedButUnsafe?: TranslationKey;
// tooltipAlignment?: ComponentProps<typeof Field>["tooltipAlignment"];

onChange(ev: React.FormEvent<HTMLElement>): void;
onValidate?(result: IValidationResult): void;
}

class PassphraseField extends PureComponent<IProps> {
public static defaultProps = {
label: _td("common|password"),
labelEnterPassword: _td("auth|password_field_label"),
labelStrongPassword: _td("auth|password_field_strong_label"),
labelAllowedButUnsafe: _td("auth|password_field_weak_label"),
};
const DEFAULT_PROPS = {
label: _td("common|password"),
labelEnterPassword: _td("auth|password_field_label"),
labelStrongPassword: _td("auth|password_field_strong_label"),
labelAllowedButUnsafe: _td("auth|password_field_weak_label"),
};

public readonly validate = withValidation<this, ZxcvbnResult | null>({
const PassphraseField: React.FC<IProps> = (props) => {
const { labelEnterPassword, userInputs, minScore, label, labelStrongPassword, labelAllowedButUnsafe, className, id, fieldRef, autoFocus, onChange, onValidate} = {...DEFAULT_PROPS, ...props};
const validateFn = useMemo(() => withValidation<{}, ZxcvbnResult | null>({
description: function (complexity) {
const score = complexity ? complexity.score : 0;
return <progress className="mx_PassphraseField_progress" max={4} value={score} />;
return <Progress tint={SCORE_TINT[score]} size="sm" value={score} max={4} />
},
deriveData: async ({ value }): Promise<ZxcvbnResult | null> => {
if (!value) return null;
const { scorePassword } = await import("../../../utils/PasswordScorer");
return scorePassword(MatrixClientPeg.get(), value, this.props.userInputs);
return scorePassword(MatrixClientPeg.get(), value, userInputs);
},
rules: [
{
key: "required",
test: ({ value, allowEmpty }) => allowEmpty || !!value,
invalid: () => _t(this.props.labelEnterPassword),
invalid: () => _t(labelEnterPassword),
},
{
key: "complexity",
test: async function ({ value }, complexity): Promise<boolean> {
if (!value || !complexity) {
return false;
}
const safe = complexity.score >= this.props.minScore;
const safe = complexity.score >= minScore;
const allowUnsafe = SdkConfig.get("dangerously_allow_unsafe_and_insecure_passwords");
return allowUnsafe || safe;
},
valid: function (complexity) {
// Unsafe passwords that are valid are only possible through a
// configuration flag. We'll print some helper text to signal
// to the user that their password is allowed, but unsafe.
if (complexity && complexity.score >= this.props.minScore) {
return _t(this.props.labelStrongPassword);
if (complexity && complexity.score >= minScore) {
return _t(labelStrongPassword);
}
return _t(this.props.labelAllowedButUnsafe);
return _t(labelAllowedButUnsafe);
},
invalid: function (complexity) {
if (!complexity) {
Expand All @@ -89,33 +99,26 @@
},
],
memoize: true,
});
}), [labelEnterPassword, userInputs, minScore, labelStrongPassword, labelAllowedButUnsafe]);
const [feedback, setFeedback]= useState<string|JSX.Element>();

const onInputChange = useCallback<React.ChangeEventHandler<HTMLInputElement>>((ev) => {
onChange(ev);
validateFn({
value: ev.target.value,
focused: true,
}).then((v) => {
setFeedback(v.feedback);
onValidate?.(v);
});
}, [validateFn]);

public onValidate = async (fieldState: IFieldState): Promise<IValidationResult> => {
const result = await this.validate(fieldState);
if (this.props.onValidate) {
this.props.onValidate(result);
}
return result;
};

public render(): React.ReactNode {
return (
<Field
id={this.props.id}
autoFocus={this.props.autoFocus}
className={classNames("mx_PassphraseField", this.props.className)}
ref={this.props.fieldRef}
type="password"
autoComplete="new-password"
label={_t(this.props.label)}
value={this.props.value}
onChange={this.props.onChange}
onValidate={this.onValidate}
tooltipAlignment={this.props.tooltipAlignment}
/>
);
}
return <Field id={id} name="password" className={classNames("mx_PassphraseField", className)}>
<Label>{_t(label)}</Label>
<PasswordInput ref={fieldRef} autoFocus={autoFocus} onChange={onInputChange} />
{feedback}
</Field>
}

export default PassphraseField;
Loading
Loading