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 */}
+
+
+
+
+ or
+
+
+
+ {/* Social Login Options */}
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {showVerification && (
+
+ )}
+
+ {authMethod === "email" && (
+
+ )}
+
+
+ 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?