diff --git a/messages/en.json b/messages/en.json index 0a5c438..e78cbf9 100644 --- a/messages/en.json +++ b/messages/en.json @@ -17,5 +17,29 @@ "ShowPasswordTooltip": "Show password", "Submit": "Submit", "RegisteredSuccessfully": "Registered successfully" + }, + "DashboardPage": { + "SecurityCardTitle": "Account security", + "SecurityCardDescription": "Update your password and keep your account protected.", + "SecurityCardAction": "Open settings" + }, + "SettingsPage": { + "Title": "Change password", + "Description": "Use your current password to set a new one for this account.", + "CurrentPasswordLabel": "Current password", + "CurrentPasswordPlaceholder": "Enter your current password", + "CurrentPasswordRequired": "Enter your current password", + "NewPasswordLabel": "New password", + "NewPasswordPlaceholder": "Enter your new password", + "PasswordTooShort": "Password must be at least 8 characters long", + "ConfirmPasswordLabel": "Confirm new password", + "ConfirmPasswordPlaceholder": "Repeat your new password", + "ConfirmPasswordRequired": "Repeat your new password", + "PasswordsDoNotMatch": "Passwords do not match", + "PasswordMustDiffer": "Your new password must be different from the current password", + "ShowPasswordTooltip": "Show password", + "HidePasswordTooltip": "Hide password", + "Submit": "Save new password", + "PasswordUpdatedSuccessfully": "Password updated successfully" } } diff --git a/messages/pl.json b/messages/pl.json index 641bc13..2a606d3 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -17,5 +17,29 @@ "ShowPasswordTooltip": "Pokaż hasło", "Submit": "Wyślij", "RegisteredSuccessfully": "Zarejestrowano pomyślnie" + }, + "DashboardPage": { + "SecurityCardTitle": "Bezpieczeństwo konta", + "SecurityCardDescription": "Zmień hasło i zadbaj o bezpieczeństwo swojego konta.", + "SecurityCardAction": "Otwórz ustawienia" + }, + "SettingsPage": { + "Title": "Zmień hasło", + "Description": "Użyj obecnego hasła, aby ustawić nowe hasło dla tego konta.", + "CurrentPasswordLabel": "Obecne hasło", + "CurrentPasswordPlaceholder": "Wpisz obecne hasło", + "CurrentPasswordRequired": "Wpisz obecne hasło", + "NewPasswordLabel": "Nowe hasło", + "NewPasswordPlaceholder": "Wpisz nowe hasło", + "PasswordTooShort": "Hasło musi mieć co najmniej 8 znaków", + "ConfirmPasswordLabel": "Potwierdź nowe hasło", + "ConfirmPasswordPlaceholder": "Powtórz nowe hasło", + "ConfirmPasswordRequired": "Powtórz nowe hasło", + "PasswordsDoNotMatch": "Hasła nie są takie same", + "PasswordMustDiffer": "Nowe hasło musi różnić się od obecnego hasła", + "ShowPasswordTooltip": "Pokaż hasło", + "HidePasswordTooltip": "Ukryj hasło", + "Submit": "Zapisz nowe hasło", + "PasswordUpdatedSuccessfully": "Hasło zostało zmienione" } } diff --git a/src/components/settings/PasswordChangeCard.tsx b/src/components/settings/PasswordChangeCard.tsx new file mode 100644 index 0000000..6b47426 --- /dev/null +++ b/src/components/settings/PasswordChangeCard.tsx @@ -0,0 +1,261 @@ +'use client'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { EyeIcon, EyeOff } from '@hugeicons/core-free-icons'; +import { HugeiconsIcon } from '@hugeicons/react'; +import { useTranslations } from 'next-intl'; +import { useState } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import { toast } from 'sonner'; +import { z } from 'zod/v4'; + +import { Button } from '@/components/ui/button'; +import { defaultPasswordValidator } from '@/constants'; +import { authClient } from '@/lib/auth-client'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { Field, FieldError, FieldGroup, FieldLabel } from '@/components/ui/field'; +import { + InputGroup, + InputGroupAddon, + InputGroupInput, +} from '@/components/ui/input-group'; +import { Spinner } from '@/components/ui/spinner'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; + +type PasswordField = 'currentPassword' | 'newPassword' | 'confirmPassword'; + +type ChangePasswordMessages = { + currentPasswordRequired: string; + passwordTooShort: string; + confirmPasswordRequired: string; + passwordsDoNotMatch: string; + passwordMustDiffer: string; +}; + +export function createChangePasswordSchema(messages: ChangePasswordMessages) { + return z + .object({ + currentPassword: z.string().min(1, { + error: messages.currentPasswordRequired, + }), + newPassword: defaultPasswordValidator(messages.passwordTooShort), + confirmPassword: z.string().min(1, { + error: messages.confirmPasswordRequired, + }), + }) + .refine((value) => value.newPassword === value.confirmPassword, { + error: messages.passwordsDoNotMatch, + path: ['confirmPassword'], + }) + .refine((value) => value.currentPassword !== value.newPassword, { + error: messages.passwordMustDiffer, + path: ['newPassword'], + }); +} + +export function PasswordChangeCard() { + const t = useTranslations('SettingsPage'); + const [visibleFields, setVisibleFields] = useState>({ + currentPassword: false, + newPassword: false, + confirmPassword: false, + }); + + const changePasswordSchema = createChangePasswordSchema({ + currentPasswordRequired: t('CurrentPasswordRequired'), + passwordTooShort: t('PasswordTooShort'), + confirmPasswordRequired: t('ConfirmPasswordRequired'), + passwordsDoNotMatch: t('PasswordsDoNotMatch'), + passwordMustDiffer: t('PasswordMustDiffer'), + }); + + type ChangePasswordValues = z.infer; + + const form = useForm({ + resolver: zodResolver(changePasswordSchema), + defaultValues: { + currentPassword: '', + newPassword: '', + confirmPassword: '', + }, + }); + + const isSubmitting = form.formState.isSubmitting; + + function toggleVisibility(field: PasswordField) { + setVisibleFields((current) => ({ + ...current, + [field]: !current[field], + })); + } + + async function onSubmit(values: ChangePasswordValues) { + const result = await authClient.changePassword({ + currentPassword: values.currentPassword, + newPassword: values.newPassword, + revokeOtherSessions: true, + }); + + if (result.error) { + toast(result.error.message); + return; + } + + form.reset(); + toast(t('PasswordUpdatedSuccessfully')); + } + + return ( + + + {t('Title')} + {t('Description')} + + +
+ + ( + + + {t('CurrentPasswordLabel')} + + + + toggleVisibility('currentPassword')} + > + + + {visibleFields.currentPassword ? ( + + ) : ( + + )} + + +

+ {visibleFields.currentPassword + ? t('HidePasswordTooltip') + : t('ShowPasswordTooltip')} +

+
+
+
+
+ {fieldState.invalid && } +
+ )} + /> + ( + + {t('NewPasswordLabel')} + + + toggleVisibility('newPassword')} + > + + + {visibleFields.newPassword ? ( + + ) : ( + + )} + + +

+ {visibleFields.newPassword + ? t('HidePasswordTooltip') + : t('ShowPasswordTooltip')} +

+
+
+
+
+ {fieldState.invalid && } +
+ )} + /> + ( + + + {t('ConfirmPasswordLabel')} + + + + toggleVisibility('confirmPassword')} + > + + + {visibleFields.confirmPassword ? ( + + ) : ( + + )} + + +

+ {visibleFields.confirmPassword + ? t('HidePasswordTooltip') + : t('ShowPasswordTooltip')} +

+
+
+
+
+ {fieldState.invalid && } +
+ )} + /> + +
+
+
+
+ ); +}