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..333f2f3 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,8 +8,10 @@ "name": "client", "version": "0.0.0", "dependencies": { + "axios": "^1.13.6", "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", @@ -1529,6 +1531,23 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1595,6 +1614,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1663,6 +1695,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1677,6 +1721,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", @@ -1724,6 +1781,29 @@ "dev": true, "license": "MIT" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.302", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", @@ -1731,6 +1811,51 @@ "dev": true, "license": "ISC" }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.27.3", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", @@ -2071,6 +2196,42 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2086,6 +2247,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2096,6 +2266,43 @@ "node": ">=6.9.0" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2122,6 +2329,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2132,6 +2351,45 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -2340,6 +2598,36 @@ "yallist": "^3.0.2" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.4.tgz", @@ -2536,6 +2824,12 @@ "node": ">= 0.8.0" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2561,6 +2855,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 +2873,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 +2982,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..a3219c2 100644 --- a/client/package.json +++ b/client/package.json @@ -10,8 +10,10 @@ "preview": "vite preview" }, "dependencies": { + "axios": "^1.13.6", "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..ec70322 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -1,35 +1,83 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' -import './App.css' +import { Routes, Route } from "react-router-dom"; -function App() { - const [count, setCount] = useState(0) +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 ProtectedRoute from "./components/common/ProtectedRoute"; +import PublicRoute from "./components/common/PublicRoute"; +import { AuthProvider } from "./context/AuthContext"; + +function App() { return ( - <> -
- - Vite logo - - - React logo - -
-

Vite + React

-
- -

- Edit src/App.jsx and save to test HMR -

+ +
+
+ +
+ + + {/* Public */} + } /> + + + + + } + /> + + + + + } + /> + + {/* Protected */} + + + + } + /> + + } /> + + +
+ +
-

- Click on the Vite and React logos to learn more -

- - ) +
+ ); } -export default App +const NotFound = () => ( +
+

404 - Page Not Found

+
+); + +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/common/LoadingSpinner.jsx b/client/src/components/common/LoadingSpinner.jsx new file mode 100644 index 0000000..e190072 --- /dev/null +++ b/client/src/components/common/LoadingSpinner.jsx @@ -0,0 +1,33 @@ +const LoadingSpinner = ({ message = 'Loading...' }) => { + return ( +
+
+

{message}

+
+ ); +}; + +const containerStyle = { + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + minHeight: '80vh', +}; + +const spinnerStyle = { + width: '50px', + height: '50px', + border: '4px solid #f3f3f3', + borderTop: '4px solid #007bff', + borderRadius: '50%', + animation: 'spin 1s linear infinite', +}; + +const textStyle = { + marginTop: '1rem', + color: '#666', + fontSize: '1.1rem', +}; + +export default LoadingSpinner; \ No newline at end of file diff --git a/client/src/components/common/ProtectedRoute.jsx b/client/src/components/common/ProtectedRoute.jsx new file mode 100644 index 0000000..4b8ba2b --- /dev/null +++ b/client/src/components/common/ProtectedRoute.jsx @@ -0,0 +1,23 @@ +import { Navigate, useLocation } from 'react-router-dom'; +import { useAuth } from '../../context/AuthContext'; +import LoadingSpinner from './LoadingSpinner'; + +const ProtectedRoute = ({ children }) => { + const { user, loading } = useAuth(); + const location = useLocation(); + + // Show loading while checking authentication + if (loading) { + return ; + } + + // If not authenticated, redirect to login + if (!user) { + return ; + } + + // If authenticated, render the protected content + return children; +}; + +export default ProtectedRoute; \ No newline at end of file diff --git a/client/src/components/common/PublicRoute.jsx b/client/src/components/common/PublicRoute.jsx new file mode 100644 index 0000000..15dcdb7 --- /dev/null +++ b/client/src/components/common/PublicRoute.jsx @@ -0,0 +1,27 @@ +import { Navigate } from 'react-router-dom'; +import { useAuth } from '../../context/AuthContext'; + +const PublicRoute = ({ children }) => { + const { user, loading } = useAuth(); + + if (loading) { + return
Loading...
; + } + + // If already logged in, redirect to dashboard + if (user) { + return ; + } + + // If not logged in, show the public page + return children; +}; + +const loadingStyle = { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + minHeight: '80vh', +}; + +export default PublicRoute; \ 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 ( +
+
+

© 2024 {/* YOUR PLATFORM NAME */}. All rights reserved.

+

Built by {/* YOUR NAME */}

+
+
+ ); +}; + +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..f316def 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -1,68 +1,35 @@ -: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; +h1, h2, h3, h4, h5, h6 { + margin-bottom: 0.5rem; } -body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -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; } +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} \ 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..2fb04b7 --- /dev/null +++ b/client/src/pages/Dashboard.jsx @@ -0,0 +1,146 @@ +import { useNavigate, Navigate } from "react-router-dom"; +// 1. Import the useAuth hook +import { useAuth } from "../context/AuthContext"; +import api from "../services/api"; + +function Dashboard() { + // 2. Grab user, logout, and loading from our new Context + const { user, logout, loading } = useAuth(); + const navigate = useNavigate(); + + // 3. Instead of the manual useEffect, we use the loading state from Context + if (loading) { + return ( +
+ Loading... +
+ ); + } + + // 4. If there is no user, send them to login automatically + if (!user) { + return ; + } + const fetchUsers = async () => { + try { + const response = await api.get("/api/users"); + const users = response.data; + console.log(users); + } catch (error) { + console.error("Error fetching users:", error); + } + }; + + // Update Profile + const updateProfile = async () => { + try { + const response = await api.put("/api/profile", { + name: "Anisha", + bio: "Full Stack Developer" + }); + + console.log("Profile updated:", response.data); + } catch (error) { + console.error("Update failed:", error); + } + }; + + // Delete Post + const deletePost = async (id) => { + try { + await api.delete(`/api/posts/${id}`); + console.log("Post deleted"); + } catch (error) { + console.error("Delete failed:", error); + } + }; + + + return ( +
+
+

Welcome, {user.name}!

+ {/* 5. Use the logout function from context */} + +
+ +
+
+

Your Account

+
+

Name: {user.name}

+

Email: {user.email}

+

+ Member Since:{" "} + {user.createdAt + ? new Date(user.createdAt).toLocaleDateString() + : "N/A"} +

+
+
+ +
+

Dashboard Features

+

This is your personalized dashboard. Future features will include:

+
    +
  • Create and manage your content
  • +
  • View your statistics
  • +
  • Edit your profile
  • +
  • See your activity
  • +
+
+
+
+ ); +} + +// --- STYLES (Stay exactly the same) --- +const containerStyle = { + minHeight: "80vh", + padding: "2rem", + maxWidth: "1200px", + margin: "0 auto", +}; + +const headerStyle = { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + marginBottom: "2rem", + padding: "1rem", + backgroundColor: "white", + borderRadius: "8px", + boxShadow: "0 2px 4px rgba(0,0,0,0.1)", +}; + +const logoutButtonStyle = { + padding: "0.5rem 1.5rem", + backgroundColor: "#dc3545", + color: "white", + border: "none", + borderRadius: "5px", + cursor: "pointer", + fontWeight: "500", +}; + +const contentStyle = { + display: "grid", + gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))", + gap: "2rem", +}; + +const cardStyle = { + padding: "2rem", + backgroundColor: "white", + borderRadius: "8px", + boxShadow: "0 2px 4px rgba(0,0,0,0.1)", +}; + +const infoStyle = { + marginTop: "1rem", + lineHeight: "2", +}; + +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..fb6a548 --- /dev/null +++ b/client/src/pages/Login.jsx @@ -0,0 +1,259 @@ +import { useState } from 'react'; +import { useLocation, useNavigate, Link } from 'react-router-dom'; +import { useAuth } from '../context/AuthContext'; +import api from "../services/api"; + +const Login = () => { + const location = useLocation(); + const navigate = useNavigate(); + const { login } = useAuth(); + + const [formData, setFormData] = useState({ + email: '', + password: '' + }); + + + const [errors, setErrors] = useState({}); + const [isLoading, setIsLoading] = useState(false); + const [apiError, setApiError] = useState(''); + + const handleChange = (e) => { + const { name, value } = e.target; + + setFormData(prev => ({ + ...prev, + [name]: value + })); + + if (errors[name]) { + setErrors(prev => ({ + ...prev, + [name]: '' + })); + } + + if (apiError) setApiError(''); + }; + + 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; + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setApiError(''); + if (!validateForm()) return; + setIsLoading(true); + try { + const response = await api.post('/api/auth/login', { + email: formData.email, + password: formData.password + }); + const data = response.data; + if (response.status === 200) { + login(data.user, data.token); + setFormData({ email: '', password: '' }); + const from = location.state?.from?.pathname || '/dashboard'; + navigate(from, { replace: true }); + } + } catch (error) { + console.error('Login error:', error); + setApiError('Unable to connect to server. Please try again.'); + } finally { + setIsLoading(false); + } + }; + + + 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 +

+ +
+
+ ); +}; + +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/src/services/api.js b/client/src/services/api.js new file mode 100644 index 0000000..b792d93 --- /dev/null +++ b/client/src/services/api.js @@ -0,0 +1,41 @@ +import axios from 'axios'; + +// Create instance +const api = axios.create({ + baseURL: import.meta.env.VITE_API_URL || 'http://localhost:5000', + timeout: 10000, + headers: { + 'Content-Type': 'application/json', + }, +}); + +// Request interceptor - attach token +api.interceptors.request.use( + (config) => { + const token = localStorage.getItem('token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => { + return Promise.reject(error); + } +); + +// Response interceptor - handle errors +api.interceptors.response.use( + (response) => { + return response; + }, + (error) => { + if (error.response?.status === 401) { + localStorage.removeItem('token'); + localStorage.removeItem('user'); + window.location.href = '/login'; + } + return Promise.reject(error); + } +); + +export default api; \ 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..41eb3e6 --- /dev/null +++ b/server/controllers/authController.js @@ -0,0 +1,56 @@ +import jwt from "jsonwebtoken"; +import bcrypt from "bcryptjs"; +import User from "../models/User.js"; + +export const loginUser = async (req, res) => { + try { + const { email, password } = req.body || {}; + + if (!email || !password) { + return res.status(400).json({ + success: false, + message: "Please provide email and password" + }); + } + + const user = await User.findOne({ email }).select("+password"); + + if (!user) { + return res.status(401).json({ + success: false, + message: "Invalid credentials" + }); + } + + const isMatch = await bcrypt.compare(password, user.password); + console.log("LOGIN SECRET:", process.env.JWT_SECRET); + + if (!isMatch) { + return res.status(401).json({ + success: false, + message: "Invalid credentials" + }); + } + + const token = jwt.sign( + { id: user._id, email: user.email }, + process.env.JWT_SECRET, + { expiresIn: process.env.JWT_EXPIRE } + ); + + user.password = undefined; + + res.status(200).json({ + success: true, + token, + data: user + }); + + } catch (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/postController.js b/server/controllers/postController.js new file mode 100644 index 0000000..e3e4676 --- /dev/null +++ b/server/controllers/postController.js @@ -0,0 +1,89 @@ +import Post from '../models/Post.js'; + +// @desc Create new post +// @route POST /api/posts +// @access Private +export const createPost = async (req, res) => { + try { + const { title, content, category, status } = req.body; + + // Validate required fields + if (!title || !content) { + return res.status(400).json({ + success: false, + message: 'Please provide title and content' + }); + } + + // Create post with authenticated user as author + const post = await Post.create({ + title, + content, + category, + status, + author: req.user._id // From protect middleware + }); + + res.status(201).json({ + success: true, + message: 'Post created successfully', + data: post + }); + + } catch (error) { + console.error('Create post error:', error); + res.status(500).json({ + success: false, + message: 'Error creating post', + error: error.message + }); + } +}; + +// @desc Get posts with pagination +// @route GET /api/posts?page=1&limit=10 +// @access Private +export const getPosts = async (req, res) => { + try { + // Get page and limit from query params (with defaults) + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 10; + + // Calculate skip value + const skip = (page - 1) * limit; + + // Get posts for logged-in user only + const posts = await Post.find({ author: req.user._id }) + .sort({ createdAt: -1 }) // Newest first + .skip(skip) + .limit(limit) + .populate('author', 'name email'); // Include author info + + // Get total count for pagination + const total = await Post.countDocuments({ author: req.user._id }); + + // Calculate total pages + const totalPages = Math.ceil(total / limit); + + res.status(200).json({ + success: true, + data: posts, + pagination: { + page, + limit, + total, + totalPages, + hasNextPage: page < totalPages, + hasPrevPage: page > 1 + } + }); + + } catch (error) { + console.error('Get posts error:', error); + res.status(500).json({ + success: false, + message: 'Error fetching posts', + error: error.message + }); + } +}; \ No newline at end of file diff --git a/server/controllers/userController.js b/server/controllers/userController.js index 4544257..10467d6 100644 --- a/server/controllers/userController.js +++ b/server/controllers/userController.js @@ -1,5 +1,6 @@ import User from '../models/user.js'; import bcrypt from 'bcrypt'; +import jwt from 'jsonwebtoken'; /* =========================== Register User @@ -20,7 +21,7 @@ const registerUser = async (req, res) => { if (existingUser) { return res.status(400).json({ success: false, - message: 'User already exists with this email' + message: 'User already exists' }); } @@ -36,157 +37,99 @@ const registerUser = async (req, res) => { res.status(201).json({ success: true, - message: 'User registered successfully', data: user }); } catch (error) { res.status(500).json({ success: false, - message: 'Server error during registration', - error: error.message + message: error.message }); } }; - /* =========================== - Get All Users + Login User ✅ ONLY ONCE =========================== */ -const getAllUsers = async (req, res) => { +const loginUser = async (req, res) => { try { - const users = await User.find().select('-password'); - - res.status(200).json({ - success: true, - count: users.length, - data: users - }); - - } catch (error) { - res.status(500).json({ - success: false, - message: 'Error fetching users', - error: error.message - }); - } -}; - + const { email, password } = req.body || {}; -/* =========================== - Get Single User -=========================== */ -const getUserById = async (req, res) => { - try { - const { id } = req.params; - - const user = await User.findById(id).select('-password'); - - if (!user) { - return res.status(404).json({ + if (!email || !password) { + return res.status(400).json({ success: false, - message: 'User not found' + message: 'Email and password required' }); } - res.status(200).json({ - success: true, - data: user - }); - - } catch (error) { - res.status(500).json({ - success: false, - message: 'Error fetching user', - error: error.message - }); - } -}; - - -/* =========================== - Update User -=========================== */ -const updateUser = async (req, res) => { - try { - const { id } = req.params; - const { name, email } = req.body || {}; + const user = await User.findOne({ email }); - if (!name && !email) { - return res.status(400).json({ + if (!user) { + return res.status(401).json({ success: false, - message: 'Please provide at least one field to update' + message: 'Invalid credentials' }); } - const user = await User.findById(id); + const isMatch = await bcrypt.compare(password, user.password); - if (!user) { - return res.status(404).json({ + if (!isMatch) { + return res.status(401).json({ success: false, - message: 'User not found' + message: 'Invalid credentials' }); } - if (name) user.name = name; - if (email) user.email = email; - - await user.save(); - - user.password = undefined; + const token = jwt.sign( + { id: user._id, name: user.name }, + process.env.JWT_SECRET, + { expiresIn: '2h' } + ); res.status(200).json({ success: true, - message: 'User updated successfully', - data: user + token }); } catch (error) { res.status(500).json({ success: false, - message: 'Error updating user', - error: error.message + message: error.message }); } }; - /* =========================== - Delete User + Other Controllers =========================== */ -const deleteUser = async (req, res) => { - try { - const { id } = req.params; - - const user = await User.findByIdAndDelete(id); - - if (!user) { - return res.status(404).json({ - success: false, - message: 'User not found' - }); - } +const getAllUsers = async (req, res) => { + const users = await User.find().select('-password'); + res.json(users); +}; - res.status(200).json({ - success: true, - message: 'User deleted successfully' - }); +const getUserById = async (req, res) => { + const user = await User.findById(req.params.id).select('-password'); + res.json(user); +}; - } catch (error) { - res.status(500).json({ - success: false, - message: 'Error deleting user', - error: error.message - }); - } +const updateUser = async (req, res) => { + const user = await User.findByIdAndUpdate(req.params.id, req.body, { + new: true + }); + res.json(user); }; +const deleteUser = async (req, res) => { + await User.findByIdAndDelete(req.params.id); + res.json({ message: 'User deleted' }); +}; /* =========================== - EXPORTS + EXPORTS (ONLY ONCE) =========================== */ export { registerUser, + loginUser, getAllUsers, getUserById, updateUser, diff --git a/server/middleware/auth.js b/server/middleware/auth.js new file mode 100644 index 0000000..303e714 --- /dev/null +++ b/server/middleware/auth.js @@ -0,0 +1,48 @@ +import jwt from 'jsonwebtoken'; +import User from '../models/User.js'; + +export const protect = async (req, res, next) => { + try { + // Get token from header + let token; + + if (req.headers.authorization && + req.headers.authorization.startsWith('Bearer')) { + // Extract token from "Bearer " + token = req.headers.authorization.split(' ')[1]; + } + + // Check if token exists + if (!token) { + return res.status(401).json({ + success: false, + message: 'Not authorized, no token' + }); + } + + // Verify token + const decoded = jwt.verify(token, process.env.JWT_SECRET); + + // Get user from token (exclude password) + req.user = await User.findById(decoded.id).select('-password'); + console.log("LOGIN SECRET:", process.env.JWT_SECRET); + + if (!req.user) { + return res.status(401).json({ + success: false, + message: 'User not found' + }); + } + + // Continue to next middleware/route + next(); + + }catch (error) { + console.error("JWT ERROR:", error.message); + + return res.status(401).json({ + success: false, + message: error.message + }); +} +}; \ No newline at end of file diff --git a/server/middleware/authMiddleware.js b/server/middleware/authMiddleware.js new file mode 100644 index 0000000..e69de29 diff --git a/server/models/Post.js b/server/models/Post.js new file mode 100644 index 0000000..36d0b17 --- /dev/null +++ b/server/models/Post.js @@ -0,0 +1,40 @@ +import mongoose from 'mongoose'; + +const postSchema = new mongoose.Schema( + { + title: { + type: String, + required: [true, 'Title is required'], + trim: true, + maxlength: [100, 'Title cannot exceed 100 characters'] + }, + content: { + type: String, + required: [true, 'Content is required'], + minlength: [10, 'Content must be at least 10 characters'] + }, + author: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true + }, + category: { + type: String, + enum: ['Technology', 'Lifestyle', 'Travel', 'Food'], + default: 'Technology' + }, + status: { + type: String, + enum: ['draft', 'published'], + default: 'draft' + } + }, + { + timestamps: true // Adds createdAt and updatedAt + } +); + +// ✅ Fix for OverwriteModelError and hot reload +const Post = mongoose.models.Post || mongoose.model('Post', postSchema); + +export default Post; \ No newline at end of file diff --git a/server/models/user.js b/server/models/user.js index 6838fda..f781fd7 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -24,14 +24,15 @@ const userSchema = new mongoose.Schema( type: String, required: [true, 'Password is required'], minlength: [6, 'Password must be at least 6 characters'], - select: false // Don't return password in queries by default + select: false } }, { - timestamps: true // Automatically creates createdAt and updatedAt fields + timestamps: true } ); -const User = mongoose.model('User', userSchema); +// ✅ Fix for OverwriteModelError +const User = mongoose.models.User || mongoose.model('User', userSchema); export default User; \ No newline at end of file diff --git a/server/package-lock.json b/server/package-lock.json index 144a0a2..eb89826 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -10,9 +10,11 @@ "license": "ISC", "dependencies": { "bcrypt": "^6.0.0", + "bcryptjs": "^3.0.3", "cors": "^2.8.6", "dotenv": "^17.3.1", "express": "^5.2.1", + "jsonwebtoken": "^9.0.3", "mongoose": "^9.2.2" }, "devDependencies": { @@ -94,6 +96,15 @@ "node": ">= 18" } }, + "node_modules/bcryptjs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", + "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -166,6 +177,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 +355,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 +749,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 +801,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 +1263,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 +1293,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..ea8aa54 100644 --- a/server/package.json +++ b/server/package.json @@ -13,9 +13,11 @@ "license": "ISC", "dependencies": { "bcrypt": "^6.0.0", + "bcryptjs": "^3.0.3", "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..e05582d --- /dev/null +++ b/server/routes/authRoutes.js @@ -0,0 +1,8 @@ +import express from 'express'; +import { loginUser } from '../controllers/authController.js'; + +const router = express.Router(); + +router.post('/login', loginUser); + +export default router; \ No newline at end of file diff --git a/server/routes/postRoutes.js b/server/routes/postRoutes.js new file mode 100644 index 0000000..e014f6b --- /dev/null +++ b/server/routes/postRoutes.js @@ -0,0 +1,11 @@ +import express from 'express'; +import { protect } from '../middleware/auth.js'; +import { createPost, getPosts } from '../controllers/postController.js'; + +const router = express.Router(); + +// Both routes require authentication +router.post('/', protect, createPost); +router.get('/', protect, getPosts); + +export default router; \ No newline at end of file diff --git a/server/routes/userRoutes.js b/server/routes/userRoutes.js index 736cdf5..9d9768d 100644 --- a/server/routes/userRoutes.js +++ b/server/routes/userRoutes.js @@ -1,6 +1,8 @@ import express from 'express'; +import { protect } from '../middleware/auth.js'; import { registerUser, + loginUser, getAllUsers, getUserById, updateUser, @@ -9,11 +11,14 @@ import { const router = express.Router(); -// User routes +// Public routes router.post('/register', registerUser); -router.get('/', getAllUsers); -router.get('/:id', getUserById); -router.put('/:id', updateUser); -router.delete('/:id', deleteUser); +router.post('/login', loginUser); + +// Protected routes +router.get('/', protect, getAllUsers); +router.get('/:id', protect, getUserById); +router.put('/:id', protect, updateUser); +router.delete('/:id', protect, deleteUser); export default router; \ No newline at end of file diff --git a/server/server.js b/server/server.js index 73a42c3..bfc5028 100644 --- a/server/server.js +++ b/server/server.js @@ -3,33 +3,36 @@ 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 +import postRoutes from './routes/postRoutes.js'; -// 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()); +app.use('/api/posts', postRoutes); + // 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