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 logo - - - React logo - -
-

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} +
+ )} + +
+ +
+ + + {errors.email && ( + {errors.email} + )} +
+ +
+ + + {errors.password && ( + {errors.password} + )} +
+ + +
+ +

+ 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 */} +
+ + {/* Name Field */} +
+ + + {errors.name && ( + {errors.name} + )} +
+ + {/* Email Field */} +
+ + + {errors.email && ( + {errors.email} + )} +
+ + {/* Password Field */} +
+ + + {errors.password && ( + {errors.password} + )} +
+ + {/* Confirm Password Field */} +
+ + + {errors.confirmPassword && ( + {errors.confirmPassword} + )} +
+ + {/* Submit Button */} + +
+ + {/* 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