feat: add password change functionality

- Add password change button to sidebar account menu
- Create PasswordChangeDialog component with shadcn UI components
- Implement backend API endpoint /api/v1/user/change-password
- Add form validation with current password verification
- Include internationalization support for Chinese and English
- Add proper error handling and success notifications

Co-Authored-By: Rock <rockchinq@gmail.com>
This commit is contained in:
Devin AI
2025-08-17 03:03:36 +00:00
parent 29f0075bd8
commit dd30d08c68
7 changed files with 257 additions and 0 deletions
@@ -0,0 +1,163 @@
'use client';
import * as React from 'react';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { httpClient } from '@/app/infra/http/HttpClient';
const getFormSchema = (t: (key: string) => string) =>
z
.object({
currentPassword: z
.string()
.min(1, { message: t('common.currentPasswordRequired') }),
newPassword: z
.string()
.min(1, { message: t('common.newPasswordRequired') }),
confirmNewPassword: z
.string()
.min(1, { message: t('common.confirmPasswordRequired') }),
})
.refine((data) => data.newPassword === data.confirmNewPassword, {
message: t('common.passwordsDoNotMatch'),
path: ['confirmNewPassword'],
});
interface PasswordChangeDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
export default function PasswordChangeDialog({
open,
onOpenChange,
}: PasswordChangeDialogProps) {
const { t } = useTranslation();
const [isSubmitting, setIsSubmitting] = useState(false);
const formSchema = getFormSchema(t);
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
currentPassword: '',
newPassword: '',
confirmNewPassword: '',
},
});
const onSubmit = async (values: z.infer<typeof formSchema>) => {
setIsSubmitting(true);
try {
await httpClient.changePassword(
values.currentPassword,
values.newPassword,
);
toast.success(t('common.changePasswordSuccess'));
form.reset();
onOpenChange(false);
} catch {
toast.error(t('common.changePasswordFailed'));
} finally {
setIsSubmitting(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{t('common.changePassword')}</DialogTitle>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="currentPassword"
render={({ field }) => (
<FormItem>
<FormLabel>{t('common.currentPassword')}</FormLabel>
<FormControl>
<Input
type="password"
placeholder={t('common.enterCurrentPassword')}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="newPassword"
render={({ field }) => (
<FormItem>
<FormLabel>{t('common.newPassword')}</FormLabel>
<FormControl>
<Input
type="password"
placeholder={t('common.enterNewPassword')}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmNewPassword"
render={({ field }) => (
<FormItem>
<FormLabel>{t('common.confirmNewPassword')}</FormLabel>
<FormControl>
<Input
type="password"
placeholder={t('common.enterConfirmPassword')}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isSubmitting}
>
{t('common.cancel')}
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? t('common.saving') : t('common.save')}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
}