diff --git a/Backend/.env-example b/Backend/.env-example index c5c7345..f23fbd0 100644 --- a/Backend/.env-example +++ b/Backend/.env-example @@ -5,4 +5,4 @@ port=5432 dbname=postgres GROQ_API_KEY= SUPABASE_URL= -SUPABASE_KEY= +SUPABASE_KEY= \ No newline at end of file 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/context/AuthContext.tsx b/Frontend/src/context/AuthContext.tsx index 7e69984..53c9507 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,20 @@ export const AuthProvider = ({ children }: AuthProviderProps) => { const { data: listener } = supabase.auth.onAuthStateChange( (event, session) => { setUser(session?.user || null); - if (session?.user) { + // Only redirect to dashboard if not on /reset-password and not during password recovery + + 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..f2d1a03 100644 --- a/Frontend/src/pages/ForgotPassword.tsx +++ b/Frontend/src/pages/ForgotPassword.tsx @@ -1,24 +1,44 @@ 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 the email exists in the users table before sending a reset link + const { data: users, error: userError } = await supabase + .from("users") + .select("id") + .eq("email", email) + .maybeSingle(); + if (userError) throw userError; + if (!users) { + // If the email does not exist, prompt the user to sign up + setShowSignupPrompt(true); + setIsLoading(false); + return; + } + // Send the password reset email using Supabase Auth + 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 +108,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..2beff71 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,15 @@ 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)); + // 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); - - // Redirect to login after 3 seconds - setTimeout(() => { - router("/login"); - }, 3000); - } catch (err) { - setError("Something went wrong. Please try again."); + // After success,redirect to dashboard + } catch (err: any) { + + setError(err.message || "Something went wrong. Please try again."); } finally { setIsLoading(false); } @@ -68,8 +93,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 +107,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 +150,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..cc95e51 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); @@ -18,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) => { @@ -29,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(); @@ -40,8 +54,29 @@ 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 } = formData; + const { name, email, password, accountType, username } = formData; + // Check if username already exists in the users table before signup + 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; + } + // Sign up user with Supabase Auth (handles password securely) const { data, error } = await supabase.auth.signUp({ email, password, @@ -55,25 +90,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 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: data.user.email, // Use canonical email from Supabase + 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 +216,28 @@ export default function SignupPage() {
{step === 1 ? ( <> +
+ + {/* User enters username here. Must be unique. */} + + {usernameError && ( +
{usernameError}
+ )} +