From e3d934615a7a2a9d36392c6598abdd8c3705ca25 Mon Sep 17 00:00:00 2001 From: Saahi30 Date: Thu, 5 Jun 2025 04:53:28 +0530 Subject: [PATCH 1/4] feat: improved Supabase password reset flow, username check, and UX enhancements --- Backend/.env-example | 8 - Frontend/src/context/AuthContext.tsx | 11 +- Frontend/src/pages/ForgotPassword.tsx | 33 +- Frontend/src/pages/ResetPassword.tsx | 468 +++++++++++++------------- Frontend/src/pages/Signup.tsx | 66 +++- 5 files changed, 316 insertions(+), 270 deletions(-) delete mode 100644 Backend/.env-example diff --git a/Backend/.env-example b/Backend/.env-example deleted file mode 100644 index c5c7345..0000000 --- a/Backend/.env-example +++ /dev/null @@ -1,8 +0,0 @@ -user=postgres -password=[YOUR-PASSWORD] -host= -port=5432 -dbname=postgres -GROQ_API_KEY= -SUPABASE_URL= -SUPABASE_KEY= diff --git a/Frontend/src/context/AuthContext.tsx b/Frontend/src/context/AuthContext.tsx index 7e69984..91d179f 100644 --- a/Frontend/src/context/AuthContext.tsx +++ b/Frontend/src/context/AuthContext.tsx @@ -5,7 +5,7 @@ import { ReactNode, useEffect, } from "react"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useLocation } from "react-router-dom"; import { supabase, User } from "../utils/supabase"; interface AuthContextType { @@ -25,6 +25,7 @@ export const AuthProvider = ({ children }: AuthProviderProps) => { const [user, setUser] = useState(null); const [isAuthenticated, setIsAuthenticated] = useState(false); const navigate = useNavigate(); + const location = useLocation(); useEffect(() => { supabase.auth.getSession().then(({ data }) => { @@ -34,14 +35,18 @@ export const AuthProvider = ({ children }: AuthProviderProps) => { const { data: listener } = supabase.auth.onAuthStateChange( (event, session) => { setUser(session?.user || null); - if (session?.user) { + if ( + session?.user && + location.pathname !== "/reset-password" && + event !== "PASSWORD_RECOVERY" + ) { navigate("/dashboard"); } } ); return () => listener.subscription.unsubscribe(); - }, []); + }, [location.pathname, navigate]); const login = () => { setIsAuthenticated(true); diff --git a/Frontend/src/pages/ForgotPassword.tsx b/Frontend/src/pages/ForgotPassword.tsx index a7896ea..a7ce192 100644 --- a/Frontend/src/pages/ForgotPassword.tsx +++ b/Frontend/src/pages/ForgotPassword.tsx @@ -1,24 +1,41 @@ import { useState } from "react"; import { Link } from "react-router-dom"; import { ArrowLeft, Check, Rocket } from "lucide-react"; +import { supabase } from "../utils/supabase"; export default function ForgotPasswordPage() { const [email, setEmail] = useState(""); const [isLoading, setIsLoading] = useState(false); const [isSubmitted, setIsSubmitted] = useState(false); const [error, setError] = useState(""); + const [showSignupPrompt, setShowSignupPrompt] = useState(false); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); setError(""); + setShowSignupPrompt(false); try { - // In a real app, you would call your auth API here - await new Promise((resolve) => setTimeout(resolve, 1500)); + // Check if email exists in users table + const { data: users, error: userError } = await supabase + .from("users") + .select("id") + .eq("email", email) + .maybeSingle(); + if (userError) throw userError; + if (!users) { + setShowSignupPrompt(true); + setIsLoading(false); + return; + } + const { error } = await supabase.auth.resetPasswordForEmail(email, { + redirectTo: window.location.origin + "/reset-password" + }); + if (error) throw error; setIsSubmitted(true); - } catch (err) { - setError("Something went wrong. Please try again."); + } catch (err: any) { + setError(err.message || "Something went wrong. Please try again."); } finally { setIsLoading(false); } @@ -88,6 +105,12 @@ export default function ForgotPasswordPage() { )} + {showSignupPrompt && ( +
+ No account found with this email. Sign up? +
+ )} +
diff --git a/Frontend/src/pages/ResetPassword.tsx b/Frontend/src/pages/ResetPassword.tsx index e5f55f1..033021c 100644 --- a/Frontend/src/pages/ResetPassword.tsx +++ b/Frontend/src/pages/ResetPassword.tsx @@ -1,12 +1,12 @@ -import { useState } from "react"; +import { useState, useEffect, useRef } from "react"; import { Link } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom"; import { Check, Eye, EyeOff, Rocket } from "lucide-react"; +import { supabase } from "../utils/supabase"; export default function ResetPasswordPage() { const router = useNavigate(); const searchParams = useParams(); - const token = 5; const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); @@ -14,6 +14,32 @@ export default function ResetPasswordPage() { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(""); const [isSuccess, setIsSuccess] = useState(false); + const [progress, setProgress] = useState(0); + const progressRef = useRef(null); + + useEffect(() => { + // Supabase will automatically handle the session if the user comes from the reset link + // No need to manually extract token + }, []); + + useEffect(() => { + if (isSuccess) { + setProgress(0); + progressRef.current = setInterval(() => { + setProgress((prev) => { + if (prev >= 100) { + if (progressRef.current) clearInterval(progressRef.current); + router("/dashboard"); + return 100; + } + return prev + (100 / 30); // 3 seconds, 100ms interval + }); + }, 100); + } + return () => { + if (progressRef.current) clearInterval(progressRef.current); + }; + }, [isSuccess, router]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -27,16 +53,11 @@ export default function ResetPasswordPage() { setError(""); try { - // In a real app, you would call your auth API here with the token and new password - await new Promise((resolve) => setTimeout(resolve, 1500)); + const { error } = await supabase.auth.updateUser({ password }); + if (error) throw error; setIsSuccess(true); - - // Redirect to login after 3 seconds - setTimeout(() => { - router("/login"); - }, 3000); - } catch (err) { - setError("Something went wrong. Please try again."); + } catch (err: any) { + setError(err.message || "Something went wrong. Please try again."); } finally { setIsLoading(false); } @@ -68,8 +89,7 @@ export default function ResetPasswordPage() { const { strength, text, color } = passwordStrength(); - // If no token is provided, show an error - if (!token && !isSuccess) { + if (isSuccess) { return (
@@ -83,27 +103,24 @@ export default function ResetPasswordPage() {
-

- Invalid or Expired Link + Password Changed Successfully

- This password reset link is invalid or has expired. Please - request a new one. + Redirecting you to your application...

- - Request New Link - +
+
+
-
© 2024 Inpact. All rights reserved.
@@ -129,231 +146,200 @@ export default function ResetPasswordPage() {
- {isSuccess ? ( -
-
- -
-

- Password Reset Successful -

-

- Your password has been reset successfully. You will be - redirected to the login page shortly. -

- - Go to Login - + {error && ( +
+ {error}
- ) : ( - <> -

- Create new password -

-

- Your new password must be different from previously used - passwords -

- - {error && ( -
- {error} -
- )} - - -
- -
- setPassword(e.target.value)} - required - className="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-purple-500 dark:focus:ring-purple-400 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white transition-all duration-200" - placeholder="••••••••" - /> - -
+ )} - {password && ( -
-
- - Password strength: {text} - - - {strength}/4 - -
-
-
-
-
    -
  • - = 8 - ? "text-green-500" - : "text-gray-400" - }`} - > - {password.length >= 8 ? ( - - ) : ( - "○" - )} - - At least 8 characters -
  • -
  • - - {/[A-Z]/.test(password) ? ( - - ) : ( - "○" - )} - - At least 1 uppercase letter -
  • -
  • - - {/[0-9]/.test(password) ? ( - - ) : ( - "○" - )} - - At least 1 number -
  • -
  • - - {/[^A-Za-z0-9]/.test(password) ? ( - - ) : ( - "○" - )} - - At least 1 special character -
  • -
-
+ +
+ +
+ setPassword(e.target.value)} + required + className="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-purple-500 dark:focus:ring-purple-400 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white transition-all duration-200" + placeholder="••••••••" + /> +
+ +
-
- -
- setConfirmPassword(e.target.value)} - required - className={`w-full px-4 py-3 rounded-lg border focus:outline-none focus:ring-2 focus:ring-purple-500 dark:focus:ring-purple-400 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white transition-all duration-200 ${ - confirmPassword && password !== confirmPassword - ? "border-red-500 dark:border-red-500" - : "border-gray-300 dark:border-gray-600" - }`} - placeholder="••••••••" - /> + {password && ( +
+
+ + Password strength: {text} + + + {strength}/4 +
- {confirmPassword && password !== confirmPassword && ( -

- Passwords don't match -

- )} +
+
+
+
    +
  • + = 8 + ? "text-green-500" + : "text-gray-400" + }`} + > + {password.length >= 8 ? ( + + ) : ( + "○" + )} + + At least 8 characters +
  • +
  • + + {/[A-Z]/.test(password) ? ( + + ) : ( + "○" + )} + + At least 1 uppercase letter +
  • +
  • + + {/[0-9]/.test(password) ? ( + + ) : ( + "○" + )} + + At least 1 number +
  • +
  • + + {/[^A-Za-z0-9]/.test(password) ? ( + + ) : ( + "○" + )} + + At least 1 special character +
  • +
+ )} +
- - - - )} + placeholder="••••••••" + /> +
+ {confirmPassword && password !== confirmPassword && ( +

+ Passwords don't match +

+ )} +
+ + +
diff --git a/Frontend/src/pages/Signup.tsx b/Frontend/src/pages/Signup.tsx index 64c1d08..bb9a07d 100644 --- a/Frontend/src/pages/Signup.tsx +++ b/Frontend/src/pages/Signup.tsx @@ -10,6 +10,7 @@ export default function SignupPage() { name: "", email: "", password: "", + username: "", accountType: "creator", }); const [showPassword, setShowPassword] = useState(false); @@ -41,7 +42,19 @@ export default function SignupPage() { setError(""); try { - const { name, email, password, accountType } = formData; + const { name, email, password, accountType, username } = formData; + // Check if username already exists + const { data: existingUser, error: userCheckError } = await supabase + .from("users") + .select("id") + .eq("username", username) + .maybeSingle(); + if (userCheckError) throw userCheckError; + if (existingUser) { + setError("This username is not available. Please choose another."); + setIsLoading(false); + return; + } const { data, error } = await supabase.auth.signUp({ email, password, @@ -55,25 +68,34 @@ export default function SignupPage() { return; } - console.log("Signup success", data); - if (data.user) { - console.log("Inserting user into profiles table:", data.user.id); - - const { error: profileError } = await supabase - .from("profiles") - .insert([{ id: data.user.id, accounttype: accountType }]); - - if (profileError) { - console.error("Profile insert error:", profileError.message); - setError("Error saving profile. Please try again."); + // Insert into users table + const { error: userInsertError } = await supabase + .from("users") + .insert([ + { + id: data.user.id, + username: username, + email: email, + password_hash: "managed_by_supabase_auth", + role: accountType, + profile_image: null, + bio: null, + created_at: new Date().toISOString(), + is_online: false, + last_seen: new Date().toISOString(), + }, + ]); + if (userInsertError) { + console.error("User insert error:", userInsertError.message); + setError("Error saving user. Please try again."); setIsLoading(false); return; } } setIsLoading(false); - navigate(`/BasicDetails/${user}`); + navigate(`/BasicDetails/${accountType}`); } catch (err) { setError("Something went wrong. Please try again."); } finally { @@ -172,6 +194,24 @@ export default function SignupPage() {
{step === 1 ? ( <> +
+ + +
diff --git a/Frontend/src/pages/ResetPassword.tsx b/Frontend/src/pages/ResetPassword.tsx index 033021c..2beff71 100644 --- a/Frontend/src/pages/ResetPassword.tsx +++ b/Frontend/src/pages/ResetPassword.tsx @@ -53,10 +53,14 @@ export default function ResetPasswordPage() { setError(""); try { + // Update the user's password using Supabase Auth + // Supabase automatically authenticates the user from the reset link const { error } = await supabase.auth.updateUser({ password }); if (error) throw error; setIsSuccess(true); + // After success,redirect to dashboard } catch (err: any) { + setError(err.message || "Something went wrong. Please try again."); } finally { setIsLoading(false); diff --git a/Frontend/src/pages/Signup.tsx b/Frontend/src/pages/Signup.tsx index bb9a07d..ecb74f9 100644 --- a/Frontend/src/pages/Signup.tsx +++ b/Frontend/src/pages/Signup.tsx @@ -43,7 +43,7 @@ export default function SignupPage() { try { const { name, email, password, accountType, username } = formData; - // Check if username already exists + // Check if username already exists in the users table before signup const { data: existingUser, error: userCheckError } = await supabase .from("users") .select("id") @@ -51,10 +51,12 @@ export default function SignupPage() { .maybeSingle(); if (userCheckError) throw userCheckError; if (existingUser) { + // Show error if username is not available setError("This username is not available. Please choose another."); setIsLoading(false); return; } + // Sign up user with Supabase Auth (handles password securely) const { data, error } = await supabase.auth.signUp({ email, password, @@ -62,6 +64,7 @@ export default function SignupPage() { }); if (error) { + console.error("Signup error:", error.message); setError(error.message); setIsLoading(false); @@ -69,7 +72,8 @@ export default function SignupPage() { } if (data.user) { - // Insert into users table + + // Use the id from Supabase Auth for consistency const { error: userInsertError } = await supabase .from("users") .insert([ @@ -77,7 +81,7 @@ export default function SignupPage() { id: data.user.id, username: username, email: email, - password_hash: "managed_by_supabase_auth", + password_hash: "managed_by_supabase_auth", role: accountType, profile_image: null, bio: null, @@ -87,6 +91,7 @@ export default function SignupPage() { }, ]); if (userInsertError) { + console.error("User insert error:", userInsertError.message); setError("Error saving user. Please try again."); setIsLoading(false); @@ -201,6 +206,7 @@ export default function SignupPage() { > Username + {/* User enters username here. Must be unique. */} Date: Thu, 5 Jun 2025 05:33:22 +0530 Subject: [PATCH 3/4] feat: username validation, canonical email, and frontend improvements (no password_hash removal) --- Backend/app/models/models.py | 2 +- Backend/app/routes/post.py | 2 +- Frontend/src/pages/Signup.tsx | 34 +++++++++++++++++++++++++++------- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/Backend/app/models/models.py b/Backend/app/models/models.py index 6a232a2..fec8452 100644 --- a/Backend/app/models/models.py +++ b/Backend/app/models/models.py @@ -27,7 +27,7 @@ class User(Base): id = Column(String, primary_key=True, default=generate_uuid) username = Column(String, unique=True, nullable=False) email = Column(String, unique=True, nullable=False) - password_hash = Column(Text, nullable=False) + password_hash = Column(Text, nullable=False) # Restored for now role = Column(String, nullable=False) # 'creator' or 'brand' profile_image = Column(Text, nullable=True) bio = Column(Text, nullable=True) diff --git a/Backend/app/routes/post.py b/Backend/app/routes/post.py index ce669d2..d0f1413 100644 --- a/Backend/app/routes/post.py +++ b/Backend/app/routes/post.py @@ -44,7 +44,7 @@ async def create_user(user: UserCreate): "id": user_id, "username": user.username, "email": user.email, - "password_hash": user.password_hash, + "password_hash": user.password_hash, "role": user.role, "profile_image": user.profile_image, "bio": user.bio, diff --git a/Frontend/src/pages/Signup.tsx b/Frontend/src/pages/Signup.tsx index ecb74f9..cc95e51 100644 --- a/Frontend/src/pages/Signup.tsx +++ b/Frontend/src/pages/Signup.tsx @@ -19,10 +19,14 @@ export default function SignupPage() { const [step, setStep] = useState(1); const [user, setuser] = useState("influencer"); const { login } = useAuth(); + const [usernameError, setUsernameError] = useState(""); const handleChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setFormData((prev) => ({ ...prev, [name]: value })); + if (name === "username") { + setUsernameError(validateUsername(value)); + } }; const handleAccountTypeChange = (type: string) => { @@ -30,6 +34,15 @@ export default function SignupPage() { setFormData((prev) => ({ ...prev, accountType: type })); }; + const validateUsername = (username: string) => { + // Username must be 3-20 chars, start with a letter, only letters, numbers, underscores + const regex = /^[a-zA-Z][a-zA-Z0-9_]{2,19}$/; + if (!regex.test(username)) { + return "Username must be 3-20 characters, start with a letter, and contain only letters, numbers, or underscores."; + } + return ""; + }; + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -41,6 +54,14 @@ export default function SignupPage() { setIsLoading(true); setError(""); + // Validate username before submitting + const usernameValidation = validateUsername(formData.username); + if (usernameValidation) { + setUsernameError(usernameValidation); + setIsLoading(false); + return; + } + try { const { name, email, password, accountType, username } = formData; // Check if username already exists in the users table before signup @@ -51,7 +72,6 @@ export default function SignupPage() { .maybeSingle(); if (userCheckError) throw userCheckError; if (existingUser) { - // Show error if username is not available setError("This username is not available. Please choose another."); setIsLoading(false); return; @@ -64,7 +84,6 @@ export default function SignupPage() { }); if (error) { - console.error("Signup error:", error.message); setError(error.message); setIsLoading(false); @@ -72,16 +91,15 @@ export default function SignupPage() { } if (data.user) { - - // Use the id from Supabase Auth for consistency + // Insert the new user into the users table with all required fields + // Use the id and canonical email from Supabase Auth for consistency const { error: userInsertError } = await supabase .from("users") .insert([ { id: data.user.id, username: username, - email: email, - password_hash: "managed_by_supabase_auth", + email: data.user.email, // Use canonical email from Supabase role: accountType, profile_image: null, bio: null, @@ -91,7 +109,6 @@ export default function SignupPage() { }, ]); if (userInsertError) { - console.error("User insert error:", userInsertError.message); setError("Error saving user. Please try again."); setIsLoading(false); @@ -217,6 +234,9 @@ export default function SignupPage() { className="w-full px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-2 focus:ring-purple-500 dark:focus:ring-purple-400 focus:border-transparent bg-white dark:bg-gray-700 text-gray-900 dark:text-white transition-all duration-200" placeholder="Choose a unique username" /> + {usernameError && ( +
{usernameError}
+ )}