From 92caf338fd62a01403ddde8b1589ea5c6799c4a5 Mon Sep 17 00:00:00 2001 From: Arnab Kumar Dey Date: Sun, 15 Feb 2026 10:17:00 +0530 Subject: [PATCH] feat: Improvement of Login and Sign-Up Page #101 --- savebook/app/(auth)/login/page.js | 6 +- savebook/app/(auth)/register/page.js | 349 ++++++--------- savebook/app/api/auth/register/route.js | 39 +- savebook/app/api/auth/update-profile/route.js | 38 +- savebook/app/api/auth/user/route.js | 7 +- savebook/app/profile/page.js | 400 ++++++++++++------ savebook/context/auth/AuthState.js | 44 +- savebook/lib/models/User.js | 23 +- 8 files changed, 506 insertions(+), 400 deletions(-) diff --git a/savebook/app/(auth)/login/page.js b/savebook/app/(auth)/login/page.js index 852c736..aec8f06 100644 --- a/savebook/app/(auth)/login/page.js +++ b/savebook/app/(auth)/login/page.js @@ -110,7 +110,7 @@ const LoginForm = () => { {/* Username */}
{ required disabled={isLoading} className="w-full px-4 py-3 bg-gray-700 border border-gray-600 rounded-lg text-white" - placeholder="Enter username" + placeholder="Enter username or email" />
@@ -178,7 +178,7 @@ const LoginForm = () => { onClick={() => window.location.href = "/api/auth/github"} className="w-full flex items-center justify-center gap-3 bg-gray-700 hover:bg-gray-600 text-white py-3 rounded-lg transition-colors" > - + Continue with GitHub diff --git a/savebook/app/(auth)/register/page.js b/savebook/app/(auth)/register/page.js index f793b78..e89261a 100644 --- a/savebook/app/(auth)/register/page.js +++ b/savebook/app/(auth)/register/page.js @@ -4,7 +4,6 @@ import { useRouter } from 'next/navigation'; import React, { useState, useEffect } from 'react'; import toast from 'react-hot-toast'; import Link from 'next/link'; - import { Eye, EyeOff } from 'lucide-react'; // Signup Form Component @@ -12,14 +11,16 @@ const SignupForm = () => { const { register, isAuthenticated } = useAuth(); const [credentials, setCredentials] = useState({ username: '', + email: '', password: '', - confirmPassword: '' - }); - const [errors, setErrors] = useState({ - username: '', - password: '', - confirmPassword: '' + confirmPassword: '', + name: '', + education: '', + course: '', + phoneNumber: '', + subjectsOfInterest: '' }); + const [errors, setErrors] = useState({}); const [isLoading, setIsLoading] = useState(false); const [showPassword, setShowPassword] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false); @@ -44,14 +45,23 @@ const SignupForm = () => { // Validate and collect errors const newErrors = {}; - if (!credentials.username.trim()) { - newErrors.username = 'Username is required'; + if (!credentials.username.trim()) newErrors.username = 'Username is required'; + if (!credentials.name.trim()) newErrors.name = 'Full Name is required'; + + const emailRegex = /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/; + if (!credentials.email.trim()) { + newErrors.email = 'Email is required'; + } else if (!emailRegex.test(credentials.email)) { + newErrors.email = 'Invalid email address'; } if (!credentials.password) { newErrors.password = 'Password is required'; - } else if (credentials.password.length < 6) { - newErrors.password = 'Password must be at least 6 characters'; + } else { + if (credentials.password.length < 8) newErrors.password = 'Password must be at least 8 characters'; + else if (!/[A-Z]/.test(credentials.password)) newErrors.password = 'Password must contain at least one uppercase letter'; + else if (!/[0-9]/.test(credentials.password)) newErrors.password = 'Password must contain at least one number'; + else if (!/[!@#$%^&*]/.test(credentials.password)) newErrors.password = 'Password must contain at least one special character (!@#$%^&*)'; } if (!credentials.confirmPassword) { @@ -60,6 +70,10 @@ const SignupForm = () => { newErrors.confirmPassword = 'Passwords do not match'; } + if (credentials.phoneNumber && !/^\d{10}$/.test(credentials.phoneNumber)) { + newErrors.phoneNumber = 'Phone number must be 10 digits'; + } + // If validation errors exist, show them and return if (Object.keys(newErrors).length > 0) { setErrors(newErrors); @@ -69,67 +83,30 @@ const SignupForm = () => { setIsLoading(true); try { + // Prepare data for registration + const userData = { + username: credentials.username, + password: credentials.password, + email: credentials.email, + name: credentials.name, + education: credentials.education, + course: credentials.course, + phoneNumber: credentials.phoneNumber, + subjectsOfInterest: credentials.subjectsOfInterest.split(',').map(s => s.trim()).filter(s => s) + }; + // Use the register method from AuthContext - const result = await register( - credentials.username, - credentials.password - ); + const result = await register(userData); if (result.success) { toast.success("Account created successfully! 🎉"); router.push("/login") - // The useEffect will handle the redirect } else { toast.error(result.message || "Registration failed"); } } catch (error) { console.error("Registration error:", error); - - // Attempt to extract meaningful error message - let errorMessage = "Something went wrong. Please try again."; - - try { - // Check if response exists and extract message from JSON - if (error.response?.data) { - const data = error.response.data; - - // Try to get message from JSON response - if (typeof data === 'object' && data !== null) { - if (data.message) { - errorMessage = data.message; - } else if (data.error) { - errorMessage = data.error; - } - } - // If data is a string (HTML), it means we got an error page - else if (typeof data === 'string' && data.includes('<')) { - // HTML response detected, use status code instead - throw new Error('HTML_RESPONSE'); - } - } - - // Handle HTTP status codes if no JSON message was found - if (errorMessage.includes('Something went wrong')) { - if (error.response?.status === 500) { - errorMessage = "Server error. Please try again later."; - } else if (error.response?.status === 400) { - errorMessage = "Invalid registration details. Please check your input."; - } - } - } catch (parseError) { - // If JSON parsing failed or HTML was detected, use status-based message - if (error.response?.status === 500) { - errorMessage = "Server error. Please try again later."; - } else if (error.response?.status === 400) { - errorMessage = "Invalid registration details. Please check your input."; - } else if (error.response?.status) { - errorMessage = `Registration failed with error ${error.response.status}. Please try again.`; - } else if (error.message && !error.message.includes(' { } return ( -
- {/* Username Field */} + +
+ {/* Full Name */} +
+ + + {errors.name &&

{errors.name}

} +
+ + {/* Username */} +
+ + + {errors.username &&

{errors.username}

} +
+
+ + {/* Email */}
- - - {errors.username && ( -

{errors.username}

- )} + + + {errors.email &&

{errors.email}

} +
+ +
+ {/* Phone Number */} +
+ + + {errors.phoneNumber &&

{errors.phoneNumber}

} +
+ + {/* Education */} +
+ + +
+
+ +
+ {/* Course */} +
+ + +
+ + {/* Subjects of Interest */} +
+ + +
- {/* Password Field */} + {/* Password */}
- +
- -
- {errors.password && ( -

{errors.password}

- )} - {!errors.password && ( -

Password must be at least 6 characters long.

- )} + {errors.password &&

{errors.password}

} + {!errors.password &&

Min 8 chars, uppercase, number, special char.

}
- {/* Confirm Password Field */} + {/* Confirm Password */}
- +
- -
- {errors.confirmPassword && ( -

{errors.confirmPassword}

- )} + {errors.confirmPassword &&

{errors.confirmPassword}

}
{/* Submit Button */} - {/* Login link */} -
+
Already have an account?{' '} - { - if (isLoading || isAuthenticated) { - e.preventDefault(); - } - }} - > - Login - + Login
@@ -287,31 +240,10 @@ const SignupForm = () => { const SignupFormSkeleton = () => { return (
- {/* Username Field Skeleton */} -
-
-
-
- - {/* Password Field Skeleton */} -
-
-
-
- - {/* Confirm Password Field Skeleton */} -
-
-
-
- - {/* Button Skeleton */} -
- - {/* Sign up link Skeleton */} -
-
-
+
+
+
+
); }; @@ -320,58 +252,23 @@ const SignupFormSkeleton = () => { export default function Signup() { const { isAuthenticated } = useAuth(); - // Show loading while checking initial auth state if (isAuthenticated === undefined) { return (
- {/* Header Skeleton */} -
-
-
-
-
- - {/* Form Skeleton */} -
- -
+
); } - // // Redirect if already authenticated - // if (isAuthenticated) { - // return ( - //
- //
- //
- //

Redirecting...

- //
- //
- // ); - // } - return (
-
- {/* Header */} +
-
- - - -
-

- Create Account -

-

- Join us and get started -

+

Create Account

+

Join us and get started

- - {/* Signup Form */}
diff --git a/savebook/app/api/auth/register/route.js b/savebook/app/api/auth/register/route.js index 1dc57da..4719791 100644 --- a/savebook/app/api/auth/register/route.js +++ b/savebook/app/api/auth/register/route.js @@ -6,36 +6,63 @@ export async function POST(request) { try { await dbConnect(); - const { username, password } = await request.json(); + const { username, password, email, education, course, phoneNumber, subjectsOfInterest, name } = await request.json(); // ✅ Input validation if ( !username || !password || + !email || typeof username !== "string" || typeof password !== "string" || + typeof email !== "string" || password.length < 6 ) { return NextResponse.json( - { success: false, message: "Invalid input" }, + { success: false, message: "Invalid input. Username, password (min 6 chars), and email are required." }, { status: 400 } ); } - // ✅ Prevent username enumeration - const existingUser = await User.findOne({ username }); + // Basic email validation regex + const emailRegex = /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/; + if (!emailRegex.test(email)) { + return NextResponse.json( + { success: false, message: "Invalid email format." }, + { status: 400 } + ); + } + + // ✅ Prevent username/email enumeration + const existingUser = await User.findOne({ $or: [{ username }, { email }] }); if (existingUser) { return NextResponse.json( - { success: false, message: "Unable to create account" }, + { success: false, message: "Username or Email already exists" }, { status: 400 } ); } + // Split name into firstName and lastName if provided + let firstName = ''; + let lastName = ''; + if (name) { + const parts = name.trim().split(' '); + firstName = parts[0]; + lastName = parts.slice(1).join(' '); + } + // ✅ Create user await User.create({ username, - password + password, + email, + education, + course, + phoneNumber, + subjectsOfInterest: Array.isArray(subjectsOfInterest) ? subjectsOfInterest : [], + firstName, + lastName }); return NextResponse.json( diff --git a/savebook/app/api/auth/update-profile/route.js b/savebook/app/api/auth/update-profile/route.js index c584cf5..f19749e 100644 --- a/savebook/app/api/auth/update-profile/route.js +++ b/savebook/app/api/auth/update-profile/route.js @@ -11,33 +11,46 @@ export async function PUT(request) { // Get token from cookies const authtoken = request.cookies.get("authToken"); - + if (!authtoken) { return NextResponse.json({ success: false, message: "Unauthorized - No token provided" }, { status: 401 }); } // Verify token - const decoded = verifyJwtToken(authtoken.value); - + const decoded = await verifyJwtToken(authtoken.value); + if (!decoded || !decoded.success) { return NextResponse.json({ success: false, message: "Unauthorized - Invalid token" }, { status: 401 }); } - + // Get user ID from token const userId = new mongoose.Types.ObjectId(decoded.userId); // Get updated user data from request - const { profileImage, firstName, lastName, bio, location } = await request.json(); + const { profileImage, firstName, lastName, bio, location, email, education, course, phoneNumber, subjectsOfInterest } = await request.json(); + + // Check if email is being updated and if it's already taken + if (email) { + const emailExists = await User.findOne({ email, _id: { $ne: userId } }); + if (emailExists) { + return NextResponse.json({ success: false, message: "Email already in use" }, { status: 400 }); + } + } // Update user const updatedUser = await User.findByIdAndUpdate( userId, - { + { ...(profileImage !== undefined && { profileImage }), ...(firstName !== undefined && { firstName }), ...(lastName !== undefined && { lastName }), ...(bio !== undefined && { bio }), - ...(location !== undefined && { location }) + ...(location !== undefined && { location }), + ...(email !== undefined && { email }), + ...(education !== undefined && { education }), + ...(course !== undefined && { course }), + ...(phoneNumber !== undefined && { phoneNumber }), + ...(subjectsOfInterest !== undefined && { subjectsOfInterest }) }, { new: true, select: "-password" } // Return updated user without password ); @@ -47,16 +60,21 @@ export async function PUT(request) { } // Return success response with updated user data - return NextResponse.json({ - success: true, + return NextResponse.json({ + success: true, message: "Profile updated successfully", user: { username: updatedUser.username, + email: updatedUser.email, profileImage: updatedUser.profileImage, firstName: updatedUser.firstName, lastName: updatedUser.lastName, bio: updatedUser.bio, - location: updatedUser.location + location: updatedUser.location, + education: updatedUser.education, + course: updatedUser.course, + phoneNumber: updatedUser.phoneNumber, + subjectsOfInterest: updatedUser.subjectsOfInterest } }, { status: 200 }); diff --git a/savebook/app/api/auth/user/route.js b/savebook/app/api/auth/user/route.js index 97eca38..3326551 100644 --- a/savebook/app/api/auth/user/route.js +++ b/savebook/app/api/auth/user/route.js @@ -48,7 +48,12 @@ export async function GET(request) { firstName: user.firstName, lastName: user.lastName, bio: user.bio, - location: user.location + location: user.location, + email: user.email, + education: user.education, + course: user.course, + phoneNumber: user.phoneNumber, + subjectsOfInterest: user.subjectsOfInterest } }, { status: 200 }); diff --git a/savebook/app/profile/page.js b/savebook/app/profile/page.js index d1f4beb..ac88114 100644 --- a/savebook/app/profile/page.js +++ b/savebook/app/profile/page.js @@ -11,7 +11,12 @@ export default function ProfilePage() { firstName: '', lastName: '', bio: '', - location: '' + location: '', + email: '', + education: '', + course: '', + phoneNumber: '', + subjectsOfInterest: '' }); const [imagePreview, setImagePreview] = useState(''); const [message, setMessage] = useState(''); @@ -26,7 +31,12 @@ export default function ProfilePage() { firstName: user.firstName || '', lastName: user.lastName || '', bio: user.bio || '', - location: user.location || '' + location: user.location || '', + email: user.email || '', + education: user.education || '', + course: user.course || '', + phoneNumber: user.phoneNumber || '', + subjectsOfInterest: user.subjectsOfInterest ? (Array.isArray(user.subjectsOfInterest) ? user.subjectsOfInterest.join(', ') : user.subjectsOfInterest) : '' }); setImagePreview(user.profileImage || ''); setIsDataLoaded(true); @@ -43,35 +53,35 @@ export default function ProfilePage() { const handleImageChange = async (e) => { const file = e.target.files[0]; - + if (file) { // Validate file type if (!file.type.match('image.*')) { setError('Please select an image file'); return; } - + // Validate file size (max 5MB) if (file.size > 5 * 1024 * 1024) { setError('File size exceeds 5MB limit'); return; } - + // Show loading state setMessage('Uploading image...'); - + const formData = new FormData(); formData.append('image', file); - + try { const response = await fetch('/api/upload', { method: 'POST', body: formData, credentials: 'include' }); - + const result = await response.json(); - + if (result.success) { setImagePreview(result.imageUrl); setFormData(prev => ({ @@ -80,7 +90,7 @@ export default function ProfilePage() { })); setMessage('Image uploaded successfully!'); setError(''); // Clear any previous error - + // Clear message after 2 seconds setTimeout(() => setMessage(''), 2000); } else { @@ -109,7 +119,12 @@ export default function ProfilePage() { firstName: formData.firstName, lastName: formData.lastName, bio: formData.bio, - location: formData.location + location: formData.location, + email: formData.email, + education: formData.education, + course: formData.course, + phoneNumber: formData.phoneNumber, + subjectsOfInterest: formData.subjectsOfInterest.split(',').map(s => s.trim()).filter(s => s) }), credentials: 'include' }); @@ -118,28 +133,33 @@ export default function ProfilePage() { if (data.success) { setMessage('Profile updated successfully!'); - + // Update form data to reflect the changes immediately setFormData({ profileImage: data.user.profileImage, firstName: data.user.firstName, lastName: data.user.lastName, bio: data.user.bio, - location: data.user.location + location: data.user.location, + email: data.user.email, + education: data.user.education, + course: data.user.course, + phoneNumber: data.user.phoneNumber, + subjectsOfInterest: Array.isArray(data.user.subjectsOfInterest) ? data.user.subjectsOfInterest.join(', ') : data.user.subjectsOfInterest }); - + // Update image preview setImagePreview(data.user.profileImage); - + // Refresh user data from the server to ensure we have the latest data in context if (checkUserAuthentication) { await checkUserAuthentication(); } - + setTimeout(() => { setIsEditing(false); }, 500); - + setTimeout(() => { setMessage(''); // Clear message // Optionally redirect after update @@ -174,11 +194,11 @@ export default function ProfilePage() {
); } - + const handleEditClick = () => { setIsEditing(true); }; - + const handleCancelEdit = () => { setIsEditing(false); // Reset form to current user data @@ -188,39 +208,44 @@ export default function ProfilePage() { firstName: user.firstName || '', lastName: user.lastName || '', bio: user.bio || '', - location: user.location || '' + location: user.location || '', + email: user.email || '', + education: user.education || '', + course: user.course || '', + phoneNumber: user.phoneNumber || '', + subjectsOfInterest: user.subjectsOfInterest ? (Array.isArray(user.subjectsOfInterest) ? user.subjectsOfInterest.join(', ') : user.subjectsOfInterest) : '' }); setImagePreview(user.profileImage || ''); } }; - + return (

Edit Profile

- + {message && (
{message}
)} - + {error && (
{error}
)} - + {!isEditing ? (
{/* Profile Preview Card */}

Your Profile

-
- +
{user?.profileImage ? ( - Profile ) : ( @@ -249,7 +274,7 @@ export default function ProfilePage() {

{user?.username || 'N/A'}

- +

Full Name

@@ -258,146 +283,259 @@ export default function ProfilePage() { {(user?.firstName || user?.lastName) ? '' : 'N/A'}

- +

Bio

{user?.bio || 'N/A'}

- +

Location

{user?.location || 'N/A'}

+ +
+

Email

+

+ {user?.email || 'N/A'} +

+
+ +
+

Phone

+

+ {user?.phoneNumber || 'N/A'} +

+
+ +
+

Education

+

+ {user?.course ? `${user.course} at ` : ''}{user?.education || 'N/A'} +

+
+ +
+

Interests

+
+ {user?.subjectsOfInterest && user.subjectsOfInterest.length > 0 ? ( + user.subjectsOfInterest.map((subject, index) => ( + + {subject} + + )) + ) : ( +

N/A

+ )} +
+
) : isDataLoaded ? (
-
-
-
- {imagePreview ? ( - Profile Preview +
+
+ {imagePreview ? ( + Profile Preview + ) : ( +
+ + {user?.username?.charAt(0)?.toUpperCase() || 'U'} + +
+ )} +
+
-
-
-
-
- -
- {user?.username || 'N/A'} +
+
+ +
+ {user?.username || 'N/A'} +
-
- -
+ +
+
+ + +
+ +
+ + +
+
+ +
+ + +
+ +
+ + +
+
-
+ +
+
+ + +
+ +
+ + +
+
+ +
+
- +
-
- -
- - -
- -
- - -
-
-
- - -
- +
+ + +
+ ) : (
diff --git a/savebook/context/auth/AuthState.js b/savebook/context/auth/AuthState.js index dcdc85d..a8722be 100644 --- a/savebook/context/auth/AuthState.js +++ b/savebook/context/auth/AuthState.js @@ -26,9 +26,9 @@ const AuthProvider = ({ children }) => { }, credentials: 'include', // Important: sends cookies with request }); - + const data = await response.json(); - + if (data.success) { setUser(data.user); setIsAuthenticated(true); @@ -59,22 +59,22 @@ const AuthProvider = ({ children }) => { }); const data = await response.json(); - + if (data.success) { setUser(data.data.user); setIsAuthenticated(true); - return { success: true, message: data.message,recoveryCodes: data.data?.recoveryCodes || null }; + return { success: true, message: data.message, recoveryCodes: data.data?.recoveryCodes || null }; } else { - return { - success: false, - message: data.message || "Login failed" + return { + success: false, + message: data.message || "Login failed" }; } } catch (error) { console.error("Login error:", error); - return { - success: false, - message: "An error occurred during login" + return { + success: false, + message: "An error occurred during login" }; } finally { setLoading(false); @@ -82,7 +82,7 @@ const AuthProvider = ({ children }) => { }; // Register function - const register = async (username, password) => { + const register = async (userData) => { try { setLoading(true); const response = await fetch('/api/auth/register', { @@ -90,28 +90,28 @@ const AuthProvider = ({ children }) => { headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ username,password }), + body: JSON.stringify(userData), credentials: 'include' }); const data = await response.json(); - + if (data.success) { return { - success:true, - message:data.message + success: true, + message: data.message } } else { - return { - success: false, - message: data.message || data.error || "Registration failed" + return { + success: false, + message: data.message || data.error || "Registration failed" }; } } catch (error) { console.error("Registration error:", error); - return { - success: false, - message: "An error occurred during registration" + return { + success: false, + message: "An error occurred during registration" }; } finally { setLoading(false); @@ -126,7 +126,7 @@ const AuthProvider = ({ children }) => { method: 'GET', credentials: 'include' }); - + setUser(null); setIsAuthenticated(false); router.push('/'); diff --git a/savebook/lib/models/User.js b/savebook/lib/models/User.js index f95afde..2e313c7 100644 --- a/savebook/lib/models/User.js +++ b/savebook/lib/models/User.js @@ -44,7 +44,28 @@ const UserSchema = new Schema({ } ], - + email: { + type: String, + required: true, + unique: true, + match: [/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/, 'Please fill a valid email address'] + }, + education: { + type: String, + default: '' + }, + course: { + type: String, + default: '' + }, + phoneNumber: { + type: String, + default: '' + }, + subjectsOfInterest: { + type: [String], + default: [] + }, }); // Password hashing middleware