From a23753f65272dca3f0b54bed16d96512a3cbe20d Mon Sep 17 00:00:00 2001 From: schererleander Date: Fri, 26 Dec 2025 18:08:25 +0100 Subject: refactor(settings): split settings page into separate form components --- src/app/settings/page.tsx | 19 +- src/app/settings/password-form.tsx | 9 +- src/app/settings/profile-form.tsx | 12 +- src/app/settings/profile-image.tsx | 3 +- src/app/settings/settings-content.tsx | 537 ---------------------------------- src/app/settings/settings-form.tsx | 4 + 6 files changed, 19 insertions(+), 565 deletions(-) delete mode 100644 src/app/settings/settings-content.tsx (limited to 'src') diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index 75d9d3d..1baba3f 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -1,22 +1,21 @@ import { getServerSession } from "next-auth" -import { redirect } from "next/navigation" -import dbConnect from "@/lib/mongodb" -import User from "@/model/User" import { authOptions } from "@/lib/auth" -import SettingsContent from "./settings-content" +import { SettingsForm } from "./settings-form" +import { getUserProfile } from "@/services/user.service" export default async function SettingsPage() { const session = await getServerSession(authOptions) - + if (!session?.user?.email) { - redirect("/login") + // This case should be handled by middleware, but for type safety: + return null } - await dbConnect() - const user = await User.findOne({ email: session.user.email }).lean() as any + const user = await getUserProfile(session.user.email) if (!user) { - redirect("/login") + // This case suggests a data inconsistency (session exists but user not in DB) + return null } // Sanitize user object for client component @@ -27,5 +26,5 @@ export default async function SettingsPage() { twoFactorEnabled: !!user.twoFactorEnabled, } - return + return } diff --git a/src/app/settings/password-form.tsx b/src/app/settings/password-form.tsx index 2377408..f56ae9e 100644 --- a/src/app/settings/password-form.tsx +++ b/src/app/settings/password-form.tsx @@ -16,7 +16,6 @@ export function PasswordForm() { const [showCurrentPassword, setShowCurrentPassword] = useState(false) const [showNewPassword, setShowNewPassword] = useState(false) const [showConfirmPassword, setShowConfirmPassword] = useState(false) - const [isLoading, setIsLoading] = useState(false) const form = useForm({ resolver: zodResolver(passwordChangeSchema), @@ -28,8 +27,6 @@ export function PasswordForm() { }) const onSubmit = async (data: PasswordChangeInput) => { - setIsLoading(true) - try { const response = await fetch("/api/user/password", { method: "PATCH", @@ -51,8 +48,6 @@ export function PasswordForm() { form.reset() } catch { toast.error("An unexpected error occurred") - } finally { - setIsLoading(false) } } @@ -169,8 +164,8 @@ export function PasswordForm() {
Password must contain at least 8 characters with uppercase, lowercase, and a number.
- diff --git a/src/app/settings/profile-form.tsx b/src/app/settings/profile-form.tsx index 6f532f0..2cd8b52 100644 --- a/src/app/settings/profile-form.tsx +++ b/src/app/settings/profile-form.tsx @@ -4,8 +4,8 @@ import { useState } from "react" import { zodResolver } from "@hookform/resolvers/zod" import { useForm } from "react-hook-form" import { Loader2, User, Save } from "lucide-react" -import { toast } from "sonner" import { Session } from "next-auth" +import { toast } from "sonner" import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" @@ -22,8 +22,6 @@ interface ProfileFormProps { } export function ProfileForm({ user, update }: ProfileFormProps) { - const [isLoading, setIsLoading] = useState(false) - const form = useForm({ resolver: zodResolver(updateProfileSchema), defaultValues: { @@ -33,8 +31,6 @@ export function ProfileForm({ user, update }: ProfileFormProps) { }) const onSubmit = async (data: UpdateProfileInput) => { - setIsLoading(true) - try { const response = await fetch("/api/user/profile", { method: "PATCH", @@ -57,8 +53,6 @@ export function ProfileForm({ user, update }: ProfileFormProps) { toast.success("Profile updated successfully!") } catch { toast.error("An unexpected error occurred") - } finally { - setIsLoading(false) } } @@ -102,8 +96,8 @@ export function ProfileForm({ user, update }: ProfileFormProps) { )} /> - diff --git a/src/app/settings/profile-image.tsx b/src/app/settings/profile-image.tsx index 7b4114d..73c7e7a 100644 --- a/src/app/settings/profile-image.tsx +++ b/src/app/settings/profile-image.tsx @@ -97,7 +97,7 @@ export function ProfileImage({ user, update }: ProfileImageProps) { Upload or update your profile picture - +
@@ -136,7 +136,6 @@ export function ProfileImage({ user, update }: ProfileImageProps) {

Supported formats: JPEG, PNG, WebP, GIF. Maximum size: 10MB. - Images will be resized to 400x400 pixels.

data.newPassword === data.confirmPassword, { - message: "Passwords don't match", - path: ["confirmPassword"], -}) - -type ProfileFormData = UpdateProfileInput -type PasswordFormData = z.infer - -interface SettingsContentProps { - initialUser: { - name: string - email: string - image: string | null - twoFactorEnabled: boolean - } -} - -export default function SettingsContent({ initialUser }: SettingsContentProps) { - const { update } = useSession() - const router = useRouter() - const [twoFactorEnabled, setTwoFactorEnabled] = useState(initialUser.twoFactorEnabled) - const [is2FALoading, setIs2FALoading] = useState(false) - const [setupData, setSetupData] = useState<{ secret: string;qrCode: string } | null>(null) - const [verificationCode, setVerificationCode] = useState("") - const [isDialogOpen, setIsDialogOpen] = useState(false) - - // Existing state for other forms - const [isLoading, setIsLoading] = useState(false) - const [isImageLoading, setIsImageLoading] = useState(false) - const [profileImageUrl, setProfileImageUrl] = useState(initialUser.image) - - const profileForm = useForm({ - resolver: zodResolver(updateProfileSchema), - defaultValues: { - name: initialUser.name, - email: initialUser.email, - }, - }) - - const passwordForm = useForm({ - resolver: zodResolver(passwordChangeSchema), - defaultValues: { - currentPassword: "", - newPassword: "", - confirmPassword: "", - }, - }) - - // 2FA Handlers - const start2FASetup = async () => { - setIs2FALoading(true) - try { - const res = await fetch("/api/user/2fa", { method: "PUT" }) - const data = await res.json() - if (data.error) throw new Error(data.error) - setSetupData(data) - setIsDialogOpen(true) - } catch (error) { - toast.error("Failed to start 2FA setup") - } finally { - setIs2FALoading(false) - } - } - - const verifyAndEnable2FA = async () => { - if (verificationCode.length !== 6) { - toast.error("Please enter a 6-digit code") - return - } - - setIs2FALoading(true) - try { - const res = await fetch("/api/user/2fa", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - code: verificationCode, - secret: setupData?.secret, - }), - }) - const data = await res.json() - - if (data.error) throw new Error(data.error) - - setTwoFactorEnabled(true) - setIsDialogOpen(false) - toast.success("Two-factor authentication enabled") - router.refresh() - } catch (error) { - toast.error("Invalid verification code") - } finally { - setIs2FALoading(false) - setVerificationCode("") - } - } - - const disable2FA = async () => { - if (!confirm("Are you sure you want to disable 2FA? This will make your account less secure.")) return - - setIs2FALoading(true) - try { - const res = await fetch("/api/user/2fa", { method: "DELETE" }) - const data = await res.json() - - if (data.error) throw new Error(data.error) - - setTwoFactorEnabled(false) - toast.success("Two-factor authentication disabled") - router.refresh() - } catch (error) { - toast.error("Failed to disable 2FA") - } finally { - setIs2FALoading(false) - } - } - - const copyToClipboard = () => { - if (setupData?.secret) { - navigator.clipboard.writeText(setupData.secret) - toast.success("Secret copied to clipboard") - } - } - - // Existing Handlers (Profile, Password, Image) - const onProfileSubmit = async (data: ProfileFormData) => { - setIsLoading(true) - try { - const response = await fetch("/api/user/profile", { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(data), - }) - const result = await response.json() - if (!response.ok) { - toast.error(result.error || "Failed to update profile") - return - } - await update({ name: data.name, email: data.email }) - toast.success("Profile updated successfully!") - } catch { - toast.error("An unexpected error occurred") - } finally { - setIsLoading(false) - } - } - - const onPasswordSubmit = async (data: PasswordFormData) => { - setIsLoading(true) - try { - const response = await fetch("/api/user/password", { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - currentPassword: data.currentPassword, - newPassword: data.newPassword, - }), - }) - const result = await response.json() - if (!response.ok) { - toast.error(result.error || "Failed to update password") - return - } - toast.success("Password updated successfully!") - passwordForm.reset() - } catch { - toast.error("An unexpected error occurred") - } finally { - setIsLoading(false) - } - } - - const handleImageUpload = async (event: React.ChangeEvent) => { - const file = event.target.files?.[0] - if (!file) return - setIsImageLoading(true) - try { - const formData = new FormData() - formData.append('image', file) - const response = await fetch('/api/user/profile-image', { - method: 'POST', - body: formData, - }) - const result = await response.json() - if (!response.ok) { - toast.error(result.error || 'Failed to upload image') - return - } - setProfileImageUrl(result.profileImage.url) - toast.success('Profile image uploaded successfully!') - await update({ image: result.profileImage.url }) - } catch { - toast.error('An unexpected error occurred') - } finally { - setIsImageLoading(false) - } - } - - const handleImageDelete = async () => { - setIsImageLoading(true) - try { - const response = await fetch('/api/user/profile-image', { method: 'DELETE' }) - const result = await response.json() - if (!response.ok) { - toast.error(result.error || 'Failed to delete image') - return - } - setProfileImageUrl(null) - toast.success('Profile image deleted successfully!') - await update({ image: null }) - } catch { - toast.error('An unexpected error occurred') - } finally { - setIsImageLoading(false) - } - } - - return ( -
- - -
-
-
-

Account Settings

-

- Manage your account information and security settings -

-
- - {/* Profile Information */} - - - - - Profile Information - - Update your personal information - - -
- - ( - - Full Name - - - - - - )} - /> - ( - - Email Address - - - - - - )} - /> - - - -
-
- - - - {/* Profile Image */} - - - - - Profile Image - - Upload or update your profile picture - - -
- - - - {initialUser.name.charAt(0).toUpperCase()} - - - -
-
-
- -
- - {profileImageUrl && ( - - )} -
-

- Supported formats: JPEG, PNG, WebP, GIF. Maximum size: 10MB. -

-
-
-
-
- - - - {/* Two-Factor Authentication */} - - - - - Two-Factor Authentication - - - Add an extra layer of security to your account - - - -
-
-

Status: {twoFactorEnabled ? "Enabled" : "Disabled"}

-

- {twoFactorEnabled - ? "Your account is secured with 2FA." - : "Protect your account by enabling 2FA."} -

-
- {twoFactorEnabled ? ( - - ) : ( - - - - - - - Set up Two-Factor Authentication - - Scan the QR code with your authenticator app (like Google Authenticator or Authy). - - - - {setupData && ( -
-
- 2FA QR Code -
- -
- - {setupData.secret} - - -
- -
- - setVerificationCode(e.target.value.slice(0, 6))} - maxLength={6} - /> -
- - -
- )} -
-
- )} -
-
-
- - - - {/* Password Change */} - - - - - Change Password - - - Update your password to keep your account secure - - - -
- - ( - - Current Password - - - - - - )} - /> - ( - - New Password - - - - - - )} - /> - ( - - Confirm New Password - - - - - - )} - /> - - - -
-
-
-
-
- ) -} diff --git a/src/app/settings/settings-form.tsx b/src/app/settings/settings-form.tsx index 477473d..8b09cdf 100644 --- a/src/app/settings/settings-form.tsx +++ b/src/app/settings/settings-form.tsx @@ -5,12 +5,14 @@ import { Separator } from "@/components/ui/separator" import { ProfileForm } from "./profile-form" import { ProfileImage } from "./profile-image" import { PasswordForm } from "./password-form" +import { TwoFactorForm } from "./two-factor-form" interface SettingsFormProps { user: { name?: string | null email?: string | null image?: string | null + twoFactorEnabled: boolean } } @@ -31,6 +33,8 @@ export function SettingsForm({ user }: SettingsFormProps) { + +
-- cgit v1.3.1