diff --git a/client/eslint.config.js b/client/eslint.config.js
index 4fa125d..8f54ce3 100644
--- a/client/eslint.config.js
+++ b/client/eslint.config.js
@@ -5,25 +5,26 @@ import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
- globalIgnores(['dist']),
- {
- files: ['**/*.{js,jsx}'],
- extends: [
- js.configs.recommended,
- reactHooks.configs.flat.recommended,
- reactRefresh.configs.vite,
- ],
- languageOptions: {
- ecmaVersion: 2020,
- globals: globals.browser,
- parserOptions: {
- ecmaVersion: 'latest',
- ecmaFeatures: { jsx: true },
- sourceType: 'module',
- },
- },
- rules: {
- 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
- },
- },
-])
+globalIgnores(['dist']),
+{
+files: ['**/*.{js,jsx}'],
+extends: [
+js.configs.recommended,
+reactHooks.configs.flat.recommended,
+reactRefresh.configs.vite,
+],
+languageOptions: {
+ecmaVersion: 2020,
+globals: globals.browser,
+parserOptions: {
+ecmaVersion: 'latest',
+ecmaFeatures: { jsx: true },
+sourceType: 'module',
+},
+},
+rules: {
+'no-unused-vars': ['warn', { varsIgnorePattern: '^[A-Z_]|set[A-Z]' }],
+'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
+},
+},
+])
\ No newline at end of file
diff --git a/client/package-lock.json b/client/package-lock.json
index 0abc62a..873e636 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -9,7 +9,8 @@
"version": "0.0.0",
"dependencies": {
"react": "^19.2.0",
- "react-dom": "^19.2.0"
+ "react-dom": "^19.2.0",
+ "react-router-dom": "^7.13.1"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
@@ -1677,6 +1678,19 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/cookie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -2561,6 +2575,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -2578,6 +2593,44 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-router": {
+ "version": "7.13.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz",
+ "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.13.1",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz",
+ "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.13.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -2649,6 +2702,12 @@
"semver": "bin/semver.js"
}
},
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
+ "license": "MIT"
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
diff --git a/client/package.json b/client/package.json
index 6895186..d123ae8 100644
--- a/client/package.json
+++ b/client/package.json
@@ -11,7 +11,8 @@
},
"dependencies": {
"react": "^19.2.0",
- "react-dom": "^19.2.0"
+ "react-dom": "^19.2.0",
+ "react-router-dom": "^7.13.1"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
diff --git a/client/src/App.jsx b/client/src/App.jsx
index f67355a..884e104 100644
--- a/client/src/App.jsx
+++ b/client/src/App.jsx
@@ -1,35 +1,51 @@
-import { useState } from 'react'
-import reactLogo from './assets/react.svg'
-import viteLogo from '/vite.svg'
-import './App.css'
+import Header from './components/layout/Header';
+import Footer from './components/layout/Footer';
+import Home from './pages/Home';
+import Login from './pages/Login';
+import Register from './pages/Register';
+import Dashboard from './pages/Dashboard';
+import { Routes, Route } from 'react-router-dom';
function App() {
- const [count, setCount] = useState(0)
-
return (
- <>
-
- Vite + React
-
-
-
- Edit src/App.jsx and save to test HMR
-
-
-
- Click on the Vite and React logos to learn more
-
- >
- )
+
+ {/* Wrap everything */}
+
+
+
+
+ {/* Your routes */}
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+
+
+ {/* Close wrapper */}
+
+ );
}
-export default App
+// Simple 404 Component
+const NotFound = () => (
+
+
404 - Page Not Found
+
The page you're looking for doesn't exist.
+
+);
+
+const appStyle = {
+ display: 'flex',
+ flexDirection: 'column',
+ minHeight: '100vh',
+};
+
+const mainStyle = {
+ flex: 1,
+};
+
+export default App;
\ No newline at end of file
diff --git a/client/src/components/common/ConnectionTest.jsx b/client/src/components/common/ConnectionTest.jsx
new file mode 100644
index 0000000..0ed4fbc
--- /dev/null
+++ b/client/src/components/common/ConnectionTest.jsx
@@ -0,0 +1,109 @@
+import { useState } from 'react';
+
+const ConnectionTest = () => {
+ const [message, setMessage] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+
+ const testConnection = async () => {
+ setLoading(true);
+ setError('');
+ setMessage('');
+
+ try {
+ // Using relative URL because of Vite proxy
+ const response = await fetch('/api/health');
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const data = await response.json();
+ setMessage(data.message);
+
+ } catch (err) {
+ setError('Failed to connect to server: ' + err.message);
+ console.error('Connection test error:', err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
Backend Connection Test
+
+
+
+ {message && (
+
+ ✅ Success: {message}
+
+ )}
+
+ {error && (
+
+ ❌ Error: {error}
+
+ )}
+
+
+
Backend URL: http://localhost:5000
+
Frontend URL: http://localhost:5173
+
Using: Vite Proxy
+
+
+ );
+};
+
+const containerStyle = {
+ padding: '2rem',
+ backgroundColor: '#f8f9fa',
+ borderRadius: '8px',
+ margin: '2rem 0',
+};
+
+const buttonStyle = {
+ backgroundColor: '#007bff',
+ color: 'white',
+ padding: '0.75rem 1.5rem',
+ border: 'none',
+ borderRadius: '5px',
+ fontSize: '1rem',
+ cursor: 'pointer',
+ marginTop: '1rem',
+};
+
+const successStyle = {
+ marginTop: '1rem',
+ padding: '1rem',
+ backgroundColor: '#d4edda',
+ color: '#155724',
+ borderRadius: '5px',
+ border: '1px solid #c3e6cb',
+};
+
+const errorStyle = {
+ marginTop: '1rem',
+ padding: '1rem',
+ backgroundColor: '#f8d7da',
+ color: '#721c24',
+ borderRadius: '5px',
+ border: '1px solid #f5c6cb',
+};
+
+const infoStyle = {
+ marginTop: '1.5rem',
+ padding: '1rem',
+ backgroundColor: 'white',
+ borderRadius: '5px',
+ fontSize: '0.9rem',
+};
+
+export default ConnectionTest;
\ No newline at end of file
diff --git a/client/src/components/layout/Footer.jsx b/client/src/components/layout/Footer.jsx
new file mode 100644
index 0000000..51ed6d8
--- /dev/null
+++ b/client/src/components/layout/Footer.jsx
@@ -0,0 +1,26 @@
+const Footer = () => {
+ return (
+
+ );
+};
+
+const footerStyle = {
+ backgroundColor: '#333',
+ color: 'white',
+ padding: '2rem 0',
+ marginTop: 'auto',
+ textAlign: 'center',
+};
+
+const containerStyle = {
+ maxWidth: '1200px',
+ margin: '0 auto',
+ padding: '0 2rem',
+};
+
+export default Footer;
\ No newline at end of file
diff --git a/client/src/components/layout/Header.jsx b/client/src/components/layout/Header.jsx
new file mode 100644
index 0000000..acb3fce
--- /dev/null
+++ b/client/src/components/layout/Header.jsx
@@ -0,0 +1,98 @@
+import { Link } from 'react-router-dom';
+import { useAuth } from '../../context/AuthContext';
+
+const Header = () => {
+ // Grab the auth state from our global context
+ const { user, logout, isAuthenticated } = useAuth();
+
+ return (
+
+
+
+
+ Creators Platform
+
+
+
+
+
+
+ );
+};
+
+// --- STYLES ---
+
+const headerStyle = {
+ backgroundColor: '#212529',
+ padding: '1rem 0',
+ color: 'white',
+ boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
+};
+
+const containerStyle = {
+ maxWidth: '1200px',
+ margin: '0 auto',
+ padding: '0 2rem',
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center'
+};
+
+const logoStyle = {
+ margin: 0,
+ fontSize: '1.5rem'
+};
+
+const linkStyle = {
+ color: 'white',
+ textDecoration: 'none',
+ fontWeight: 'bold'
+};
+
+const navStyle = {
+ display: 'flex',
+ alignItems: 'center',
+ gap: '1.5rem',
+};
+
+const navLinkStyle = {
+ color: '#ced4da',
+ textDecoration: 'none',
+ fontSize: '0.9rem'
+};
+
+const userNameStyle = {
+ color: '#ffc107', // Highlight the name in gold/yellow
+ fontSize: '0.9rem',
+ fontWeight: '500'
+};
+
+const logoutBtnStyle = {
+ padding: '0.4rem 0.8rem',
+ backgroundColor: '#dc3545',
+ color: 'white',
+ border: 'none',
+ borderRadius: '4px',
+ cursor: 'pointer',
+ fontSize: '0.85rem'
+};
+
+export default Header;
\ No newline at end of file
diff --git a/client/src/context/AuthContext.jsx b/client/src/context/AuthContext.jsx
new file mode 100644
index 0000000..51d9c4c
--- /dev/null
+++ b/client/src/context/AuthContext.jsx
@@ -0,0 +1,66 @@
+import { createContext, useState, useEffect, useContext } from 'react';
+import { useNavigate } from 'react-router-dom';
+
+const AuthContext = createContext(null);
+
+export const AuthProvider = ({ children }) => {
+ const [user, setUser] = useState(null);
+ const [token, setToken] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ const storedToken = localStorage.getItem('token');
+ const storedUser = localStorage.getItem('user');
+
+ if (storedToken && storedUser) {
+ try {
+ setToken(storedToken);
+ setUser(JSON.parse(storedUser));
+ } catch (error) {
+ console.error('Auth error:', error);
+ localStorage.removeItem('token');
+ localStorage.removeItem('user');
+ }
+ }
+ setLoading(false);
+ }, []);
+
+ const login = (userData, userToken) => {
+ setUser(userData);
+ setToken(userToken);
+ localStorage.setItem('token', userToken);
+ localStorage.setItem('user', JSON.stringify(userData));
+ };
+
+ const logout = () => {
+ setUser(null);
+ setToken(null);
+ localStorage.removeItem('token');
+ localStorage.removeItem('user');
+ navigate('/login');
+ };
+
+ const value = {
+ user,
+ token,
+ loading,
+ login,
+ logout,
+ isAuthenticated: () => !!token && !!user
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useAuth = () => {
+ const context = useContext(AuthContext);
+ if (!context) {
+ throw new Error('useAuth must be used within AuthProvider');
+ }
+ return context;
+};
\ No newline at end of file
diff --git a/client/src/index.css b/client/src/index.css
index 08a3ac9..a7f816f 100644
--- a/client/src/index.css
+++ b/client/src/index.css
@@ -1,68 +1,31 @@
-:root {
- font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
- line-height: 1.5;
- font-weight: 400;
-
- color-scheme: light dark;
- color: rgba(255, 255, 255, 0.87);
- background-color: #242424;
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
- font-synthesis: none;
- text-rendering: optimizeLegibility;
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+ sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
+ background-color: #f8f9fa;
}
-a {
- font-weight: 500;
- color: #646cff;
- text-decoration: inherit;
-}
-a:hover {
- color: #535bf2;
-}
-
-body {
- margin: 0;
- display: flex;
- place-items: center;
- min-width: 320px;
- min-height: 100vh;
+h1, h2, h3, h4, h5, h6 {
+ margin-bottom: 0.5rem;
}
-h1 {
- font-size: 3.2em;
- line-height: 1.1;
+p {
+ margin-bottom: 1rem;
}
-button {
- border-radius: 8px;
- border: 1px solid transparent;
- padding: 0.6em 1.2em;
- font-size: 1em;
- font-weight: 500;
- font-family: inherit;
- background-color: #1a1a1a;
- cursor: pointer;
- transition: border-color 0.25s;
-}
-button:hover {
- border-color: #646cff;
-}
-button:focus,
-button:focus-visible {
- outline: 4px auto -webkit-focus-ring-color;
+a {
+ color: #007bff;
+ text-decoration: none;
}
-@media (prefers-color-scheme: light) {
- :root {
- color: #213547;
- background-color: #ffffff;
- }
- a:hover {
- color: #747bff;
- }
- button {
- background-color: #f9f9f9;
- }
-}
+a:hover {
+ text-decoration: underline;
+}
\ No newline at end of file
diff --git a/client/src/main.jsx b/client/src/main.jsx
index b9a1a6d..4a0a381 100644
--- a/client/src/main.jsx
+++ b/client/src/main.jsx
@@ -1,10 +1,13 @@
-import { StrictMode } from 'react'
-import { createRoot } from 'react-dom/client'
-import './index.css'
-import App from './App.jsx'
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import { BrowserRouter } from 'react-router-dom';
+import App from './App';
+import './index.css';
-createRoot(document.getElementById('root')).render(
-
-
- ,
-)
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+
+
+
+);
\ No newline at end of file
diff --git a/client/src/pages/Dashboard.jsx b/client/src/pages/Dashboard.jsx
new file mode 100644
index 0000000..f1904d2
--- /dev/null
+++ b/client/src/pages/Dashboard.jsx
@@ -0,0 +1,101 @@
+import { useNavigate } from "react-router-dom";
+
+// Function to check if JWT token is expired
+const isTokenExpired = (token) => {
+ if (!token) return true;
+
+ try {
+ const payload = JSON.parse(atob(token.split(".")[1]));
+ const currentTime = Date.now() / 1000;
+ return payload.exp < currentTime;
+ } catch {
+ return true;
+ }
+};
+
+const Dashboard = () => {
+ const navigate = useNavigate();
+
+ const token = localStorage.getItem("token");
+ const user = JSON.parse(localStorage.getItem("user") || "{}");
+
+ // Check authentication
+ if (!token || isTokenExpired(token)) {
+ localStorage.removeItem("token");
+ localStorage.removeItem("user");
+ navigate("/login");
+ return null;
+ }
+
+ const handleLogout = () => {
+ localStorage.removeItem("token");
+ localStorage.removeItem("user");
+ navigate("/login");
+ };
+
+ return (
+
+
+
Dashboard
+
Welcome back, {user.name}!
+
+
+
+
+
+
Your Dashboard
+
+
Name: {user.name}
+
Email: {user.email}
+
+
This page will display:
+
+
+ - Your created content (posts/recipes/workouts)
+ - Statistics and analytics
+ - Quick actions (create new, edit, delete)
+ - Recent activity
+
+
+
+ This will be built after authentication is implemented
+
+
+
+
+ );
+};
+
+const containerStyle = {
+ minHeight: "80vh",
+ padding: "2rem",
+};
+
+const headerStyle = {
+ maxWidth: "1200px",
+ margin: "0 auto 2rem",
+};
+
+const contentStyle = {
+ maxWidth: "1200px",
+ margin: "0 auto",
+};
+
+const placeholderStyle = {
+ backgroundColor: "#f8f9fa",
+ padding: "2rem",
+ borderRadius: "8px",
+};
+
+const listStyle = {
+ paddingLeft: "1.5rem",
+ marginTop: "1rem",
+};
+
+const noteStyle = {
+ marginTop: "1rem",
+ fontStyle: "italic",
+ color: "#666",
+};
+
+export default Dashboard;
\ No newline at end of file
diff --git a/client/src/pages/Home.jsx b/client/src/pages/Home.jsx
new file mode 100644
index 0000000..903708f
--- /dev/null
+++ b/client/src/pages/Home.jsx
@@ -0,0 +1,117 @@
+import { Link } from 'react-router-dom';
+import ConnectionTest from '../components/common/ConnectionTest';
+
+const Home = () => {
+ return (
+
+
+
{/* Platform Name/Tagline */}
+
+ {/* Brief description of what your platform does */}
+
+
+
+ Get Started
+
+
+ Login
+
+
+
+ {/* Backend Connection Test */}
+
+
+
+
+ {/* Features Section (Optional) */}
+
+
Why Choose {/* Platform Name */}?
+
+ {/* Add 3 feature cards based on your theme */}
+
+
Feature 1
+
Description
+
+
+
Feature 2
+
Description
+
+
+
Feature 3
+
Description
+
+
+
+
+ );
+};
+
+const containerStyle = {
+ minHeight: '100vh',
+};
+
+const heroStyle = {
+ textAlign: 'center',
+ padding: '4rem 2rem',
+ backgroundColor: '#f5f5f5',
+};
+
+const titleStyle = {
+ fontSize: '3rem',
+ marginBottom: '1rem',
+ color: '#333',
+};
+
+const subtitleStyle = {
+ fontSize: '1.25rem',
+ color: '#666',
+ marginBottom: '2rem',
+};
+
+const ctaStyle = {
+ display: 'flex',
+ gap: '1rem',
+ justifyContent: 'center',
+};
+
+const buttonStyle = {
+ backgroundColor: '#007bff',
+ color: 'white',
+ padding: '0.75rem 2rem',
+ borderRadius: '5px',
+ textDecoration: 'none',
+ fontWeight: 'bold',
+};
+
+const secondaryButtonStyle = {
+ backgroundColor: 'white',
+ color: '#007bff',
+ padding: '0.75rem 2rem',
+ borderRadius: '5px',
+ textDecoration: 'none',
+ fontWeight: 'bold',
+ border: '2px solid #007bff',
+};
+
+const featuresStyle = {
+ padding: '4rem 2rem',
+ textAlign: 'center',
+};
+
+const featureGridStyle = {
+ display: 'grid',
+ gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
+ gap: '2rem',
+ marginTop: '2rem',
+ maxWidth: '1200px',
+ margin: '2rem auto',
+};
+
+const featureCardStyle = {
+ padding: '2rem',
+ backgroundColor: 'white',
+ borderRadius: '8px',
+ boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
+};
+
+export default Home;
\ No newline at end of file
diff --git a/client/src/pages/Login.jsx b/client/src/pages/Login.jsx
new file mode 100644
index 0000000..4abfc70
--- /dev/null
+++ b/client/src/pages/Login.jsx
@@ -0,0 +1,289 @@
+import { useState } from 'react';
+import { Link, useNavigate } from 'react-router-dom';
+
+const Login = () => {
+ // Form field state
+ const [formData, setFormData] = useState({
+ email: '',
+ password: ''
+ });
+
+ // UI states
+ const [errors, setErrors] = useState({});
+ const [isLoading, setIsLoading] = useState(false);
+ const [apiError, setApiError] = useState('');
+
+ const navigate = useNavigate();
+
+ // Step 8: Input handler
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+
+ setFormData(prev => ({
+ ...prev,
+ [name]: value
+ }));
+
+ // Clear field error
+ if (errors[name]) {
+ setErrors(prev => ({
+ ...prev,
+ [name]: ''
+ }));
+ }
+
+ // Clear API error
+ if (apiError) {
+ setApiError('');
+ }
+ };
+
+ // Step 9: Validation
+ const validateForm = () => {
+ const newErrors = {};
+
+ if (!formData.email.trim()) {
+ newErrors.email = 'Email is required';
+ } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
+ newErrors.email = 'Please enter a valid email';
+ }
+
+ if (!formData.password) {
+ newErrors.password = 'Password is required';
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ // Step 10: Submit handler
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ setApiError('');
+
+ if (!validateForm()) return;
+
+ setIsLoading(true);
+
+ try {
+ const response = await fetch('/api/auth/login', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ email: formData.email.trim().toLowerCase(),
+ password: formData.password
+ })
+ });
+
+ const data = await response.json();
+
+ if (response.ok) {
+ // Store JWT
+ localStorage.setItem('token', data.token);
+
+ // Store user
+ localStorage.setItem('user', JSON.stringify(data.user));
+
+ // Clear form
+ setFormData({ email: '', password: '' });
+
+ // Redirect
+ navigate('/dashboard');
+
+ } else {
+ setApiError(data.message || 'Login failed. Please try again.');
+ }
+
+ } catch (error) {
+ console.error('Login error:', error);
+ setApiError('Unable to connect to server. Please try again.');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ // Step 11: UI
+ return (
+
+
+
Welcome Back
+
+ Login to your Creators Platform account
+
+
+ {apiError && (
+
+ {apiError}
+
+ )}
+
+
+
+
+ Don't have an account?{' '}
+
+ Sign up here
+
+
+
+
+ );
+};
+
+// Step 12: Styles
+const containerStyle = {
+ minHeight: '80vh',
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: '2rem',
+ backgroundColor: '#f8f9fa'
+};
+
+const formContainerStyle = {
+ maxWidth: '400px',
+ width: '100%',
+ padding: '2.5rem',
+ backgroundColor: 'white',
+ borderRadius: '10px',
+ boxShadow: '0 4px 6px rgba(0,0,0,0.1)'
+};
+
+const titleStyle = {
+ textAlign: 'center',
+ marginBottom: '0.5rem',
+ color: '#333',
+ fontSize: '2rem'
+};
+
+const subtitleStyle = {
+ textAlign: 'center',
+ color: '#666',
+ marginBottom: '2rem',
+ fontSize: '0.95rem'
+};
+
+const formStyle = {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '1.5rem'
+};
+
+const fieldStyle = {
+ display: 'flex',
+ flexDirection: 'column'
+};
+
+const labelStyle = {
+ marginBottom: '0.5rem',
+ fontWeight: '500',
+ color: '#333',
+ fontSize: '0.9rem'
+};
+
+const inputStyle = {
+ padding: '0.75rem',
+ fontSize: '1rem',
+ border: '1px solid #ddd',
+ borderRadius: '5px'
+};
+
+const inputErrorStyle = {
+ ...inputStyle,
+ borderColor: '#dc3545'
+};
+
+const errorTextStyle = {
+ color: '#dc3545',
+ fontSize: '0.85rem',
+ marginTop: '0.25rem'
+};
+
+const buttonStyle = {
+ padding: '0.875rem',
+ fontSize: '1rem',
+ fontWeight: 'bold',
+ color: 'white',
+ backgroundColor: '#007bff',
+ border: 'none',
+ borderRadius: '5px',
+ cursor: 'pointer',
+ marginTop: '0.5rem'
+};
+
+const buttonDisabledStyle = {
+ ...buttonStyle,
+ backgroundColor: '#6c757d',
+ cursor: 'not-allowed'
+};
+
+const errorMessageStyle = {
+ padding: '1rem',
+ backgroundColor: '#f8d7da',
+ color: '#721c24',
+ borderRadius: '5px',
+ marginBottom: '1rem',
+ border: '1px solid #f5c6cb'
+};
+
+const linkTextStyle = {
+ textAlign: 'center',
+ marginTop: '1.5rem',
+ color: '#666'
+};
+
+const linkStyle = {
+ color: '#007bff',
+ textDecoration: 'none',
+ fontWeight: '500'
+};
+
+export default Login;
\ No newline at end of file
diff --git a/client/src/pages/Register.jsx b/client/src/pages/Register.jsx
new file mode 100644
index 0000000..dc70a82
--- /dev/null
+++ b/client/src/pages/Register.jsx
@@ -0,0 +1,395 @@
+import { useState } from 'react';
+import { Link, useNavigate } from 'react-router-dom';
+
+const Register = () => {
+ // Form field states
+ const [formData, setFormData] = useState({
+ name: '',
+ email: '',
+ password: '',
+ confirmPassword: ''
+ });
+
+ // UI states
+ const [errors, setErrors] = useState({});
+ const [isLoading, setIsLoading] = useState(false);
+ const [successMessage, setSuccessMessage] = useState('');
+ const [apiError, setApiError] = useState('');
+
+ // For navigation after successful registration
+ const navigate = useNavigate();
+
+ // Handle input change
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+
+ setFormData(prev => ({
+ ...prev,
+ [name]: value
+ }));
+
+ // Clear error for this field when user starts typing
+ if (errors[name]) {
+ setErrors(prev => ({
+ ...prev,
+ [name]: ''
+ }));
+ }
+ };
+
+ // Validate form fields
+ const validateForm = () => {
+ const newErrors = {};
+
+ // Name validation
+ if (!formData.name.trim()) {
+ newErrors.name = 'Name is required';
+ } else if (formData.name.trim().length < 2) {
+ newErrors.name = 'Name must be at least 2 characters';
+ } else if (formData.name.trim().length > 50) {
+ newErrors.name = 'Name cannot exceed 50 characters';
+ }
+
+ // Email validation
+ if (!formData.email.trim()) {
+ newErrors.email = 'Email is required';
+ } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
+ newErrors.email = 'Please enter a valid email address';
+ }
+
+ // Password validation
+ if (!formData.password) {
+ newErrors.password = 'Password is required';
+ } else if (formData.password.length < 6) {
+ newErrors.password = 'Password must be at least 6 characters';
+ }
+
+ // Confirm password validation
+ if (!formData.confirmPassword) {
+ newErrors.confirmPassword = 'Please confirm your password';
+ } else if (formData.password !== formData.confirmPassword) {
+ newErrors.confirmPassword = 'Passwords do not match';
+ }
+
+ setErrors(newErrors);
+
+ // Return true if no errors
+ return Object.keys(newErrors).length === 0;
+ };
+
+ // Handle form submission
+ const handleSubmit = async (e) => {
+ e.preventDefault(); // Prevent page reload
+
+ // Clear previous messages
+ setSuccessMessage('');
+ setApiError('');
+
+ // Validate form
+ if (!validateForm()) {
+ return; // Stop if validation fails
+ }
+
+ // Start loading
+ setIsLoading(true);
+
+ try {
+ // Prepare data to send (exclude confirmPassword)
+ const registrationData = {
+ name: formData.name.trim(),
+ email: formData.email.trim().toLowerCase(),
+ password: formData.password
+ };
+
+ // Send POST request to backend
+ const response = await fetch('/api/users/register', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(registrationData)
+ });
+
+ const data = await response.json();
+
+ if (response.ok) {
+ // Registration successful
+ setSuccessMessage('Account created successfully! Redirecting to login...');
+
+ // Clear form
+ setFormData({
+ name: '',
+ email: '',
+ password: '',
+ confirmPassword: ''
+ });
+
+ // Redirect to login page after 2 seconds
+ setTimeout(() => {
+ navigate('/login');
+ }, 2000);
+
+ } else {
+ // Registration failed - show error from backend
+ setApiError(data.message || 'Registration failed. Please try again.');
+ }
+
+ } catch (error) {
+ // Network or other error
+ console.error('Registration error:', error);
+ setApiError('Unable to connect to server. Please check your connection and try again.');
+ } finally {
+ // Stop loading regardless of success/failure
+ setIsLoading(false);
+ }
+ };
+
+ // JSX for registration form
+ return (
+
+
+
Create Your Account
+
+ Join Your Platform Name and start creating today
+
+
+ {/* Success Message */}
+ {successMessage && (
+
+ {successMessage}
+
+ )}
+
+ {/* API Error Message */}
+ {apiError && (
+
+ {apiError}
+
+ )}
+
+ {/* Registration Form */}
+
+
+ {/* Login Link */}
+
+ Already have an account?{' '}
+
+ Login here
+
+
+
+
+ );
+};
+
+export default Register;
+
+// ===================== STYLES =====================
+
+const containerStyle = {
+ minHeight: '80vh',
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: '2rem',
+ backgroundColor: '#f8f9fa',
+};
+
+const formContainerStyle = {
+ maxWidth: '450px',
+ width: '100%',
+ padding: '2.5rem',
+ backgroundColor: 'white',
+ borderRadius: '10px',
+ boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
+};
+
+const titleStyle = {
+ textAlign: 'center',
+ marginBottom: '0.5rem',
+ color: '#333',
+ fontSize: '2rem',
+};
+
+const subtitleStyle = {
+ textAlign: 'center',
+ color: '#666',
+ marginBottom: '2rem',
+ fontSize: '0.95rem',
+};
+
+const formStyle = {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '1.5rem',
+};
+
+const fieldStyle = {
+ display: 'flex',
+ flexDirection: 'column',
+};
+
+const labelStyle = {
+ marginBottom: '0.5rem',
+ fontWeight: '500',
+ color: '#333',
+ fontSize: '0.9rem',
+};
+
+const inputStyle = {
+ padding: '0.75rem',
+ fontSize: '1rem',
+ border: '1px solid #ddd',
+ borderRadius: '5px',
+ transition: 'border-color 0.3s',
+};
+
+const inputErrorStyle = {
+ ...inputStyle,
+ borderColor: '#dc3545',
+};
+
+const errorTextStyle = {
+ color: '#dc3545',
+ fontSize: '0.85rem',
+ marginTop: '0.25rem',
+};
+
+const buttonStyle = {
+ padding: '0.875rem',
+ fontSize: '1rem',
+ fontWeight: 'bold',
+ color: 'white',
+ backgroundColor: '#007bff',
+ border: 'none',
+ borderRadius: '5px',
+ cursor: 'pointer',
+ transition: 'background-color 0.3s',
+ marginTop: '0.5rem',
+};
+
+const buttonDisabledStyle = {
+ ...buttonStyle,
+ backgroundColor: '#6c757d',
+ cursor: 'not-allowed',
+};
+
+const successStyle = {
+ padding: '1rem',
+ backgroundColor: '#d4edda',
+ color: '#155724',
+ borderRadius: '5px',
+ marginBottom: '1rem',
+ border: '1px solid #c3e6cb',
+};
+
+const errorMessageStyle = {
+ padding: '1rem',
+ backgroundColor: '#f8d7da',
+ color: '#721c24',
+ borderRadius: '5px',
+ marginBottom: '1rem',
+ border: '1px solid #f5c6cb',
+};
+
+const linkTextStyle = {
+ textAlign: 'center',
+ marginTop: '1.5rem',
+ color: '#666',
+};
+
+const linkStyle = {
+ color: '#007bff',
+ textDecoration: 'none',
+ fontWeight: '500',
+};
\ No newline at end of file
diff --git a/client/vite.config.js b/client/vite.config.js
index 8b0f57b..7cd1ac0 100644
--- a/client/vite.config.js
+++ b/client/vite.config.js
@@ -1,7 +1,15 @@
-import { defineConfig } from 'vite'
-import react from '@vitejs/plugin-react'
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
-// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
-})
+ server: {
+ proxy: {
+ '/api': {
+ target: 'http://localhost:5000',
+ changeOrigin: true,
+ secure: false,
+ },
+ },
+ },
+});
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 9bc2fe8..997a1c2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7,6 +7,11 @@
"": {
"name": "creators-platform",
"version": "1.0.0",
+ "dependencies": {
+ "cors": "^2.8.6",
+ "prop-types": "^15.8.1",
+ "react-router-dom": "^7.13.1"
+ },
"devDependencies": {
"concurrently": "^8.2.2"
}
@@ -140,6 +145,36 @@
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
}
},
+ "node_modules/cookie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/cors": {
+ "version": "2.8.6",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
+ "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+ "license": "MIT",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
@@ -204,6 +239,12 @@
"node": ">=8"
}
},
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
"node_modules/lodash": {
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
@@ -211,6 +252,105 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.4",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
+ "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.4",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
+ "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.4"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
+ "node_modules/react-router": {
+ "version": "7.13.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz",
+ "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.13.1",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz",
+ "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.13.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -231,6 +371,18 @@
"tslib": "^2.1.0"
}
},
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
+ "license": "MIT"
+ },
"node_modules/shell-quote": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
@@ -311,6 +463,15 @@
"dev": true,
"license": "0BSD"
},
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
diff --git a/package.json b/package.json
index 95496b4..63be358 100644
--- a/package.json
+++ b/package.json
@@ -9,5 +9,10 @@
},
"devDependencies": {
"concurrently": "^8.2.2"
+ },
+ "dependencies": {
+ "cors": "^2.8.6",
+ "prop-types": "^15.8.1",
+ "react-router-dom": "^7.13.1"
}
-}
\ No newline at end of file
+}
diff --git a/server/controllers/authController.js b/server/controllers/authController.js
new file mode 100644
index 0000000..ef9b752
--- /dev/null
+++ b/server/controllers/authController.js
@@ -0,0 +1,71 @@
+import User from '../models/user.js';
+import bcrypt from 'bcrypt';
+import jwt from 'jsonwebtoken';
+
+// @desc Login user and return JWT
+// @route POST /api/auth/login
+// @access Public
+export const loginUser = async (req, res) => {
+ try {
+ const { email, password } = req.body;
+
+ // 1. Validate input
+ if (!email || !password) {
+ return res.status(400).json({
+ success: false,
+ message: 'Please provide email and password'
+ });
+ }
+
+ // 2. Find user by email (include password field)
+ const user = await User.findOne({ email }).select('+password');
+
+ if (!user) {
+ return res.status(401).json({
+ success: false,
+ message: 'Invalid email or password'
+ });
+ }
+
+ // 3. Compare password with hashed password
+ const isPasswordMatch = await bcrypt.compare(password, user.password);
+
+ if (!isPasswordMatch) {
+ return res.status(401).json({
+ success: false,
+ message: 'Invalid email or password'
+ });
+ }
+
+ // 4. Generate JWT token
+ const token = jwt.sign(
+ { userId: user._id },
+ process.env.JWT_SECRET,
+ { expiresIn: process.env.JWT_EXPIRE || '7d' }
+ );
+
+ // 5. Remove password from user object
+ user.password = undefined;
+
+ // 6. Send response with token and user data
+ res.status(200).json({
+ success: true,
+ message: 'Login successful',
+ token,
+ user: {
+ id: user._id,
+ name: user.name,
+ email: user.email,
+ createdAt: user.createdAt
+ }
+ });
+
+ } catch (error) {
+ console.error('Login error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Server error during login',
+ error: error.message
+ });
+ }
+};
\ No newline at end of file
diff --git a/server/controllers/userController.js b/server/controllers/userController.js
index 4544257..5575887 100644
--- a/server/controllers/userController.js
+++ b/server/controllers/userController.js
@@ -1,13 +1,20 @@
import User from '../models/user.js';
import bcrypt from 'bcrypt';
+/* ===========================
+ Register User
+=========================== */
/* ===========================
Register User
=========================== */
const registerUser = async (req, res) => {
try {
+ // 🔍 DEBUG: See what frontend is sending
+ console.log("Incoming body:", req.body);
+
const { name, email, password } = req.body || {};
+ // ✅ Validate required fields
if (!name || !email || !password) {
return res.status(400).json({
success: false,
@@ -15,6 +22,7 @@ const registerUser = async (req, res) => {
});
}
+ // ✅ Check if user already exists
const existingUser = await User.findOne({ email });
if (existingUser) {
@@ -24,14 +32,17 @@ const registerUser = async (req, res) => {
});
}
+ // ✅ Hash password
const hashedPassword = await bcrypt.hash(password, 10);
+ // ✅ Create user
const user = await User.create({
name,
email,
password: hashedPassword
});
+ // Remove password from response
user.password = undefined;
res.status(201).json({
@@ -41,6 +52,8 @@ const registerUser = async (req, res) => {
});
} catch (error) {
+ console.error("Registration Error:", error);
+
res.status(500).json({
success: false,
message: 'Server error during registration',
diff --git a/server/package-lock.json b/server/package-lock.json
index 144a0a2..e3a7780 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -13,6 +13,7 @@
"cors": "^2.8.6",
"dotenv": "^17.3.1",
"express": "^5.2.1",
+ "jsonwebtoken": "^9.0.3",
"mongoose": "^9.2.2"
},
"devDependencies": {
@@ -166,6 +167,12 @@
"node": ">=20.19.0"
}
},
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -338,6 +345,15 @@
"node": ">= 0.4"
}
},
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -723,6 +739,49 @@
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
"license": "MIT"
},
+ "node_modules/jsonwebtoken": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
+ "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
+ "license": "MIT",
+ "dependencies": {
+ "jws": "^4.0.1",
+ "lodash.includes": "^4.3.0",
+ "lodash.isboolean": "^3.0.3",
+ "lodash.isinteger": "^4.0.4",
+ "lodash.isnumber": "^3.0.3",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.isstring": "^4.0.1",
+ "lodash.once": "^4.0.0",
+ "ms": "^2.1.1",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ }
+ },
+ "node_modules/jwa": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
+ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-equal-constant-time": "^1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
+ "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
+ "license": "MIT",
+ "dependencies": {
+ "jwa": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
"node_modules/kareem": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/kareem/-/kareem-3.2.0.tgz",
@@ -732,6 +791,48 @@
"node": ">=18.0.0"
}
},
+ "node_modules/lodash.includes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isboolean": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isinteger": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isnumber": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isstring": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
+ "license": "MIT"
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -1152,6 +1253,26 @@
"node": ">= 18"
}
},
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -1162,7 +1283,6 @@
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
- "dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
diff --git a/server/package.json b/server/package.json
index 621a417..d9c2849 100644
--- a/server/package.json
+++ b/server/package.json
@@ -16,6 +16,7 @@
"cors": "^2.8.6",
"dotenv": "^17.3.1",
"express": "^5.2.1",
+ "jsonwebtoken": "^9.0.3",
"mongoose": "^9.2.2"
},
"devDependencies": {
diff --git a/server/routes/authRoutes.js b/server/routes/authRoutes.js
new file mode 100644
index 0000000..9b34831
--- /dev/null
+++ b/server/routes/authRoutes.js
@@ -0,0 +1,9 @@
+import express from 'express';
+import { loginUser } from '../controllers/authController.js';
+
+const router = express.Router();
+
+// Login route
+router.post('/login', loginUser);
+
+export default router;
\ No newline at end of file
diff --git a/server/server.js b/server/server.js
index 73a42c3..3c88ccd 100644
--- a/server/server.js
+++ b/server/server.js
@@ -3,33 +3,33 @@ import dotenv from 'dotenv';
import cors from 'cors';
import connectDB from './config/database.js';
import userRoutes from './routes/userRoutes.js';
+import authRoutes from './routes/authRoutes.js'; // Add this import
-// Load environment variables
dotenv.config();
-
-// Connect to database
connectDB();
const app = express();
const PORT = process.env.PORT || 5000;
// Middleware
-app.use(cors());
+app.use(cors({
+ origin: process.env.CLIENT_URL || 'http://localhost:5173',
+ credentials: true
+}));
app.use(express.json());
// Routes
app.use('/api/users', userRoutes);
+app.use('/api/auth', authRoutes); // Add this line
-// Health check endpoint (keep this for testing)
+// Health check
app.get('/api/health', (req, res) => {
res.json({
message: 'Server is running!',
- timestamp: new Date(),
- database: 'Connected'
+ timestamp: new Date()
});
});
-// Start server
app.listen(PORT, () => {
console.log(`🚀 Server running on http://localhost:${PORT}`);
});
\ No newline at end of file