Skip to content
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
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
'use client';

import { zodResolver } from '@hookform/resolvers/zod';
import { useMemo } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';

import { ValidationCheckList } from '@/app/components/ui/feedback/validation-check-item';
import { Form } from '@/app/components/ui/forms/form';
import { FormSection } from '@/app/components/ui/forms/form-section';
import { Input } from '@/app/components/ui/forms/input';
import { NarrowContainer } from '@/app/components/ui/layout/layout';
import { PageSection } from '@/app/components/ui/layout/page-section';
import { Button } from '@/app/components/ui/primitives/button';
import { useHasCredentialAccount } from '@/app/features/auth/hooks/queries';
import { usePasswordValidation } from '@/app/hooks/use-password-validation';
import { useToast } from '@/app/hooks/use-toast';
import { useT } from '@/lib/i18n/client';
import { createPasswordSchema } from '@/lib/shared/schemas/password';

import { useUpdatePassword } from '../hooks/mutations';

Expand Down Expand Up @@ -91,13 +98,40 @@ function ChangePasswordForm({
tCommon: ReturnType<typeof useT>['t'];
tToast: ReturnType<typeof useT>['t'];
}) {
const changePasswordSchema = useMemo(
() =>
z
.object({
currentPassword: z
.string()
.min(1, tAuth('changePassword.validation.currentRequired')),
newPassword: createPasswordSchema({
minLength: tAuth('validation.passwordMinLength'),
lowercase: tAuth('validation.passwordLowercase'),
uppercase: tAuth('validation.passwordUppercase'),
number: tAuth('validation.passwordNumber'),
specialChar: tAuth('validation.passwordSpecial'),
}),
confirmPassword: z
.string()
.min(1, tAuth('changePassword.validation.confirmRequired')),
})
.refine((data) => data.newPassword === data.confirmPassword, {
message: tAuth('changePassword.validation.mismatch'),
path: ['confirmPassword'],
}),
[tAuth],
);

const {
register,
handleSubmit,
formState: { errors, isSubmitting },
reset,
watch,
} = useForm<ChangePasswordFormData>({
resolver: zodResolver(changePasswordSchema),
mode: 'onChange',
defaultValues: {
currentPassword: '',
newPassword: '',
Expand All @@ -106,6 +140,7 @@ function ChangePasswordForm({
});

const newPassword = watch('newPassword');
const passwordValidationItems = usePasswordValidation(newPassword);

const onSubmit = async (data: ChangePasswordFormData) => {
try {
Expand Down Expand Up @@ -137,26 +172,26 @@ function ChangePasswordForm({
placeholder={tAuth('changePassword.placeholder.current')}
disabled={isSubmitting}
errorMessage={errors.currentPassword?.message}
{...register('currentPassword', {
required: tAuth('changePassword.validation.currentRequired'),
})}
{...register('currentPassword')}
/>

<Input
id="new-password"
type="password"
label={tAuth('changePassword.newPassword')}
placeholder={tAuth('changePassword.placeholder.new')}
disabled={isSubmitting}
errorMessage={errors.newPassword?.message}
{...register('newPassword', {
required: tAuth('changePassword.validation.newRequired'),
minLength: {
value: 8,
message: tAuth('changePassword.validation.minLength'),
},
})}
/>
<FormSection>
<Input
id="new-password"
type="password"
label={tAuth('changePassword.newPassword')}
placeholder={tAuth('changePassword.placeholder.new')}
disabled={isSubmitting}
errorMessage={errors.newPassword?.message}
{...register('newPassword')}
/>
{newPassword && (
<ValidationCheckList
items={passwordValidationItems}
className="text-xs"
/>
)}
</FormSection>

<Input
id="confirm-password"
Expand All @@ -165,12 +200,7 @@ function ChangePasswordForm({
placeholder={tAuth('changePassword.placeholder.confirm')}
disabled={isSubmitting}
errorMessage={errors.confirmPassword?.message}
{...register('confirmPassword', {
required: tAuth('changePassword.validation.confirmRequired'),
validate: (value) =>
value === newPassword ||
tAuth('changePassword.validation.mismatch'),
})}
{...register('confirmPassword')}
/>

<Button type="submit" disabled={isSubmitting} fullWidth>
Expand All @@ -195,20 +225,45 @@ function SetPasswordForm({
tCommon: ReturnType<typeof useT>['t'];
tToast: ReturnType<typeof useT>['t'];
}) {
const setPasswordSchema = useMemo(
() =>
z
.object({
newPassword: createPasswordSchema({
minLength: tAuth('validation.passwordMinLength'),
lowercase: tAuth('validation.passwordLowercase'),
uppercase: tAuth('validation.passwordUppercase'),
number: tAuth('validation.passwordNumber'),
specialChar: tAuth('validation.passwordSpecial'),
}),
confirmPassword: z
.string()
.min(1, tAuth('changePassword.validation.confirmRequired')),
})
.refine((data) => data.newPassword === data.confirmPassword, {
message: tAuth('changePassword.validation.mismatch'),
path: ['confirmPassword'],
}),
[tAuth],
);

const {
register,
handleSubmit,
formState: { errors, isSubmitting },
reset,
watch,
} = useForm<SetPasswordFormData>({
resolver: zodResolver(setPasswordSchema),
mode: 'onChange',
defaultValues: {
newPassword: '',
confirmPassword: '',
},
});

const newPassword = watch('newPassword');
const passwordValidationItems = usePasswordValidation(newPassword);

const onSubmit = async (data: SetPasswordFormData) => {
try {
Expand All @@ -232,21 +287,23 @@ function SetPasswordForm({

return (
<Form onSubmit={handleSubmit(onSubmit)}>
<Input
id="new-password"
type="password"
label={tAuth('setPassword.newPassword')}
placeholder={tAuth('changePassword.placeholder.new')}
disabled={isSubmitting}
errorMessage={errors.newPassword?.message}
{...register('newPassword', {
required: tAuth('changePassword.validation.newRequired'),
minLength: {
value: 8,
message: tAuth('changePassword.validation.minLength'),
},
})}
/>
<FormSection>
<Input
id="new-password"
type="password"
label={tAuth('setPassword.newPassword')}
placeholder={tAuth('changePassword.placeholder.new')}
disabled={isSubmitting}
errorMessage={errors.newPassword?.message}
{...register('newPassword')}
/>
{newPassword && (
<ValidationCheckList
items={passwordValidationItems}
className="text-xs"
/>
)}
</FormSection>

<Input
id="confirm-password"
Expand All @@ -255,12 +312,7 @@ function SetPasswordForm({
placeholder={tAuth('changePassword.placeholder.confirm')}
disabled={isSubmitting}
errorMessage={errors.confirmPassword?.message}
{...register('confirmPassword', {
required: tAuth('changePassword.validation.confirmRequired'),
validate: (value) =>
value === newPassword ||
tAuth('changePassword.validation.mismatch'),
})}
{...register('confirmPassword')}
/>

<Button type="submit" disabled={isSubmitting} fullWidth>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ import { Input } from '@/app/components/ui/forms/input';
import { Select } from '@/app/components/ui/forms/select';
import { Stack } from '@/app/components/ui/layout/layout';
import { Button } from '@/app/components/ui/primitives/button';
import { usePasswordValidation } from '@/app/hooks/use-password-validation';
import { useToast } from '@/app/hooks/use-toast';
import { useT } from '@/lib/i18n/client';
import { createOptionalPasswordSchema } from '@/lib/shared/schemas/password';
import { narrowStringUnion } from '@/lib/utils/type-guards';

import { useCreateMember } from '../hooks/mutations';
Expand Down Expand Up @@ -45,29 +47,17 @@ export function AddMemberDialog({
const { t: tAuth } = useT('auth');
const { t: tToast } = useT('toast');

// Create Zod schema with translated validation messages
const addMemberSchema = useMemo(
() =>
z.object({
email: z.string().email(tCommon('validation.email')),
password: z
.string()
.optional()
.refine(
(val) => {
// If password is provided, it must meet requirements
if (!val || val.length === 0) return true;
return (
val.length >= 8 &&
/[a-z]/.test(val) &&
/[A-Z]/.test(val) &&
/\d/.test(val)
);
},
{
message: tAuth('validation.passwordRequirements'),
},
),
password: createOptionalPasswordSchema({
minLength: tAuth('validation.passwordMinLength'),
lowercase: tAuth('validation.passwordLowercase'),
uppercase: tAuth('validation.passwordUppercase'),
number: tAuth('validation.passwordNumber'),
specialChar: tAuth('validation.passwordSpecial'),
}),
displayName: z.string().optional(),
role: z.enum(['disabled', 'admin', 'developer', 'editor', 'member']),
}),
Expand Down Expand Up @@ -98,28 +88,7 @@ export function AddMemberDialog({
const selectedRole = watch('role');
const password = watch('password') ?? '';

// Password validation checks for display
const passwordValidationItems = useMemo(
() => [
{
isValid: password.length >= 8,
message: tAuth('changePassword.requirements.length'),
},
{
isValid: /[a-z]/.test(password),
message: tAuth('changePassword.requirements.lowercase'),
},
{
isValid: /[A-Z]/.test(password),
message: tAuth('changePassword.requirements.uppercase'),
},
{
isValid: /\d/.test(password),
message: tAuth('changePassword.requirements.number'),
},
],
[password, tAuth],
);
const passwordValidationItems = usePasswordValidation(password);

const onSubmit = async (data: AddMemberFormData) => {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,20 @@ import * as z from 'zod';

import { FormDialog } from '@/app/components/ui/dialog/form-dialog';
import { Banner } from '@/app/components/ui/feedback/banner';
import { ValidationCheckList } from '@/app/components/ui/feedback/validation-check-item';
import { Checkbox } from '@/app/components/ui/forms/checkbox';
import { FormSection } from '@/app/components/ui/forms/form-section';
import { Input } from '@/app/components/ui/forms/input';
import { Select } from '@/app/components/ui/forms/select';
import { Text } from '@/app/components/ui/typography/text';
import { usePasswordValidation } from '@/app/hooks/use-password-validation';
import { toast } from '@/app/hooks/use-toast';
import { useT } from '@/lib/i18n/client';
import {
memberRoleSchema,
type MemberRole,
} from '@/lib/shared/schemas/organizations';
import { createOptionalPasswordSchema } from '@/lib/shared/schemas/password';

import {
useSetMemberPassword,
Expand Down Expand Up @@ -57,6 +60,7 @@ export function EditMemberDialog({
}: EditMemberDialogProps) {
const { t } = useT('settings');
const { t: tCommon } = useT('common');
const { t: tAuth } = useT('auth');

const editMemberSchema = useMemo(
() =>
Expand All @@ -67,9 +71,15 @@ export function EditMemberDialog({
role: memberRoleSchema,
email: z.string().email(tCommon('validation.email')),
updatePassword: z.boolean().optional(),
password: z.string().optional(),
password: createOptionalPasswordSchema({
minLength: tAuth('validation.passwordMinLength'),
lowercase: tAuth('validation.passwordLowercase'),
uppercase: tAuth('validation.passwordUppercase'),
number: tAuth('validation.passwordNumber'),
specialChar: tAuth('validation.passwordSpecial'),
}),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}),
[t, tCommon],
[t, tCommon, tAuth],
);

const form = useForm<EditMemberFormData>({
Expand Down Expand Up @@ -133,6 +143,8 @@ export function EditMemberDialog({

const { handleSubmit, register, reset, watch, formState } = form;
const { isSubmitting, isDirty } = formState;
const password = watch('password') ?? '';
const passwordValidationItems = usePasswordValidation(password);

const isEditingSelf = currentUserMemberId === member?._id;

Expand Down Expand Up @@ -232,7 +244,12 @@ export function EditMemberDialog({
<Checkbox
id="updatePassword"
checked={field.value}
onCheckedChange={field.onChange}
onCheckedChange={(checked) => {
field.onChange(checked);
if (!checked) {
form.resetField('password');
}
}}
label={t('organization.updatePassword')}
/>
)}
Expand All @@ -248,6 +265,12 @@ export function EditMemberDialog({
{...register('password')}
className="w-full"
/>
{password && (
<ValidationCheckList
items={passwordValidationItems}
className="text-xs"
/>
)}
<Text variant="caption">
{t('organization.userMustUpdatePassword')}
</Text>
Expand Down
Loading
Loading