- Notifications
You must be signed in to change notification settings - Fork1.1k
feat(site): add forgot password link#15108
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
dd9cfae648935ac125320893efec72767811c4bdee4d7ec364b22b4d7ef3df8c0d3a6cc279704File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| UPDATE notification_templates | ||
| SET | ||
| title_template = E'Reset your password for Coder', | ||
| 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.', | ||
| actions = '[{ | ||
| "label": "Reset password", | ||
| "url": "{{ base_url }}/reset-password/change?otp={{.Labels.one_time_passcode}}&email={{ .UserEmail }}" | ||
| }]'::jsonb | ||
| WHERE | ||
| id = '62f86a30-2330-4b61-a26d-311ff3b608cf' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -3,6 +3,7 @@ import type { | ||
| AuthorizationRequest, | ||
| GenerateAPIKeyResponse, | ||
| GetUsersResponse, | ||
| RequestOneTimePasscodeRequest, | ||
| UpdateUserAppearanceSettingsRequest, | ||
| UpdateUserPasswordRequest, | ||
| UpdateUserProfileRequest, | ||
| @@ -253,3 +254,16 @@ export const updateAppearanceSettings = ( | ||
| }, | ||
| }; | ||
| }; | ||
| export const requestOneTimePassword = () => { | ||
| return { | ||
| mutationFn: (req: RequestOneTimePasscodeRequest) => | ||
| API.requestOneTimePassword(req), | ||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Nit: Could we pass the function directly as below? ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Unfortunately, when we do that, we are not able to spy on the method. I'm not sure why, but we have encountered this issue in jest as well. Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Weeeeird. | ||
| }; | ||
| }; | ||
| export const changePasswordWithOTP = () => { | ||
| return { | ||
| mutationFn: API.changePasswordWithOTP, | ||
| }; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import type { Interpolation, Theme } from "@emotion/react"; | ||
| import { CoderIcon } from "components/Icons/CoderIcon"; | ||
| import type { FC } from "react"; | ||
| import { getApplicationName, getLogoURL } from "utils/appearance"; | ||
| /** | ||
| * Enterprise customers can set a custom logo for their Coder application. Use | ||
| * the custom logo wherever the Coder logo is used, if a custom one is provided. | ||
| */ | ||
| export const CustomLogo: FC<{ css?: Interpolation<Theme> }> = (props) => { | ||
| const applicationName = getApplicationName(); | ||
| const logoURL = getLogoURL(); | ||
| return logoURL ? ( | ||
| <img | ||
| {...props} | ||
| alt={applicationName} | ||
| src={logoURL} | ||
| // This prevent browser to display the ugly error icon if the | ||
| // image path is wrong or user didn't finish typing the url | ||
| onError={(e) => { | ||
| e.currentTarget.style.display = "none"; | ||
| }} | ||
| onLoad={(e) => { | ||
| e.currentTarget.style.display = "inline"; | ||
| }} | ||
| css={{ maxWidth: 200 }} | ||
| className="application-logo" | ||
| /> | ||
| ) : ( | ||
| <CoderIcon {...props} css={[{ fontSize: 64, fill: "white" }, props.css]} /> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| import type { Meta, StoryObj } from "@storybook/react"; | ||
| import { expect, spyOn, userEvent, within } from "@storybook/test"; | ||
| import { API } from "api/api"; | ||
| import { mockApiError } from "testHelpers/entities"; | ||
| import { withGlobalSnackbar } from "testHelpers/storybook"; | ||
| import ChangePasswordPage from "./ChangePasswordPage"; | ||
| const meta: Meta<typeof ChangePasswordPage> = { | ||
| title: "pages/ResetPasswordPage/ChangePasswordPage", | ||
| component: ChangePasswordPage, | ||
| args: { redirect: false }, | ||
| decorators: [withGlobalSnackbar], | ||
| }; | ||
| export default meta; | ||
| type Story = StoryObj<typeof ChangePasswordPage>; | ||
| export const Default: Story = {}; | ||
| export const Success: Story = { | ||
| play: async ({ canvasElement }) => { | ||
| spyOn(API, "changePasswordWithOTP").mockResolvedValueOnce(); | ||
| const canvas = within(canvasElement); | ||
| const user = userEvent.setup(); | ||
| const newPasswordInput = await canvas.findByLabelText("Password *"); | ||
| await user.type(newPasswordInput, "password"); | ||
| const confirmPasswordInput = | ||
| await canvas.findByLabelText("Confirm password *"); | ||
| await user.type(confirmPasswordInput, "password"); | ||
| await user.click(canvas.getByRole("button", { name: /reset password/i })); | ||
| await canvas.findByText("Password reset successfully"); | ||
| }, | ||
| }; | ||
| export const WrongConfirmationPassword: Story = { | ||
| play: async ({ canvasElement }) => { | ||
| spyOn(API, "changePasswordWithOTP").mockRejectedValueOnce( | ||
| mockApiError({ | ||
| message: "New password should be different from the old password", | ||
| }), | ||
| ); | ||
| const canvas = within(canvasElement); | ||
| const user = userEvent.setup(); | ||
| const newPasswordInput = await canvas.findByLabelText("Password *"); | ||
| await user.type(newPasswordInput, "password"); | ||
| const confirmPasswordInput = | ||
| await canvas.findByLabelText("Confirm password *"); | ||
| await user.type(confirmPasswordInput, "different-password"); | ||
| await user.click(canvas.getByRole("button", { name: /reset password/i })); | ||
| await canvas.findByText("Passwords must match"); | ||
| }, | ||
| }; | ||
| export const ServerError: Story = { | ||
| play: async ({ canvasElement }) => { | ||
| const serverError = | ||
| "New password should be different from the old password"; | ||
| spyOn(API, "changePasswordWithOTP").mockRejectedValueOnce( | ||
| mockApiError({ | ||
| message: serverError, | ||
| }), | ||
| ); | ||
| const canvas = within(canvasElement); | ||
| const user = userEvent.setup(); | ||
| const newPasswordInput = await canvas.findByLabelText("Password *"); | ||
| await user.type(newPasswordInput, "password"); | ||
| const confirmPasswordInput = | ||
| await canvas.findByLabelText("Confirm password *"); | ||
| await user.type(confirmPasswordInput, "password"); | ||
| await user.click(canvas.getByRole("button", { name: /reset password/i })); | ||
| await canvas.findByText(serverError); | ||
| }, | ||
| }; |
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.