Skip to content

Commit aaa1223

Browse files
1 parent 413928b commit aaa1223

14 files changed

+603
-44
lines changed

coderd/database/migrations/000266_update_forgot_password_notification.down.sql

Whitespace-only changes.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
UPDATE notification_templates
2+
SET
3+
title_template = E'Reset your password for Coder',
4+
body_template = E'Hi {{.UserName}},\n\nUse the link below to reset your password.\n\nIf you did not make this request, you can ignore this message.',
5+
actions = '[{
6+
"label": "Reset password",
7+
"url": "{{ base_url }}/reset-password/change?otp={{.Labels.one_time_passcode}}&email={{ .UserEmail }}"
8+
}]'::jsonb
9+
WHERE
10+
id = '62f86a30-2330-4b61-a26d-311ff3b608cf'

coderd/notifications/testdata/rendered-templates/smtp/TemplateUserRequestedOneTimePasscode.html.golden

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
From: system@coder.com
22
To: bobby@coder.com
3-
Subject: Your One-Time Passcode for Coder.
3+
Subject: Reset your password for Coder
44
Message-Id: 02ee4935-73be-4fa1-a290-ff9999026b13@blush-whale-48
55
Date: Fri, 11 Oct 2024 09:03:06 +0000
66
Content-Type: multipart/alternative; boundary=bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
@@ -12,13 +12,13 @@ Content-Type: text/plain; charset=UTF-8
1212

1313
Hi Bobby,
1414

15-
A request to reset the password for your Coder account has been made. Your =
16-
one-time passcode is:
15+
Use the link below to reset your password.
1716

18-
fad9020b-6562-4cdb-87f1-0486f1bea415
17+
If you did not make this request, you can ignore this message.
1918

20-
If you did not request to reset your password, you can ignore this message.
2119

20+
Reset password: http://test.com/reset-password/change?otp=3Dfad9020b-6562-4=
21+
cdb-87f1-0486f1bea415&email=3Dbobby@coder.com
2222

2323
--bbe61b741255b6098bb6b3c1f41b885773df633cb18d2a3002b68e4bc9c4
2424
Content-Transfer-Encoding: quoted-printable
@@ -30,7 +30,7 @@ Content-Type: text/html; charset=UTF-8
3030
<meta charset=3D"UTF-8" />
3131
<meta name=3D"viewport" content=3D"width=3Ddevice-width, initial-scale=
3232
=3D1.0" />
33-
<title>Your One-Time Passcode for Coder.</title>
33+
<title>Reset your password for Coder</title>
3434
</head>
3535
<body style=3D"margin: 0; padding: 0; font-family: -apple-system, system-=
3636
ui, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarel=
@@ -45,21 +45,24 @@ er Logo" style=3D"height: 40px;" />
4545
</div>
4646
<h1 style=3D"text-align: center; font-size: 24px; font-weight: 400; m=
4747
argin: 8px 0 32px; line-height: 1.5;">
48-
Your One-Time Passcode for Coder.
48+
Reset your password for Coder
4949
</h1>
5050
<div style=3D"line-height: 1.5;">
5151
<p>Hi Bobby,</p>
5252

53-
<p>A request to reset the password for your Coder account has been made. Yo=
54-
ur one-time passcode is:</p>
53+
<p>Use the link below to reset your password.</p>
5554

56-
<p><strong>fad9020b-6562-4cdb-87f1-0486f1bea415</strong></p>
57-
58-
<p>If you did not request to reset your password, you can ignore this messa=
59-
ge.</p>
55+
<p>If you did not make this request, you can ignore this message.</p>
6056
</div>
6157
<div style=3D"text-align: center; margin-top: 32px;">
6258
=20
59+
<a href=3D"http://test.com/reset-password/change?otp=3Dfad9020b-656=
60+
2-4cdb-87f1-0486f1bea415&email=3Dbobby@coder.com" style=3D"display: inline-=
61+
block; padding: 13px 24px; background-color: #020617; color: #f8fafc; text-=
62+
decoration: none; border-radius: 8px; margin: 0 4px;">
63+
Reset password
64+
</a>
65+
=20
6366
</div>
6467
<div style=3D"border-top: 1px solid #e2e8f0; color: #475569; font-siz=
6568
e: 12px; margin-top: 64px; padding-top: 24px; line-height: 1.6;">

coderd/notifications/testdata/rendered-templates/webhook/TemplateUserRequestedOneTimePasscode.json.golden

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,19 @@
99
"user_email": "bobby@coder.com",
1010
"user_name": "Bobby",
1111
"user_username": "bobby",
12-
"actions": [],
12+
"actions": [
13+
{
14+
"label": "Reset password",
15+
"url": "http://test.com/reset-password/change?otp=00000000-0000-0000-0000-000000000000\u0026email=bobby@coder.com"
16+
}
17+
],
1318
"labels": {
1419
"one_time_passcode": "00000000-0000-0000-0000-000000000000"
1520
},
1621
"data": null
1722
},
18-
"title": "Your One-Time Passcode for Coder.",
19-
"title_markdown": "Your One-Time Passcode for Coder.",
20-
"body": "Hi Bobby,\n\nA request to reset the password for your Coder account has been made. Your one-time passcode is:\n\n00000000-0000-0000-0000-000000000000\n\nIf you did not request to reset your password, you can ignore this message.",
21-
"body_markdown": "Hi Bobby,\n\nA request to reset the password for your Coder account has been made. Your one-time passcode is:\n\n**00000000-0000-0000-0000-000000000000**\n\nIf you did not request to reset your password, you can ignore this message."
23+
"title": "Reset your password for Coder",
24+
"title_markdown": "Reset your password for Coder",
25+
"body": "Hi Bobby,\n\nUse the link below to reset your password.\n\nIf you did not make this request, you can ignore this message.",
26+
"body_markdown": "Hi Bobby,\n\nUse the link below to reset your password.\n\nIf you did not make this request, you can ignore this message."
2227
}

site/src/api/api.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2167,6 +2167,18 @@ class ApiMethods {
21672167
);
21682168
return res.data;
21692169
};
2170+
2171+
requestOneTimePassword = async (
2172+
req: TypesGen.RequestOneTimePasscodeRequest,
2173+
) => {
2174+
await this.axios.post<void>("/api/v2/users/otp/request", req);
2175+
};
2176+
2177+
changePasswordWithOTP = async (
2178+
req: TypesGen.ChangePasswordWithOneTimePasscodeRequest,
2179+
) => {
2180+
await this.axios.post<void>("/api/v2/users/otp/change-password", req);
2181+
};
21702182
}
21712183

21722184
// This is a hard coded CSRF token/cookie pair for local development. In prod,

site/src/api/queries/users.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {
33
AuthorizationRequest,
44
GenerateAPIKeyResponse,
55
GetUsersResponse,
6+
RequestOneTimePasscodeRequest,
67
UpdateUserAppearanceSettingsRequest,
78
UpdateUserPasswordRequest,
89
UpdateUserProfileRequest,
@@ -253,3 +254,16 @@ export const updateAppearanceSettings = (
253254
},
254255
};
255256
};
257+
258+
export const requestOneTimePassword = () => {
259+
return {
260+
mutationFn: (req: RequestOneTimePasscodeRequest) =>
261+
API.requestOneTimePassword(req),
262+
};
263+
};
264+
265+
export const changePasswordWithOTP = () => {
266+
return {
267+
mutationFn: API.changePasswordWithOTP,
268+
};
269+
};
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { Interpolation, Theme } from "@emotion/react";
2+
import { CoderIcon } from "components/Icons/CoderIcon";
3+
import type { FC } from "react";
4+
import { getApplicationName, getLogoURL } from "utils/appearance";
5+
6+
/**
7+
* Enterprise customers can set a custom logo for their Coder application. Use
8+
* the custom logo wherever the Coder logo is used, if a custom one is provided.
9+
*/
10+
export const CustomLogo: FC<{ css?: Interpolation<Theme> }> = (props) => {
11+
const applicationName = getApplicationName();
12+
const logoURL = getLogoURL();
13+
14+
return logoURL ? (
15+
<img
16+
{...props}
17+
alt={applicationName}
18+
src={logoURL}
19+
// This prevent browser to display the ugly error icon if the
20+
// image path is wrong or user didn't finish typing the url
21+
onError={(e) => {
22+
e.currentTarget.style.display = "none";
23+
}}
24+
onLoad={(e) => {
25+
e.currentTarget.style.display = "inline";
26+
}}
27+
css={{ maxWidth: 200 }}
28+
className="application-logo"
29+
/>
30+
) : (
31+
<CoderIcon {...props} css={[{ fontSize: 64, fill: "white" }, props.css]} />
32+
);
33+
};

site/src/pages/LoginPage/LoginPageView.tsx

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import type { Interpolation, Theme } from "@emotion/react";
22
import Button from "@mui/material/Button";
33
import type { AuthMethods, BuildInfoResponse } from "api/typesGenerated";
4-
import { CoderIcon } from "components/Icons/CoderIcon";
4+
import { CustomLogo } from "components/CustomLogo/CustomLogo";
55
import { Loader } from "components/Loader/Loader";
66
import { type FC, useState } from "react";
77
import { useLocation } from "react-router-dom";
8-
import { getApplicationName, getLogoURL } from "utils/appearance";
98
import { retrieveRedirect } from "utils/redirect";
109
import { SignInForm } from "./SignInForm";
1110
import { TermsOfServiceLink } from "./TermsOfServiceLink";
@@ -32,37 +31,14 @@ export const LoginPageView: FC<LoginPageViewProps> = ({
3231
// This allows messages to be displayed at the top of the sign in form.
3332
// Helpful for any redirects that want to inform the user of something.
3433
const message = new URLSearchParams(location.search).get("message");
35-
const applicationName = getApplicationName();
36-
const logoURL = getLogoURL();
37-
const applicationLogo = logoURL ? (
38-
<img
39-
alt={applicationName}
40-
src={logoURL}
41-
// This prevent browser to display the ugly error icon if the
42-
// image path is wrong or user didn't finish typing the url
43-
onError={(e) => {
44-
e.currentTarget.style.display = "none";
45-
}}
46-
onLoad={(e) => {
47-
e.currentTarget.style.display = "inline";
48-
}}
49-
css={{
50-
maxWidth: "200px",
51-
}}
52-
className="application-logo"
53-
/>
54-
) : (
55-
<CoderIcon fill="white" opacity={1} css={styles.icon} />
56-
);
57-
5834
const [tosAccepted, setTosAccepted] = useState(false);
5935
const tosAcceptanceRequired =
6036
authMethods?.terms_of_service_url && !tosAccepted;
6137

6238
return (
6339
<div css={styles.root}>
6440
<div css={styles.container}>
65-
{applicationLogo}
41+
<CustomLogo />
6642
{isLoading ? (
6743
<Loader />
6844
) : tosAcceptanceRequired ? (

site/src/pages/LoginPage/PasswordSignInForm.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import LoadingButton from "@mui/lab/LoadingButton";
2+
import Link from "@mui/material/Link";
23
import TextField from "@mui/material/TextField";
34
import { Stack } from "components/Stack/Stack";
45
import { useFormik } from "formik";
56
import type { FC } from "react";
7+
import { Link as RouterLink } from "react-router-dom";
68
import { getFormHelpers, onChangeTrimmed } from "utils/formUtils";
79
import * as Yup from "yup";
810
import { Language } from "./SignInForm";
@@ -65,6 +67,17 @@ export const PasswordSignInForm: FC<PasswordSignInFormProps> = ({
6567
>
6668
{Language.passwordSignIn}
6769
</LoadingButton>
70+
<Link
71+
component={RouterLink}
72+
to="/reset-password"
73+
css={{
74+
fontSize: 12,
75+
fontWeight: 500,
76+
lineHeight: "16px",
77+
}}
78+
>
79+
Forgot password?
80+
</Link>
6881
</Stack>
6982
</form>
7083
);
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { expect, spyOn, userEvent, within } from "@storybook/test";
3+
import { API } from "api/api";
4+
import { mockApiError } from "testHelpers/entities";
5+
import { withGlobalSnackbar } from "testHelpers/storybook";
6+
import ChangePasswordPage from "./ChangePasswordPage";
7+
8+
const meta: Meta<typeof ChangePasswordPage> = {
9+
title: "pages/ResetPasswordPage/ChangePasswordPage",
10+
component: ChangePasswordPage,
11+
args: { redirect: false },
12+
decorators: [withGlobalSnackbar],
13+
};
14+
15+
export default meta;
16+
type Story = StoryObj<typeof ChangePasswordPage>;
17+
18+
export const Default: Story = {};
19+
20+
export const Success: Story = {
21+
play: async ({ canvasElement }) => {
22+
spyOn(API, "changePasswordWithOTP").mockResolvedValueOnce();
23+
const canvas = within(canvasElement);
24+
const user = userEvent.setup();
25+
const newPasswordInput = await canvas.findByLabelText("Password *");
26+
await user.type(newPasswordInput, "password");
27+
const confirmPasswordInput =
28+
await canvas.findByLabelText("Confirm password *");
29+
await user.type(confirmPasswordInput, "password");
30+
await user.click(canvas.getByRole("button", { name: /reset password/i }));
31+
await canvas.findByText("Password reset successfully");
32+
},
33+
};
34+
35+
export const WrongConfirmationPassword: Story = {
36+
play: async ({ canvasElement }) => {
37+
spyOn(API, "changePasswordWithOTP").mockRejectedValueOnce(
38+
mockApiError({
39+
message: "New password should be different from the old password",
40+
}),
41+
);
42+
const canvas = within(canvasElement);
43+
const user = userEvent.setup();
44+
const newPasswordInput = await canvas.findByLabelText("Password *");
45+
await user.type(newPasswordInput, "password");
46+
const confirmPasswordInput =
47+
await canvas.findByLabelText("Confirm password *");
48+
await user.type(confirmPasswordInput, "different-password");
49+
await user.click(canvas.getByRole("button", { name: /reset password/i }));
50+
await canvas.findByText("Passwords must match");
51+
},
52+
};
53+
54+
export const ServerError: Story = {
55+
play: async ({ canvasElement }) => {
56+
const serverError =
57+
"New password should be different from the old password";
58+
spyOn(API, "changePasswordWithOTP").mockRejectedValueOnce(
59+
mockApiError({
60+
message: serverError,
61+
}),
62+
);
63+
const canvas = within(canvasElement);
64+
const user = userEvent.setup();
65+
const newPasswordInput = await canvas.findByLabelText("Password *");
66+
await user.type(newPasswordInput, "password");
67+
const confirmPasswordInput =
68+
await canvas.findByLabelText("Confirm password *");
69+
await user.type(confirmPasswordInput, "password");
70+
await user.click(canvas.getByRole("button", { name: /reset password/i }));
71+
await canvas.findByText(serverError);
72+
},
73+
};

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy