Show inline password requirements with realtime validation
This commit is contained in:
@@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import CheckCircle from "@assets/icons/check-circle.svg?dataurl"
|
||||||
|
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import {getPasswordRequirements} from "@app/util/password"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {password}: Props = $props()
|
||||||
|
|
||||||
|
const requirements = $derived(getPasswordRequirements(password))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mt-1 flex flex-col gap-1 text-xs">
|
||||||
|
<p class="font-medium opacity-70">Password requirements</p>
|
||||||
|
<ul class="flex flex-col gap-1 opacity-80">
|
||||||
|
{#each requirements as requirement (requirement.key)}
|
||||||
|
<li class="flex items-center gap-1.5">
|
||||||
|
<Icon
|
||||||
|
icon={requirement.met ? CheckCircle : CloseCircle}
|
||||||
|
class={requirement.met ? "text-success" : "text-error"}
|
||||||
|
size={3} />
|
||||||
|
<span>{requirement.label}</span>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
@@ -16,6 +16,8 @@
|
|||||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import PasswordRequirements from "@app/components/PasswordRequirements.svelte"
|
||||||
|
import {getPasswordValidationError} from "@app/util/password"
|
||||||
import {loginWithPomade, deleteCurrentPomadeSession} from "@app/util/pomade"
|
import {loginWithPomade, deleteCurrentPomadeSession} from "@app/util/pomade"
|
||||||
import {clearModals} from "@app/util/modal"
|
import {clearModals} from "@app/util/modal"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
@@ -31,10 +33,12 @@
|
|||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
if (password.trim().length < 12) {
|
const passwordError = getPasswordValidationError(password)
|
||||||
|
|
||||||
|
if (passwordError) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
message: "Password must be at least 12 characters long.",
|
message: passwordError,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,6 +108,7 @@
|
|||||||
</label>
|
</label>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</FieldInline>
|
</FieldInline>
|
||||||
|
<PasswordRequirements {password} />
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button class="btn btn-link" onclick={back}>
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
|||||||
@@ -17,7 +17,9 @@
|
|||||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import PasswordRequirements from "@app/components/PasswordRequirements.svelte"
|
||||||
import SignUpEmailConfirm from "@app/components/SignUpEmailConfirm.svelte"
|
import SignUpEmailConfirm from "@app/components/SignUpEmailConfirm.svelte"
|
||||||
|
import {getPasswordValidationError} from "@app/util/password"
|
||||||
import {pushToast, popToast} from "@app/util/toast"
|
import {pushToast, popToast} from "@app/util/toast"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
@@ -30,10 +32,12 @@
|
|||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
if (password.trim().length < 12) {
|
const passwordError = getPasswordValidationError(password)
|
||||||
|
|
||||||
|
if (passwordError) {
|
||||||
return pushToast({
|
return pushToast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
message: "Password must be at least 12 characters long.",
|
message: passwordError,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,6 +139,7 @@
|
|||||||
</label>
|
</label>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</FieldInline>
|
</FieldInline>
|
||||||
|
<PasswordRequirements {password} />
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button class="btn btn-link" onclick={back}>
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
const DEFAULT_MIN_PASSWORD_LENGTH = 12
|
||||||
|
|
||||||
|
const parsedMinPasswordLength = Number.parseInt(import.meta.env.VITE_PASSWORD_MIN_LENGTH || "", 10)
|
||||||
|
|
||||||
|
export const MIN_PASSWORD_LENGTH =
|
||||||
|
Number.isFinite(parsedMinPasswordLength) && parsedMinPasswordLength > 0
|
||||||
|
? parsedMinPasswordLength
|
||||||
|
: DEFAULT_MIN_PASSWORD_LENGTH
|
||||||
|
|
||||||
|
export type PasswordRequirement = {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
met: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getPasswordRequirements = (password: string): PasswordRequirement[] => [
|
||||||
|
{
|
||||||
|
key: "minLength",
|
||||||
|
label: `At least ${MIN_PASSWORD_LENGTH} characters`,
|
||||||
|
met: password.trim().length >= MIN_PASSWORD_LENGTH,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const getPasswordValidationError = (password: string) => {
|
||||||
|
const failed = getPasswordRequirements(password).find(({met}) => !met)
|
||||||
|
|
||||||
|
if (failed) {
|
||||||
|
return `Password must be ${failed.label.toLowerCase()}.`
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user