diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js index 326209d..2e012ff 100644 --- a/backend/middleware/auth.js +++ b/backend/middleware/auth.js @@ -11,7 +11,14 @@ const authenticateToken = async (req, res, next) => { try { const decoded = jwt.verify(token, process.env.JWT_SECRET); - const user = await User.findByPk(decoded.userId); + // Handle both 'userId' and 'id' for backward compatibility + const userId = decoded.userId || decoded.id; + + if (!userId) { + return res.status(401).json({ error: 'Invalid token format' }); + } + + const user = await User.findByPk(userId); if (!user) { return res.status(401).json({ error: 'User not found' }); @@ -20,6 +27,7 @@ const authenticateToken = async (req, res, next) => { req.user = user; next(); } catch (error) { + console.error('Auth middleware error:', error); return res.status(403).json({ error: 'Invalid or expired token' }); } }; diff --git a/backend/models/User.js b/backend/models/User.js index 609893f..e47938c 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -11,19 +11,19 @@ const User = sequelize.define('User', { username: { type: DataTypes.STRING, unique: true, - allowNull: false + allowNull: true }, email: { type: DataTypes.STRING, unique: true, - allowNull: false, + allowNull: true, validate: { isEmail: true } }, password: { type: DataTypes.STRING, - allowNull: false + allowNull: true }, firstName: { type: DataTypes.STRING, @@ -34,10 +34,39 @@ const User = sequelize.define('User', { allowNull: false }, phone: { + type: DataTypes.STRING, + unique: true, + allowNull: true + }, + phoneVerified: { + type: DataTypes.BOOLEAN, + defaultValue: false + }, + authProvider: { + type: DataTypes.ENUM('local', 'phone', 'google', 'apple', 'facebook'), + defaultValue: 'local' + }, + providerId: { + type: DataTypes.STRING, + allowNull: true + }, + address1: { type: DataTypes.STRING }, - address: { - type: DataTypes.TEXT + address2: { + type: DataTypes.STRING + }, + city: { + type: DataTypes.STRING + }, + state: { + type: DataTypes.STRING + }, + zipCode: { + type: DataTypes.STRING + }, + country: { + type: DataTypes.STRING }, profileImage: { type: DataTypes.STRING @@ -49,10 +78,12 @@ const User = sequelize.define('User', { }, { hooks: { beforeCreate: async (user) => { - user.password = await bcrypt.hash(user.password, 10); + if (user.password) { + user.password = await bcrypt.hash(user.password, 10); + } }, beforeUpdate: async (user) => { - if (user.changed('password')) { + if (user.changed('password') && user.password) { user.password = await bcrypt.hash(user.password, 10); } } @@ -60,6 +91,9 @@ const User = sequelize.define('User', { }); User.prototype.comparePassword = async function(password) { + if (!this.password) { + return false; + } return bcrypt.compare(password, this.password); }; diff --git a/backend/routes/phone-auth.js b/backend/routes/phone-auth.js new file mode 100644 index 0000000..7411526 --- /dev/null +++ b/backend/routes/phone-auth.js @@ -0,0 +1,151 @@ +const express = require("express"); +const router = express.Router(); +const jwt = require("jsonwebtoken"); +const { User } = require("../models"); + +// Temporary in-memory storage for verification codes +// In production, use Redis or a database +const verificationCodes = new Map(); + +// Generate random 6-digit code +const generateVerificationCode = () => { + return Math.floor(100000 + Math.random() * 900000).toString(); +}; + +// Send verification code +router.post("/send-code", async (req, res) => { + try { + const { phoneNumber } = req.body; + + if (!phoneNumber) { + return res.status(400).json({ message: "Phone number is required" }); + } + + // Generate and store verification code + const code = generateVerificationCode(); + verificationCodes.set(phoneNumber, { + code, + createdAt: Date.now(), + attempts: 0, + }); + + // TODO: Integrate with SMS service (Twilio, AWS SNS, etc.) + // For development, log the code + console.log(`Verification code for ${phoneNumber}: ${code}`); + + res.json({ + message: "Verification code sent", + // Remove this in production - only for development + devCode: code, + }); + } catch (error) { + console.error("Error sending verification code:", error); + res.status(500).json({ message: "Failed to send verification code" }); + } +}); + +// Verify code and create/login user +router.post("/verify-code", async (req, res) => { + try { + const { phoneNumber, code, firstName, lastName } = req.body; + + if (!phoneNumber || !code) { + return res + .status(400) + .json({ message: "Phone number and code are required" }); + } + + // Check verification code + const storedData = verificationCodes.get(phoneNumber); + + if (!storedData) { + return res + .status(400) + .json({ + message: "No verification code found. Please request a new one.", + }); + } + + // Check if code expired (10 minutes) + if (Date.now() - storedData.createdAt > 10 * 60 * 1000) { + verificationCodes.delete(phoneNumber); + return res + .status(400) + .json({ + message: "Verification code expired. Please request a new one.", + }); + } + + // Check attempts + if (storedData.attempts >= 3) { + verificationCodes.delete(phoneNumber); + return res + .status(400) + .json({ + message: "Too many failed attempts. Please request a new code.", + }); + } + + if (storedData.code !== code) { + storedData.attempts++; + return res.status(400).json({ message: "Invalid verification code" }); + } + + // Code is valid, remove it + verificationCodes.delete(phoneNumber); + + // Find or create user + let user = await User.findOne({ where: { phone: phoneNumber } }); + + if (!user) { + // New user - require firstName and lastName + if (!firstName || !lastName) { + return res.status(400).json({ + message: "First name and last name are required for new users", + isNewUser: true, + }); + } + + user = await User.create({ + phone: phoneNumber, + phoneVerified: true, + firstName, + lastName, + authProvider: "phone", + // Generate a unique username from phone + username: `user_${phoneNumber + .replace(/\D/g, "") + .slice(-6)}_${Date.now().toString(36)}`, + }); + } else { + // Existing user - update phone verification + await user.update({ phoneVerified: true }); + } + + // Generate JWT token + const token = jwt.sign( + { id: user.id, phone: user.phone }, + process.env.JWT_SECRET, + { expiresIn: "7d" } + ); + + res.json({ + message: "Phone verified successfully", + token, + user: { + id: user.id, + username: user.username, + firstName: user.firstName, + lastName: user.lastName, + phone: user.phone, + email: user.email, + phoneVerified: user.phoneVerified, + }, + }); + } catch (error) { + console.error("Error verifying code:", error); + res.status(500).json({ message: "Failed to verify code" }); + } +}); + +module.exports = router; diff --git a/backend/server.js b/backend/server.js index d56ad01..100ead0 100644 --- a/backend/server.js +++ b/backend/server.js @@ -11,6 +11,7 @@ const bodyParser = require('body-parser'); const { sequelize } = require('./models'); // Import from models/index.js to ensure associations are loaded const authRoutes = require('./routes/auth'); +const phoneAuthRoutes = require('./routes/phone-auth'); const userRoutes = require('./routes/users'); const itemRoutes = require('./routes/items'); const rentalRoutes = require('./routes/rentals'); @@ -23,13 +24,14 @@ app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); app.use('/api/auth', authRoutes); +app.use('/api/auth/phone', phoneAuthRoutes); app.use('/api/users', userRoutes); app.use('/api/items', itemRoutes); app.use('/api/rentals', rentalRoutes); app.use('/api/messages', messageRoutes); app.get('/', (req, res) => { - res.json({ message: 'Rentall API is running!' }); + res.json({ message: 'CommunityRentals.App API is running!' }); }); const PORT = process.env.PORT || 5000; diff --git a/frontend/public/index.html b/frontend/public/index.html index 7764f4a..a3cd82f 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -7,11 +7,11 @@ - Rentall - Equipment & Tool Rental Marketplace + CommunityRentals.App - Equipment & Tool Rental Marketplace diff --git a/frontend/src/components/AuthModal.tsx b/frontend/src/components/AuthModal.tsx new file mode 100644 index 0000000..b28c163 --- /dev/null +++ b/frontend/src/components/AuthModal.tsx @@ -0,0 +1,523 @@ +import React, { useState, useEffect } from "react"; +import { useAuth } from "../contexts/AuthContext"; + +interface AuthModalProps { + show: boolean; + onHide: () => void; + initialMode?: "login" | "signup"; +} + +const AuthModal: React.FC = ({ + show, + onHide, + initialMode = "login", +}) => { + const [mode, setMode] = useState<"login" | "signup">(initialMode); + const [authMethod, setAuthMethod] = useState<"phone" | "email" | null>(null); + const [phoneNumber, setPhoneNumber] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); + const [verificationCode, setVerificationCode] = useState(""); + const [showVerification, setShowVerification] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + const { login, register, updateUser } = useAuth(); + + // Update mode when modal is opened with different initialMode + useEffect(() => { + if (show && initialMode) { + setMode(initialMode); + } + }, [show, initialMode]); + + // Format phone number as user types + const formatPhoneNumber = (value: string) => { + // Remove all non-numeric characters + let phoneNumber = value.replace(/\D/g, ""); + + // Remove leading 1 if present (US country code) + if (phoneNumber.length > 10 && phoneNumber.startsWith("1")) { + phoneNumber = phoneNumber.substring(1); + } + + // Limit to 10 digits + phoneNumber = phoneNumber.substring(0, 10); + + // Format as (XXX) XXX-XXXX + if (phoneNumber.length <= 3) { + return phoneNumber; + } else if (phoneNumber.length <= 6) { + return `(${phoneNumber.slice(0, 3)}) ${phoneNumber.slice(3)}`; + } else { + return `(${phoneNumber.slice(0, 3)}) ${phoneNumber.slice( + 3, + 6 + )}-${phoneNumber.slice(6)}`; + } + }; + + const handlePhoneChange = (e: React.ChangeEvent) => { + const formatted = formatPhoneNumber(e.target.value); + setPhoneNumber(formatted); + }; + + const handlePhoneSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(""); + + try { + // Clean the phone number + let cleanedNumber = phoneNumber.replace(/\D/g, ""); + + // Remove leading 1 if we have 11 digits + if (cleanedNumber.length === 11 && cleanedNumber.startsWith("1")) { + cleanedNumber = cleanedNumber.substring(1); + } + + const fullPhoneNumber = `+1${cleanedNumber}`; + + const response = await fetch( + "http://localhost:5001/api/auth/phone/send-code", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + phoneNumber: fullPhoneNumber, + }), + } + ); + + // Check if response is JSON + const contentType = response.headers.get("content-type"); + if (!contentType || !contentType.includes("application/json")) { + throw new Error( + "Server error: Invalid response format. Make sure the backend is running." + ); + } + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || `Server error: ${response.status}`); + } + + setShowVerification(true); + } catch (err: any) { + console.error("Phone auth error:", err); + setError(err.message || "Failed to send verification code"); + } finally { + setLoading(false); + } + }; + + const handleVerificationSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(""); + + try { + // Clean phone number - remove all formatting + let cleanedDigits = phoneNumber.replace(/\D/g, ""); + + // Remove leading 1 if we have 11 digits + if (cleanedDigits.length === 11 && cleanedDigits.startsWith("1")) { + cleanedDigits = cleanedDigits.substring(1); + } + + const cleanPhone = `+1${cleanedDigits}`; + + console.log("Sending verification request..."); + console.log("Current mode:", mode); + console.log("First Name:", firstName); + console.log("Last Name:", lastName); + + const requestBody = { + phoneNumber: cleanPhone, + code: verificationCode, + firstName: mode === "signup" ? firstName : undefined, + lastName: mode === "signup" ? lastName : undefined, + }; + + console.log("Request body:", requestBody); + + const response = await fetch( + "http://localhost:5001/api/auth/phone/verify-code", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(requestBody), + } + ); + + console.log("Verification response status:", response.status); + const data = await response.json(); + console.log("Verification response data:", data); + + if (!response.ok) { + // Check if it's a new user who needs to provide name + if (data.isNewUser) { + setMode("signup"); + setShowVerification(false); + setAuthMethod("phone"); + return; + } + throw new Error(data.message); + } + + // Store token and user data + console.log("Storing token:", data.token); + localStorage.setItem("token", data.token); + + // Verify token was stored + const storedToken = localStorage.getItem("token"); + console.log("Token stored successfully:", !!storedToken); + console.log("User data:", data.user); + + // Update auth context with the user data + updateUser(data.user); + + // Close modal and reset state + onHide(); + resetModal(); + + // Force a page reload to ensure auth state is properly initialized + // This is needed because AuthContext's useEffect only runs once on mount + setTimeout(() => { + window.location.href = '/'; + }, 100); + } catch (err: any) { + console.error("Verification error:", err); + setError(err.message || "Failed to verify code"); + } finally { + setLoading(false); + } + }; + + const handleEmailSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(""); + + try { + if (mode === "login") { + await login(email, password); + onHide(); + } else { + await register({ + email, + password, + firstName, + lastName, + username: email.split("@")[0], // Generate username from email + }); + onHide(); + } + } catch (err: any) { + setError(err.response?.data?.message || "An error occurred"); + } finally { + setLoading(false); + } + }; + + const handleSocialLogin = (provider: string) => { + // TODO: Implement social login + console.log(`Login with ${provider}`); + }; + + const resetModal = () => { + setAuthMethod(null); + setShowVerification(false); + setError(""); + setPhoneNumber(""); + setEmail(""); + setPassword(""); + setFirstName(""); + setLastName(""); + setVerificationCode(""); + }; + + if (!show) return null; + + return ( + <> +
+
+
+
+ +
+
+

+ Welcome to CommunityRentals.App +

+ + {error && ( +
+ {error} +
+ )} + + {!authMethod && !showVerification && ( +
+ {/* Phone Number Input */} +
+ {mode === "signup" && ( + <> +
+
+ + setFirstName(e.target.value)} + required + /> +
+
+ + setLastName(e.target.value)} + required + /> +
+
+ + )} + +
+ +
+ +1 + +
+
+ + + +

+ We'll text you to verify your number. +

+
+ +
+
+ or +
+
+ + {/* Social Login Options */} + + + + + + + + +
+ + {mode === "login" + ? "Don't have an account? " + : "Already have an account? "} + { + e.preventDefault(); + setMode(mode === "login" ? "signup" : "login"); + }} + > + {mode === "login" ? "Sign up" : "Log in"} + + +
+
+ )} + + {showVerification && ( +
+ + +
+ +

+ We sent a code to +1{phoneNumber} +

+ setVerificationCode(e.target.value)} + maxLength={6} + required + /> +
+ + +
+ )} + + {authMethod === "email" && ( +
+ + + {mode === "signup" && ( + <> +
+
+ + setFirstName(e.target.value)} + required + /> +
+
+ + setLastName(e.target.value)} + required + /> +
+
+ + )} + +
+ + setEmail(e.target.value)} + required + /> +
+ +
+ + setPassword(e.target.value)} + required + /> +
+ + +
+ )} + +

+ By continuing, you agree to CommunityRentals.App's{" "} + + Terms of Service + {" "} + and{" "} + + Privacy Policy + + . +

+
+
+
+
+ + ); +}; + +export default AuthModal; diff --git a/frontend/src/components/Footer.tsx b/frontend/src/components/Footer.tsx index 91fcb3e..f5dda77 100644 --- a/frontend/src/components/Footer.tsx +++ b/frontend/src/components/Footer.tsx @@ -9,7 +9,7 @@ const Footer: React.FC = () => {
- Rentall + CommunityRentals.App

The marketplace for renting anything, from anyone, anywhere. @@ -122,7 +122,7 @@ const Footer: React.FC = () => {

- © 2025 Rentall. All rights reserved. + © 2025 CommunityRentals.App. All rights reserved.

diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index 5dda59f..c4f323f 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -1,22 +1,31 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { useAuth } from '../contexts/AuthContext'; +import AuthModal from './AuthModal'; const Navbar: React.FC = () => { const { user, logout } = useAuth(); const navigate = useNavigate(); + const [showAuthModal, setShowAuthModal] = useState(false); + const [authModalMode, setAuthModalMode] = useState<'login' | 'signup'>('login'); const handleLogout = () => { logout(); navigate('/'); }; + const openAuthModal = (mode: 'login' | 'signup') => { + setAuthModalMode(mode); + setShowAuthModal(true); + }; + return ( + <>
+ + setShowAuthModal(false)} + initialMode={authModalMode} + /> + ); }; diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx index b48f0cc..81eb31c 100644 --- a/frontend/src/contexts/AuthContext.tsx +++ b/frontend/src/contexts/AuthContext.tsx @@ -32,17 +32,21 @@ export const AuthProvider: React.FC = ({ children }) => { useEffect(() => { const token = localStorage.getItem('token'); if (token) { + console.log('AuthContext: Found token, fetching profile...'); userAPI.getProfile() .then(response => { + console.log('AuthContext: Profile loaded', response.data); setUser(response.data); }) - .catch(() => { + .catch((error) => { + console.error('AuthContext: Failed to load profile', error); localStorage.removeItem('token'); }) .finally(() => { setLoading(false); }); } else { + console.log('AuthContext: No token found'); setLoading(false); } }, []); diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index b8220e0..d53792d 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -273,7 +273,7 @@ const Home: React.FC = () => {
-

Why Choose Rentall?

+

Why Choose CommunityRentals.App?