Skip to content
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

Improve password reset UI/UX [GCC2024_COFEST] #18467

Open
wants to merge 7 commits into
base: dev
Choose a base branch
from
Open
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
23 changes: 7 additions & 16 deletions client/src/components/Login/LoginForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ async function submitLogin() {
}

if (response.data.expired_user) {
window.location.href = withPrefix(`/root/login?expired_user=${response.data.expired_user}`);
window.location.href = withPrefix(`/login/start?expired_user=${response.data.expired_user}`);
} else if (connectExternalProvider.value) {
window.location.href = withPrefix("/user/external_ids?connect_external=true");
} else if (response.data.redirect) {
Expand Down Expand Up @@ -125,20 +125,6 @@ function setRedirect(url: string) {
localStorage.setItem("redirect_url", url);
}

async function resetLogin() {
loading.value = true;
try {
const response = await axios.post(withPrefix("/user/reset_password"), { email: login.value });
messageVariant.value = "info";
messageText.value = response.data.message;
} catch (e) {
messageVariant.value = "danger";
messageText.value = errorMessageAsString(e, "Password reset failed for an unknown reason.");
} finally {
loading.value = false;
}
}

function returnToLogin() {
router.push("/login/start");
}
Expand Down Expand Up @@ -205,7 +191,12 @@ function returnToLogin() {
v-localize
href="javascript:void(0)"
role="button"
@click.prevent="resetLogin">
@click.prevent="
router.push({
path: '/login/reset_password',
query: { email: login },
})
">
Click here to reset your password.
</a>
</BFormText>
Expand Down
96 changes: 96 additions & 0 deletions client/src/entry/analysis/modules/ResetPassword.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { getLocalVue } from "@tests/jest/helpers";
import { mount } from "@vue/test-utils";
import axios from "axios";
import MockAdapter from "axios-mock-adapter";

import ResetPassword from "./ResetPassword.vue";

const localVue = getLocalVue(true);

const mockRouter = (query: object) => ({
currentRoute: {
query,
},
});

function mountResetPassword(routerQuery: object = {}) {
return mount(ResetPassword, {
localVue,
attachTo: document.body,
mocks: {
$router: mockRouter(routerQuery),
},
});
}

describe("ResetPassword", () => {
it("query", async () => {
const email = "test";
const wrapper = mountResetPassword({ email: "test" });

const emailField = wrapper.find("#reset-email");
const emailValue = (emailField.element as HTMLInputElement).value;
expect(emailValue).toBe(email);
});

it("button text", async () => {
const wrapper = mountResetPassword();
const submitButton = wrapper.find("#reset-password");
(expect(submitButton.text()) as any).toBeLocalizationOf("Send password reset email");
});

it("validate email", async () => {
const wrapper = mountResetPassword();
const submitButton = wrapper.find("#reset-password");
const emailField = wrapper.find("#reset-email");
const emailElement = emailField.element as HTMLInputElement;

let email = "";
await emailField.setValue(email);
expect(emailElement.value).toBe(email);
await submitButton.trigger("click");
expect(emailElement.checkValidity()).toBe(false);

email = "test";
await emailField.setValue(email);
expect(emailElement.value).toBe(email);
await submitButton.trigger("click");
expect(emailElement.checkValidity()).toBe(false);

email = "[email protected]";
await emailField.setValue(email);
expect(emailElement.value).toBe(email);
await submitButton.trigger("click");
expect(emailElement.checkValidity()).toBe(true);
});

it("display success message", async () => {
const wrapper = mountResetPassword({ email: "[email protected]" });
const mockAxios = new MockAdapter(axios);
const submitButton = wrapper.find("#reset-password");

mockAxios.onPost("/user/reset_password").reply(200, {
message: "Reset link has been sent to your email.",
});
await submitButton.trigger("click");
setTimeout(async () => {
const alertSuccess = wrapper.find("#reset-password-alert");
expect(alertSuccess.text()).toBe("Reset link has been sent to your email.");
});
});

it("display error message", async () => {
const wrapper = mountResetPassword({ email: "[email protected]" });
const submitButton = wrapper.find("#reset-password");

const mockAxios = new MockAdapter(axios);
mockAxios.onPost("/user/reset_password").reply(400, {
err_msg: "Please provide your email.",
});
await submitButton.trigger("click");
setTimeout(async () => {
const alertError = wrapper.find("#reset-password-alert");
expect(alertError.text()).toBe("Please provide your email.");
});
});
});
56 changes: 56 additions & 0 deletions client/src/entry/analysis/modules/ResetPassword.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<script setup lang="ts">
import axios from "axios";
import { BAlert, BButton, BCard, BForm, BFormGroup, BFormInput } from "bootstrap-vue";
import { ref } from "vue";
import { useRouter } from "vue-router/composables";

import { withPrefix } from "@/utils/redirect";
import { errorMessageAsString } from "@/utils/simple-error";

const router = useRouter();

const loading = ref(false);
const email = ref(router.currentRoute.query.email || "");
const message = ref("");
const messageVariant = ref("info");

async function resetLogin() {
loading.value = true;
try {
const response = await axios.post(withPrefix("/user/reset_password"), { email: email.value });
messageVariant.value = "info";
message.value = response.data.message;
} catch (e) {
messageVariant.value = "danger";
message.value = errorMessageAsString(e, "Password reset failed for an unknown reason.");
} finally {
loading.value = false;
}
}
</script>

<template>
<div class="overflow-auto m-3">
<div class="container">
<div class="row justify-content-md-center">
<div class="col col-lg-6">
<BForm @submit.prevent="resetLogin">
<BAlert v-if="!!message" id="reset-password-alert" class="mt-2" :variant="messageVariant" show>
{{ message }}
</BAlert>

<BCard header="Reset your password">
<BFormGroup label="Email Address">
<BFormInput id="reset-email" v-model="email" type="email" name="email" required />
</BFormGroup>

<BButton id="reset-password" v-localize type="submit" :disabled="loading"
>Send password reset email</BButton
>
</BCard>
</BForm>
</div>
</div>
</div>
</div>
</template>
8 changes: 8 additions & 0 deletions client/src/entry/analysis/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import Analysis from "entry/analysis/modules/Analysis";
import CenterFrame from "entry/analysis/modules/CenterFrame";
import Home from "entry/analysis/modules/Home";
import Login from "entry/analysis/modules/Login";
import ResetPassword from "entry/analysis/modules/ResetPassword";
import WorkflowEditorModule from "entry/analysis/modules/WorkflowEditor";
import AdminRoutes from "entry/analysis/routes/admin-routes";
import LibraryRoutes from "entry/analysis/routes/library-routes";
Expand Down Expand Up @@ -125,6 +126,13 @@ export function getRouter(Galaxy) {
component: Login,
redirect: redirectLoggedIn(),
},
/** Login entry route */
{
path: "/login/reset_password",
component: ResetPassword,
props: (route) => ({ email: route.query.email }),
redirect: redirectLoggedIn(),
},
/** Page editor */
{
path: "/pages/editor",
Expand Down
Loading