mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-14 09:25:47 +01:00
✨ Add password meter to onboarding
This commit is contained in:
@@ -15,14 +15,7 @@
|
||||
"title": "Second step",
|
||||
"text": "Password",
|
||||
"password": {
|
||||
"label": "Password",
|
||||
"requirements": {
|
||||
"number": "Includes number",
|
||||
"lowercase": "Includes lowercase letter",
|
||||
"uppercase": "Includes uppercase letter",
|
||||
"special": "Includes special character",
|
||||
"length": "Includes at least {{count}} characters"
|
||||
}
|
||||
"label": "Password"
|
||||
}
|
||||
},
|
||||
"finish": {
|
||||
|
||||
7
public/locales/en/password-requirements.json
Normal file
7
public/locales/en/password-requirements.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"number": "Includes number",
|
||||
"lowercase": "Includes lowercase letter",
|
||||
"uppercase": "Includes uppercase letter",
|
||||
"special": "Includes special character",
|
||||
"length": "Includes at least {{count}} characters"
|
||||
}
|
||||
@@ -1,48 +1,13 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Flex,
|
||||
Group,
|
||||
PasswordInput,
|
||||
Popover,
|
||||
Progress,
|
||||
Text,
|
||||
} from '@mantine/core';
|
||||
import { Button, Card, Flex, Group, PasswordInput, Popover } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import {
|
||||
IconArrowLeft,
|
||||
IconArrowRight,
|
||||
IconCheck,
|
||||
IconDice,
|
||||
IconKey,
|
||||
IconX,
|
||||
} from '@tabler/icons-react';
|
||||
import { IconArrowLeft, IconArrowRight, IconDice, IconKey } from '@tabler/icons-react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { useState } from 'react';
|
||||
import { z } from 'zod';
|
||||
import { PasswordRequirements } from '~/components/Password/password-requirements';
|
||||
import { api } from '~/utils/api';
|
||||
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
||||
import { minPasswordLength, passwordSchema } from '~/validations/user';
|
||||
|
||||
const requirements = [
|
||||
{ re: /[0-9]/, label: 'number' },
|
||||
{ re: /[a-z]/, label: 'lowercase' },
|
||||
{ re: /[A-Z]/, label: 'uppercase' },
|
||||
{ re: /[$&+,:;=?@#|'<>.^*()%!-]/, label: 'special' },
|
||||
];
|
||||
|
||||
function getStrength(password: string) {
|
||||
let multiplier = password.length >= minPasswordLength ? 0 : 1;
|
||||
|
||||
requirements.forEach((requirement) => {
|
||||
if (!requirement.re.test(password)) {
|
||||
multiplier += 1;
|
||||
}
|
||||
});
|
||||
|
||||
return Math.max(100 - (100 / (requirements.length + 1)) * multiplier, 10);
|
||||
}
|
||||
import { passwordSchema } from '~/validations/user';
|
||||
|
||||
interface CreateAccountSecurityStepProps {
|
||||
defaultPassword: string;
|
||||
@@ -70,16 +35,6 @@ export const CreateAccountSecurityStep = ({
|
||||
const { mutateAsync, isLoading } = api.password.generate.useMutation();
|
||||
|
||||
const [popoverOpened, setPopoverOpened] = useState(false);
|
||||
const checks = requirements.map((requirement, index) => (
|
||||
<PasswordRequirement
|
||||
key={index}
|
||||
label={requirement.label}
|
||||
meets={requirement.re.test(form.values.password)}
|
||||
/>
|
||||
));
|
||||
|
||||
const strength = getStrength(form.values.password);
|
||||
const color = strength === 100 ? 'teal' : strength > 50 ? 'yellow' : 'red';
|
||||
|
||||
return (
|
||||
<Card mih={400}>
|
||||
@@ -122,12 +77,7 @@ export const CreateAccountSecurityStep = ({
|
||||
</div>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<Progress color={color} value={strength} size={5} mb="xs" />
|
||||
<PasswordRequirement
|
||||
label="length"
|
||||
meets={form.values.password.length >= minPasswordLength}
|
||||
/>
|
||||
{checks}
|
||||
<PasswordRequirements value={form.values.password} />
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
|
||||
@@ -153,26 +103,6 @@ export const CreateAccountSecurityStep = ({
|
||||
);
|
||||
};
|
||||
|
||||
const PasswordRequirement = ({ meets, label }: { meets: boolean; label: string }) => {
|
||||
const { t } = useTranslation('manage/users/create');
|
||||
|
||||
return (
|
||||
<Text
|
||||
color={meets ? 'teal' : 'red'}
|
||||
sx={{ display: 'flex', alignItems: 'center' }}
|
||||
mt={7}
|
||||
size="sm"
|
||||
>
|
||||
{meets ? <IconCheck size="0.9rem" /> : <IconX size="0.9rem" />}{' '}
|
||||
<Box ml={10}>
|
||||
{t(`steps.security.password.requirements.${label}`, {
|
||||
count: minPasswordLength,
|
||||
})}
|
||||
</Box>
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
export const createAccountSecurityStepValidationSchema = z.object({
|
||||
password: passwordSchema,
|
||||
});
|
||||
|
||||
@@ -2,14 +2,13 @@ import { Stack, Stepper } from '@mantine/core';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { StepCreateAccount } from './step-create-account';
|
||||
import { StepDockerImport } from './step-docker-import';
|
||||
import { StepDocumentation } from './step-documentation';
|
||||
import { StepOnboardingFinished } from './step-onboarding-finished';
|
||||
import { StepUpdatePathMappings } from './step-update-path-mappings';
|
||||
|
||||
export const OnboardingSteps = ({ isUpdate }: { isUpdate: boolean }) => {
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const nextStep = () => setCurrentStep((current) => (current < 4 ? current + 1 : current));
|
||||
const nextStep = () => setCurrentStep((current) => (current < 3 ? current + 1 : current));
|
||||
const prevStep = () => setCurrentStep((current) => (current > 0 ? current - 1 : current));
|
||||
|
||||
return (
|
||||
@@ -31,9 +30,6 @@ export const OnboardingSteps = ({ isUpdate }: { isUpdate: boolean }) => {
|
||||
<Stepper.Step label="Your account" description="Create an account">
|
||||
<StepCreateAccount next={nextStep} previous={prevStep} />
|
||||
</Stepper.Step>
|
||||
<Stepper.Step label="Docker import" description="Import applications from Docker">
|
||||
<StepDockerImport next={nextStep} />
|
||||
</Stepper.Step>
|
||||
<Stepper.Step label="Documentation" description="Introduction into Homarr">
|
||||
<StepDocumentation next={nextStep} />
|
||||
</Stepper.Step>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, Group, PasswordInput, Stack, TextInput, Title } from '@mantine/core';
|
||||
import { Button, Card, Group, PasswordInput, Stack, TextInput, Title } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { IconArrowLeft, IconArrowRight } from '@tabler/icons-react';
|
||||
import { signIn } from 'next-auth/react';
|
||||
@@ -8,6 +8,7 @@ import { api } from '~/utils/api';
|
||||
import { useI18nZodResolver } from '~/utils/i18n-zod-resolver';
|
||||
import { signUpFormSchema } from '~/validations/user';
|
||||
|
||||
import { PasswordRequirements } from '../Password/password-requirements';
|
||||
import { OnboardingStepWrapper } from './common-wrapper';
|
||||
|
||||
export const StepCreateAccount = ({
|
||||
@@ -22,6 +23,11 @@ export const StepCreateAccount = ({
|
||||
const { i18nZodResolver } = useI18nZodResolver();
|
||||
|
||||
const form = useForm<z.infer<typeof signUpFormSchema>>({
|
||||
initialValues: {
|
||||
password: '',
|
||||
username: '',
|
||||
passwordConfirmation: '',
|
||||
},
|
||||
validate: i18nZodResolver(signUpFormSchema),
|
||||
validateInputOnBlur: true,
|
||||
});
|
||||
@@ -70,6 +76,10 @@ export const StepCreateAccount = ({
|
||||
{...form.getInputProps('password')}
|
||||
/>
|
||||
|
||||
<Card mb="lg" withBorder>
|
||||
<PasswordRequirements value={form.values.password} />
|
||||
</Card>
|
||||
|
||||
<PasswordInput
|
||||
size="md"
|
||||
w="100%"
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Button, Stack, Title } from '@mantine/core';
|
||||
import { IconArrowRight } from '@tabler/icons-react';
|
||||
|
||||
import { OnboardingStepWrapper } from './common-wrapper';
|
||||
|
||||
export const StepDockerImport = ({ next }: { next: () => void }) => {
|
||||
return (
|
||||
<OnboardingStepWrapper>
|
||||
<Title order={2} align="center" mb="lg">
|
||||
Automatic container import
|
||||
</Title>
|
||||
|
||||
<Stack align="center">
|
||||
<Button onClick={next} rightIcon={<IconArrowRight size="1rem" />} fullWidth>
|
||||
Next
|
||||
</Button>
|
||||
</Stack>
|
||||
</OnboardingStepWrapper>
|
||||
);
|
||||
};
|
||||
24
src/components/Password/password-requirement.tsx
Normal file
24
src/components/Password/password-requirement.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Box, Text } from "@mantine/core";
|
||||
import { IconCheck, IconX } from "@tabler/icons-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { minPasswordLength } from "~/validations/user";
|
||||
|
||||
export const PasswordRequirement = ({ meets, label }: { meets: boolean; label: string }) => {
|
||||
const { t } = useTranslation('password-requirements');
|
||||
|
||||
return (
|
||||
<Text
|
||||
color={meets ? 'teal' : 'red'}
|
||||
sx={{ display: 'flex', alignItems: 'center' }}
|
||||
mt={7}
|
||||
size="sm"
|
||||
>
|
||||
{meets ? <IconCheck size="0.9rem" /> : <IconX size="0.9rem" />}{' '}
|
||||
<Box ml={10}>
|
||||
{t(`${label}`, {
|
||||
count: minPasswordLength,
|
||||
})}
|
||||
</Box>
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
39
src/components/Password/password-requirements.tsx
Normal file
39
src/components/Password/password-requirements.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Progress } from '@mantine/core';
|
||||
import { minPasswordLength } from '~/validations/user';
|
||||
|
||||
import { PasswordRequirement } from './password-requirement';
|
||||
|
||||
const requirements = [
|
||||
{ re: /[0-9]/, label: 'number' },
|
||||
{ re: /[a-z]/, label: 'lowercase' },
|
||||
{ re: /[A-Z]/, label: 'uppercase' },
|
||||
{ re: /[$&+,:;=?@#|'<>.^*()%!-]/, label: 'special' },
|
||||
];
|
||||
|
||||
function getStrength(password: string) {
|
||||
let multiplier = password.length >= minPasswordLength ? 0 : 1;
|
||||
|
||||
requirements.forEach((requirement) => {
|
||||
if (!requirement.re.test(password)) {
|
||||
multiplier += 1;
|
||||
}
|
||||
});
|
||||
|
||||
return Math.max(100 - (100 / (requirements.length + 1)) * multiplier, 10);
|
||||
}
|
||||
|
||||
export const PasswordRequirements = ({ value }: { value: string }) => {
|
||||
const checks = requirements.map((requirement, index) => (
|
||||
<PasswordRequirement key={index} label={requirement.label} meets={requirement.re.test(value)} />
|
||||
));
|
||||
|
||||
const strength = getStrength(value);
|
||||
const color = strength === 100 ? 'teal' : strength > 50 ? 'yellow' : 'red';
|
||||
return (
|
||||
<>
|
||||
<Progress color={color} value={strength} size={5} mb="xs" />
|
||||
<PasswordRequirement label="length" meets={value.length >= minPasswordLength} />
|
||||
{checks}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -139,7 +139,7 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
}
|
||||
|
||||
const translations = await getServerSideTranslations(
|
||||
manageNamespaces,
|
||||
[...manageNamespaces, 'password-requirements'],
|
||||
ctx.locale,
|
||||
ctx.req,
|
||||
ctx.res
|
||||
|
||||
@@ -77,7 +77,7 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
const configs = files.map((file) => getConfig(file));
|
||||
const configSchemaVersions = configs.map((config) => config.schemaVersion);
|
||||
|
||||
const translations = await getServerSideTranslations([], ctx.locale, ctx.req, ctx.res);
|
||||
const translations = await getServerSideTranslations(['password-requirements'], ctx.locale, ctx.req, ctx.res);
|
||||
|
||||
return {
|
||||
props: {
|
||||
|
||||
Reference in New Issue
Block a user