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

#884 Add forget password route and link to auth pages #888

Merged
merged 8 commits into from
Jun 16, 2024
Merged
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
30 changes: 29 additions & 1 deletion frontend/composables/usePasswordRules.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,38 @@
import type { PasswordRules } from "~/types/password-rules";

export default function usePasswordRules() {
const rules = ref<PasswordRules[]>(passwordRules);

const ruleFunctions: { [key: string]: (value: string) => boolean } = {
"number-of-chars": (value: string) => value.length >= 12,
"capital-letters": (value: string) => /[A-Z]/.test(value),
"lower-case-letters": (value: string) => /[a-z]/.test(value),
"contains-numbers": (value: string) => /[0-9]/.test(value),
"contains-special-chars": (value: string) => /[^a-zA-Z0-9]/.test(value),
};
return { ruleFunctions };

const checkRules = (event: { target: { value: string } }): void => {
const actualValue = event.target.value;
rules.value.forEach((rule) => {
if (ruleFunctions[rule.message]) {
rule.isValid = ruleFunctions[rule.message](actualValue);
}
});
};

const isPasswordMatch = (
passwordValue: string,
confirmPasswordValue: string
) => {
if (passwordValue === "" || confirmPasswordValue === "") {
return false;
}
return passwordValue === confirmPasswordValue;
};

const isAllRulesValid = computed(() => {
return rules.value.every((rule) => rule.isValid);
});

return { checkRules, isAllRulesValid, isPasswordMatch, rules };
}
7 changes: 7 additions & 0 deletions frontend/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"_global.privacy-policy": "Privacy policy",
"_global.public-matrix-chat-rooms": "public Matrix chat rooms",
"_global.repeat-password": "Repeat password",
"_global.enter-username-mail": "Enter your username or email",
"_global.resources": "Resources",
"_global.resources_lower": "resources",
"_global.roadmap": "Roadmap",
Expand All @@ -45,6 +46,8 @@
"_global.settings_lower": "settings",
"_global.sign-in": "Sign in",
"_global.sign-up": "Sign up",
"_global.reset-password": "Reset password",
"_global.set-password": "Set password",
"_global.status": "Status",
"_global.supporters": "Supporters",
"_global.tasks": "Tasks",
Expand Down Expand Up @@ -411,6 +414,8 @@
"components.view-selector.view-as-map-aria-label": "View as map",
"layouts.auth.sign-in-welcome-back": "Welcome back!",
"layouts.auth.sign-up-first-time-welcome": "Let's get to work!",
"layouts.auth.reset-password-forgot-password": "Forgot your password?",
"layouts.auth.set-password-set-new-password": "Set new password",
"layouts.auth.welcome": "Welcome!",
"pages._global.about-us": "About us",
"pages._global.become-supporter": "Become a supporter",
Expand Down Expand Up @@ -466,6 +471,8 @@
"pages.auth.sign-in.index.no-account": "Don't have an account?",
"pages.auth.sign-up.index.enter-user-name": "Enter a username",
"pages.auth.sign-up.index.have-account": "Already have an account?",
"pages.auth.reset-password.index.reset-password-info": "Enter your username or email address and we will send you instructions to reset your password.",
"pages.auth.reset-password.index.back-to-sign-in": "Back to sign in",
"pages.docs.get-active.header": "Discover and get involved",
"pages.docs.get-active.modal-image-alt-text": "Mockups that show mobile organization search and web event search on a map.",
"pages.docs.get-active.section-1-paragraph-1": "activist has the goal of making finding political events and organizations near you as easy as possible. With each event we want people to have an opportunity to get involved in the organization that's putting it on, and once in an organization we want to make it easy to find what to do next to have the biggest impact.",
Expand Down
68 changes: 47 additions & 21 deletions frontend/layouts/auth.vue
Original file line number Diff line number Diff line change
Expand Up @@ -46,27 +46,53 @@
const route = useRoute();

const page = computed(() => {
const isSignIn = route.fullPath?.includes("sign-in");
const isSignUp = route.fullPath?.includes("sign-up");
return {
route: isSignIn ? "sign-in" : isSignUp ? "sign-up" : "index",
btnAriaLabel: isSignIn
? "_global.auth.sign-up-aria-label"
: isSignUp
? "_global.auth.sign-in-aria-label"
: "",
btnLabel: isSignIn ? "_global.sign-up" : isSignUp ? "_global.sign-in" : "",
btnLink: isSignIn ? "/auth/sign-up" : isSignUp ? "/auth/sign-in" : "",
message: isSignIn
? "layouts.auth.sign-in-welcome-back"
: isSignUp
? "layouts.auth.sign-up-first-time-welcome"
: "layouts.auth.welcome",
title: isSignIn
? "_global.sign-in"
: isSignUp
? "_global.sign-up"
: "pages.auth.index.auth",
const authRoutes = [
{
route: "sign-in",
btnAriaLabel: "_global.auth.sign-up-aria-label",
btnLabel: "_global.sign-up",
btnLink: "/auth/sign-up",
message: "layouts.auth.sign-in-welcome-back",
title: "_global.sign-in",
},
{
route: "sign-up",
btnAriaLabel: "_global.auth.sign-in-aria-label",
btnLabel: "_global.sign-in",
btnLink: "/auth/sign-in",
message: "layouts.auth.sign-up-first-time-welcome",
title: "_global.sign-up",
},
{
route: "reset-password",
btnAriaLabel: "_global.auth.sign-in-aria-label",
btnLabel: "_global.sign-in",
btnLink: "/auth/sign-in",
message: "layouts.auth.reset-password-forgot-password",
title: "_global.reset-password",
},
{
route: "set-password",
btnAriaLabel: "_global.auth.sign-in-aria-label",
btnLabel: "_global.sign-in",
btnLink: "/auth/sign-in",
message: "layouts.auth.set-password-set-new-password",
title: "_global.set-new-password",
},
];

const defaultRoute = {
route: "index",
btnAriaLabel: "",
btnLabel: "",
btnLink: "",
message: "layouts.auth.welcome",
title: "pages.auth.index.auth",
};

return (
authRoutes.find((authRoute) => route.fullPath?.includes(authRoute.route)) ||
defaultRoute
);
});
</script>
31 changes: 31 additions & 0 deletions frontend/pages/auth/reset-password.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<template>
<div class="px-4 sm:px-6 md:px-8 xl:px-24 2xl:px-36">
<form class="space-y-4">
<p>{{ $t("pages.auth.reset-password.index.reset-password-info") }}</p>
<FormTextField
@update:model-value="input = $event"
:placeholder="$t('_global.enter-username-mail')"
:model-value="input"
/>
<div class="pt-4">
<BtnAction
class="flex max-h-[48px] items-center justify-center truncate md:max-h-[40px]"
:label="$t('_global.reset-password')"
:cta="true"
fontSize="lg"
:ariaLabel="$t('_global.reset-password')"
/>
</div>
<div class="link-text pt-16 text-center text-xl font-extrabold">
<NuxtLink :to="localePath('/auth/sign-in')"
>{{ $t("pages.auth.reset-password.index.back-to-sign-in") }}
</NuxtLink>
</div>
</form>
</div>
</template>
<script setup lang="ts">
const localePath = useLocalePath();

const input = ref("");
</script>
72 changes: 72 additions & 0 deletions frontend/pages/auth/set-password.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<template>
<div class="px-4 sm:px-6 md:px-8 xl:px-24 2xl:px-36">
<form class="space-y-4">
<FormTextField
@update:model-value="userNameValue = $event"
:placeholder="$t('pages.auth.sign-in.index.enter-user-name')"
:model-value="userNameValue"
/>
<FormTextField
@update:model-value="passwordValue = $event"
@input="checkRules"
@blurred="
isBlurred = true;
isFocused = false;
"
@focused="
isFocused = true;
isBlurred = false;
"
:placeholder="$t('components._global.enter-password')"
:is-icon-visible="true"
input-type="password"
:model-value="passwordValue"
:icons="[IconMap.VISIBLE]"
:error="!isAllRulesValid && isBlurred"
/>
<IndicatorPasswordStrength :password-value="passwordValue" />
<TooltipPasswordRequirements
v-if="
!!passwordValue?.length &&
!isAllRulesValid &&
(!isBlurred || isFocused)
"
:rules="rules"
/>
<FormTextField
@update:model-value="confirmPasswordValue = $event"
:placeholder="$t('_global.repeat-password')"
:is-icon-visible="true"
input-type="password"
:model-value="confirmPasswordValue"
:icons="
isPasswordMatch(passwordValue, confirmPasswordValue)
? [IconMap.CHECK, IconMap.VISIBLE]
: [IconMap.X_LG, IconMap.VISIBLE]
"
/>
<div class="pt-4">
<BtnAction
class="flex max-h-[48px] items-center justify-center truncate md:max-h-[40px]"
:label="$t('_global.set-password')"
:cta="true"
fontSize="lg"
:ariaLabel="$t('_global.set-password')"
/>
</div>
</form>
</div>
</template>
<script setup lang="ts">
import { IconMap } from "~/types/icon-map";

const userNameValue = ref("");
const passwordValue = ref("");
const confirmPasswordValue = ref("");

const isBlurred = ref(false);
const isFocused = ref(false);

const { rules, isAllRulesValid, checkRules, isPasswordMatch } =
usePasswordRules();
</script>
20 changes: 20 additions & 0 deletions frontend/pages/auth/sign-in.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,22 @@
<IndicatorPasswordStrength :password-value="passwordValue" />
<div class="flex flex-col space-y-3">
<FriendlyCaptcha />
<button
@click="navigateTo(localePath('/auth/reset-password'))"
@mouseover="hovered = true"
@focus="hovered = true"
@mouseleave="hovered = false"
@blur="hovered = false"
:disabled="isForgotPasswordDisabled"
class="text-start font-bold"
:class="{ 'link-text': !isForgotPasswordDisabled }"
>
Forgot your password?
</button>
<TooltipBase
v-if="isForgotPasswordDisabled && hovered"
text="For set new password, captcha check should passed"
/>
<BtnAction
class="flex max-h-[48px] w-[116px] items-center justify-center truncate md:max-h-[40px] md:w-[96px]"
:label="$t('_global.sign-in')"
Expand All @@ -46,6 +62,10 @@ import { IconMap } from "~/types/icon-map";

const localePath = useLocalePath();

// TODO: Please change with result of captcha check and remove the comment.
const isForgotPasswordDisabled = false;
Copy link
Contributor Author

@ahmedy00 ahmedy00 May 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andrewtavis I'm not familiar with captcha check, so I added a TODO and it would be nice if you can handle this for me.

const hovered = ref(false);

const userNameValue = ref("");
const passwordValue = ref("");

Expand Down
28 changes: 3 additions & 25 deletions frontend/pages/auth/sign-up.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
input-type="password"
:model-value="confirmPasswordValue"
:icons="
isPasswordMatch
isPasswordMatch(passwordValue, confirmPasswordValue)
? [IconMap.CHECK, IconMap.VISIBLE]
: [IconMap.X_LG, IconMap.VISIBLE]
"
Expand Down Expand Up @@ -92,7 +92,6 @@

<script setup lang="ts">
import { IconMap } from "~/types/icon-map";
import type { PasswordRules } from "~/types/password-rules";

const localePath = useLocalePath();

Expand All @@ -103,29 +102,8 @@ const hasRed = ref(false);
const isBlurred = ref(false);
const isFocused = ref(false);

const isPasswordMatch = computed(() => {
if (passwordValue.value === "" || confirmPasswordValue.value === "") {
return false;
}
return passwordValue.value === confirmPasswordValue.value;
});

const rules = ref<PasswordRules[]>(passwordRules);
const { ruleFunctions } = usePasswordRules();

const checkRules = (value: { target: { value: string } }): void => {
const actualValue = value.target.value;
rules.value.forEach((rule) => {
if (ruleFunctions[rule.message]) {
rule.isValid = ruleFunctions[rule.message](actualValue);
}
});
};

// Checks the rules to make the tooltip invisible when all rules are valid.
const isAllRulesValid = computed(() => {
return rules.value.every((rule) => rule.isValid);
});
const { rules, isAllRulesValid, checkRules, isPasswordMatch } =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great to have this extracted and reusable, @ahmedy00! 😊

usePasswordRules();

const signUp = () => {};
</script>
Loading