Skip to content

Commit 0b346d3

Browse files
committed
feat(password): apply backend logic to all password set fields
1 parent 15e9509 commit 0b346d3

File tree

9 files changed

+70
-23
lines changed

9 files changed

+70
-23
lines changed

coderd/userauth.go

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,8 @@ func (api *API) postChangePasswordWithOneTimePasscode(rw http.ResponseWriter, r
446446
// @Router /users/validate-password [post]
447447
func (api *API) validateUserPassword(rw http.ResponseWriter, r *http.Request) {
448448
var (
449-
ctx = r.Context()
449+
ctx = r.Context()
450+
valid = true
450451
)
451452

452453
var req codersdk.ValidateUserPasswordRequest
@@ -456,14 +457,11 @@ func (api *API) validateUserPassword(rw http.ResponseWriter, r *http.Request) {
456457

457458
err := userpassword.Validate(req.Password)
458459
if err != nil {
459-
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.ValidateUserPasswordResponse{
460-
Valid: false,
461-
})
462-
return
460+
valid = false
463461
}
464462

465463
httpapi.Write(ctx, rw, http.StatusOK, codersdk.ValidateUserPasswordResponse{
466-
Valid: true,
464+
Valid: valid,
467465
})
468466
}
469467

site/src/api/api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1326,7 +1326,7 @@ class ApiMethods {
13261326
password: string,
13271327
): Promise<boolean> => {
13281328
const response = await this.axios.post("/api/v2/users/validate-password", { password });
1329-
return response.data.isValid;
1329+
return response.data.valid;
13301330
};
13311331

13321332
getRoles = async (): Promise<Array<TypesGen.AssignableRoles>> => {

site/src/pages/CreateUserPage/CreateUserForm.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { FormFooter } from "components/FormFooter/FormFooter";
88
import { FullPageForm } from "components/FullPageForm/FullPageForm";
99
import { Stack } from "components/Stack/Stack";
1010
import { type FormikContextType, useFormik } from "formik";
11-
import type { FC } from "react";
11+
import { type FC, useEffect } from "react";
1212
import {
1313
displayNameValidator,
1414
getFormHelpers,
@@ -63,6 +63,8 @@ export const authMethodLanguage = {
6363
export interface CreateUserFormProps {
6464
onSubmit: (user: TypesGen.CreateUserRequestWithOrgs) => void;
6565
onCancel: () => void;
66+
onPasswordChange: (password: string) => void;
67+
passwordIsValid: boolean;
6668
error?: unknown;
6769
isLoading: boolean;
6870
authMethods?: TypesGen.AuthMethods;
@@ -85,7 +87,7 @@ const validationSchema = Yup.object({
8587

8688
export const CreateUserForm: FC<
8789
React.PropsWithChildren<CreateUserFormProps>
88-
> = ({ onSubmit, onCancel, error, isLoading, authMethods }) => {
90+
> = ({ onSubmit, onCancel, onPasswordChange, passwordIsValid, error, isLoading, authMethods }) => {
8991
const form: FormikContextType<TypesGen.CreateUserRequestWithOrgs> =
9092
useFormik<TypesGen.CreateUserRequestWithOrgs>({
9193
initialValues: {
@@ -104,6 +106,11 @@ export const CreateUserForm: FC<
104106
error,
105107
);
106108

109+
useEffect(() => {
110+
onPasswordChange?.(form.values.password);
111+
}, [form.values.password]); // Run effect when password changes
112+
113+
107114
const methods = [
108115
authMethods?.password.enabled && "password",
109116
authMethods?.oidc.enabled && "oidc",
@@ -189,8 +196,10 @@ export const CreateUserForm: FC<
189196
<TextField
190197
{...getFieldHelpers("password", {
191198
helperText:
192-
form.values.login_type !== "password" &&
193-
"No password required for this login type",
199+
(form.values.login_type !== "password" &&
200+
"No password required for this login type") ||
201+
(!passwordIsValid && "password is not strong.")
202+
194203
})}
195204
autoComplete="current-password"
196205
fullWidth

site/src/pages/CreateUserPage/CreateUserPage.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import { authMethods, createUser } from "api/queries/users";
1+
import { authMethods, createUser, validatePassword } from "api/queries/users";
22
import { displaySuccess } from "components/GlobalSnackbar/utils";
33
import { Margins } from "components/Margins/Margins";
4-
import type { FC } from "react";
4+
import { type FC, useState } from "react";
55
import { Helmet } from "react-helmet-async";
66
import { useMutation, useQuery, useQueryClient } from "react-query";
77
import { useNavigate } from "react-router-dom";
88
import { pageTitle } from "utils/page";
99
import { CreateUserForm } from "./CreateUserForm";
10+
import { useDebouncedFunction } from "hooks/debounce";
1011

1112
export const Language = {
1213
unknownError: "Oops, an unknown error occurred.",
@@ -17,6 +18,19 @@ export const CreateUserPage: FC = () => {
1718
const queryClient = useQueryClient();
1819
const createUserMutation = useMutation(createUser(queryClient));
1920
const authMethodsQuery = useQuery(authMethods());
21+
const validatePasswordMutation = useMutation(validatePassword());
22+
23+
const [passwordIsValid, setPasswordIsValid] = useState(false);
24+
25+
const validateUserPassword = async (password: string) => {
26+
validatePasswordMutation.mutate(password, {
27+
onSuccess: (data) => {
28+
setPasswordIsValid(data);
29+
},
30+
})
31+
};
32+
33+
const { debounced: debouncedValidateUserPassword } = useDebouncedFunction(validateUserPassword, 500);
2034

2135
return (
2236
<Margins>
@@ -35,6 +49,8 @@ export const CreateUserPage: FC = () => {
3549
onCancel={() => {
3650
navigate("..", { relative: "path" });
3751
}}
52+
onPasswordChange={debouncedValidateUserPassword}
53+
passwordIsValid={passwordIsValid}
3854
isLoading={createUserMutation.isLoading}
3955
/>
4056
</Margins>

site/src/pages/SetupPage/SetupPage.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { useAuthContext } from "contexts/auth/AuthProvider";
66
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
77
import { type FC, useEffect, useState } from "react";
88
import { Helmet } from "react-helmet-async";
9-
import { useMutation, useQuery, useQueryClient } from "react-query";
9+
import { useMutation, useQuery } from "react-query";
1010
import { Navigate, useNavigate } from "react-router-dom";
1111
import { pageTitle } from "utils/page";
1212
import { sendDeploymentEvent } from "utils/telemetry";
@@ -27,7 +27,6 @@ export const SetupPage: FC = () => {
2727
const setupIsComplete = !isConfiguringTheFirstUser;
2828
const { metadata } = useEmbeddedMetadata();
2929
const buildInfoQuery = useQuery(buildInfo(metadata["build-info"]));
30-
const queryClient = useQueryClient();
3130
const navigate = useNavigate();
3231

3332
const [passwordIsValid, setPasswordIsValid] = useState(false);
@@ -56,7 +55,7 @@ export const SetupPage: FC = () => {
5655
}
5756

5857
const validateUserPassword = async (password: string) => {
59-
const isValid = validatePasswordMutation.mutate(password, {
58+
validatePasswordMutation.mutate(password, {
6059
onSuccess: (data) => {
6160
setPasswordIsValid(data);
6261
},

site/src/pages/SetupPage/SetupPageView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ export const SetupPageView: FC<SetupPageViewProps> = ({
183183
id="password"
184184
label={Language.passwordLabel}
185185
type="password"
186-
helperText={!passwordIsValid ? "Password is not strong enough." : ""} // Provide feedback
186+
helperText={!passwordIsValid ? "Password is not strong." : ""} // Provide feedback
187187
/>
188188
<label
189189
htmlFor="trial"

site/src/pages/UserSettingsPage/SecurityPage/SecurityForm.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { type FormikContextType, useFormik } from "formik";
77
import type { FC } from "react";
88
import { getFormHelpers } from "utils/formUtils";
99
import * as Yup from "yup";
10+
import { useEffect } from "react";
1011

1112
interface SecurityFormValues {
1213
old_password: string;
@@ -30,10 +31,7 @@ export const Language = {
3031
const validationSchema = Yup.object({
3132
old_password: Yup.string().trim().required(Language.oldPasswordRequired),
3233
password: Yup.string()
33-
.trim()
34-
.min(8, Language.passwordMinLength)
35-
.max(64, Language.passwordMaxLength)
36-
.required(Language.newPasswordRequired),
34+
.trim().required(Language.newPasswordRequired),
3735
confirm_password: Yup.string()
3836
.trim()
3937
.test("passwords-match", Language.confirmPasswordMatch, function (value) {
@@ -44,13 +42,17 @@ const validationSchema = Yup.object({
4442
export interface SecurityFormProps {
4543
disabled: boolean;
4644
isLoading: boolean;
45+
onPasswordChange: (password: string) => void;
46+
passwordIsValid: boolean,
4747
onSubmit: (values: SecurityFormValues) => void;
4848
error?: unknown;
4949
}
5050

5151
export const SecurityForm: FC<SecurityFormProps> = ({
5252
disabled,
5353
isLoading,
54+
onPasswordChange,
55+
passwordIsValid,
5456
onSubmit,
5557
error,
5658
}) => {
@@ -74,6 +76,10 @@ export const SecurityForm: FC<SecurityFormProps> = ({
7476
);
7577
}
7678

79+
useEffect(() => {
80+
onPasswordChange(form.values.password);
81+
}, [form.values.password]);
82+
7783
return (
7884
<>
7985
<Form onSubmit={form.handleSubmit}>
@@ -91,6 +97,7 @@ export const SecurityForm: FC<SecurityFormProps> = ({
9197
autoComplete="password"
9298
fullWidth
9399
label={Language.newPasswordLabel}
100+
helperText={!passwordIsValid ? "Password is not strong." : ""} // Provide feedback
94101
type="password"
95102
/>
96103
<TextField

site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,42 @@
11
import { API } from "api/api";
2-
import { authMethods, updatePassword } from "api/queries/users";
2+
import { authMethods, updatePassword, validatePassword } from "api/queries/users";
33
import { displaySuccess } from "components/GlobalSnackbar/utils";
44
import { Loader } from "components/Loader/Loader";
55
import { Stack } from "components/Stack/Stack";
66
import { useAuthenticated } from "contexts/auth/RequireAuth";
7-
import type { ComponentProps, FC } from "react";
7+
import { type ComponentProps, FC, useState } from "react";
88
import { useMutation, useQuery } from "react-query";
99
import { Section } from "../Section";
1010
import { SecurityForm } from "./SecurityForm";
1111
import {
1212
SingleSignOnSection,
1313
useSingleSignOnSection,
1414
} from "./SingleSignOnSection";
15+
import { useDebouncedFunction } from "hooks/debounce";
1516

1617
export const SecurityPage: FC = () => {
1718
const { user: me } = useAuthenticated();
1819
const updatePasswordMutation = useMutation(updatePassword());
20+
const validatePasswordMutation = useMutation(validatePassword());
1921
const authMethodsQuery = useQuery(authMethods());
2022
const { data: userLoginType } = useQuery({
2123
queryKey: ["loginType"],
2224
queryFn: API.getUserLoginType,
2325
});
2426
const singleSignOnSection = useSingleSignOnSection();
2527

28+
const [passwordIsValid, setPasswordIsValid] = useState(false);
29+
30+
const validateUserPassword = async (password: string) => {
31+
validatePasswordMutation.mutate(password, {
32+
onSuccess: (data) => {
33+
setPasswordIsValid(data);
34+
},
35+
})
36+
};
37+
38+
const { debounced: debouncedValidateUserPassword } = useDebouncedFunction(validateUserPassword, 500);
39+
2640
if (!authMethodsQuery.data || !userLoginType) {
2741
return <Loader />;
2842
}
@@ -34,6 +48,8 @@ export const SecurityPage: FC = () => {
3448
disabled: userLoginType.login_type !== "password",
3549
error: updatePasswordMutation.error,
3650
isLoading: updatePasswordMutation.isLoading,
51+
onPasswordChange: debouncedValidateUserPassword,
52+
passwordIsValid: passwordIsValid,
3753
onSubmit: async (data) => {
3854
await updatePasswordMutation.mutateAsync({
3955
userId: me.id,

site/src/pages/UserSettingsPage/SecurityPage/SecurityPageView.stories.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ const defaultArgs: ComponentProps<typeof SecurityPageView> = {
1515
error: undefined,
1616
isLoading: false,
1717
onSubmit: action("onSubmit"),
18+
onPasswordChange: (password: string) => {},
19+
passwordIsValid: false,
1820
},
1921
},
2022
oidc: {

0 commit comments

Comments
 (0)