diff --git a/auth.js b/auth.js new file mode 100644 index 0000000..909b7e7 --- /dev/null +++ b/auth.js @@ -0,0 +1,15 @@ +router.get("/logout", (req, res) => { + req.logout(err => { + if(err) return res.status(500).json({ message: "Logout failed" }); + res.json({ message: "Logged out successfully" }); + }); +}); + +// Get logged-in user +router.get("/current", (req, res) => { + if (req.isAuthenticated()) { + res.json({ user: { username: req.user.username } }); + } else { + res.status(401).json({ user: null }); + } +}); diff --git a/backend/config/db.js b/backend/config/db.js new file mode 100644 index 0000000..6311040 --- /dev/null +++ b/backend/config/db.js @@ -0,0 +1,16 @@ +const mongoose = require('mongoose'); + +const connectDB = async () => { + try { + await mongoose.connect('mongodb://127.0.0.1:27017/githubTracker', { + useNewUrlParser: true, + useUnifiedTopology: true, + }); + console.log('MongoDB connected'); + } catch (err) { + console.error(err.message); + process.exit(1); + } +}; + +module.exports = connectDB; diff --git a/backend/config/passport.js b/backend/config/passport.js new file mode 100644 index 0000000..9f8c896 --- /dev/null +++ b/backend/config/passport.js @@ -0,0 +1,37 @@ +import passport from 'passport'; +import { Strategy as LocalStrategy } from 'passport-local'; +import bcrypt from 'bcryptjs'; +import User from '../models/User.js'; + +passport.use( + new LocalStrategy(async (username, password, done) => { + try { + // Find user by username + const user = await User.findOne({ username }); + if (!user) return done(null, false, { message: 'Incorrect username' }); + + // Compare passwords + const isMatch = await bcrypt.compare(password, user.password); + if (!isMatch) return done(null, false, { message: 'Incorrect password' }); + + return done(null, user); + } catch (err) { + return done(err); + } + }) +); + +// Serialize user into the session +passport.serializeUser((user, done) => { + done(null, user.id); +}); + +// Deserialize user from the session +passport.deserializeUser(async (id, done) => { + try { + const user = await User.findById(id).select('-password'); // Exclude password + done(null, user); + } catch (err) { + done(err); + } +}); diff --git a/backend/config/passportConfig.js b/backend/config/passportConfig.js index 842f50c..13a0a1b 100644 --- a/backend/config/passportConfig.js +++ b/backend/config/passportConfig.js @@ -2,14 +2,15 @@ const passport = require("passport"); const LocalStrategy = require('passport-local').Strategy; const User = require("../models/User"); +// Local strategy for login using email passport.use( new LocalStrategy( - { usernameField: "email" }, + { usernameField: "email" }, // use email instead of username async (email, password, done) => { try { - const user = await User.findOne( {email} ); + const user = await User.findOne({ email }); if (!user) { - return done(null, false, { message: 'Email is invalid '}); + return done(null, false, { message: 'Email is invalid' }); } const isMatch = await user.comparePassword(password); @@ -17,8 +18,9 @@ passport.use( return done(null, false, { message: 'Invalid password' }); } + // Return user object return done(null, { - id : user._id.toString(), + id: user._id.toString(), username: user.username, email: user.email }); @@ -29,7 +31,7 @@ passport.use( ) ); -// Serialize user (store user info in session) +// Serialize user (store user id in session) passport.serializeUser((user, done) => { done(null, user.id); }); diff --git a/backend/routes/auth.js b/backend/routes/auth.js index e26c7a9..f0c2d76 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -1,42 +1,44 @@ -const express = require("express"); -const passport = require("passport"); -const User = require("../models/User"); +const express = require('express'); const router = express.Router(); +const passport = require('passport'); +const User = require('../models/User'); + +// Signup +router.post('/signup', async (req, res) => { + const { username, email, password } = req.body; + try { + const user = new User({ username, email, password }); + await user.save(); + res.status(201).json({ message: 'User created successfully' }); + } catch (err) { + res.status(400).json({ error: err.message }); + } +}); -// Signup route -router.post("/signup", async (req, res) => { - - const { username, email, password } = req.body; - - try { - const existingUser = await User.findOne( {email} ); - - if (existingUser) - return res.status(400).json( {message: 'User already exists'} ); +// Login +router.post('/login', (req, res, next) => { + passport.authenticate('local', (err, user, info) => { + if (err) return next(err); + if (!user) return res.status(400).json({ message: info.message }); - const newUser = new User( {username, email, password} ); - await newUser.save(); - res.status(201).json( {message: 'User created successfully'} ); - } catch (err) { - res.status(500).json({ message: 'Error creating user', error: err.message }); - } + req.login(user, (err) => { + if (err) return next(err); + res.json({ message: 'Logged in successfully', user }); + }); + })(req, res, next); }); -// Login route -router.post("/login", passport.authenticate('local'), (req, res) => { - res.status(200).json( { message: 'Login successful', user: req.user } ); +// Logout +router.get('/logout', (req, res) => { + req.logout(() => { + res.json({ message: 'Logged out successfully' }); + }); }); -// Logout route -router.get("/logout", (req, res) => { - - req.logout((err) => { - - if (err) - return res.status(500).json({ message: 'Logout failed', error: err.message }); - else - res.status(200).json({ message: 'Logged out successfully' }); - }); +// Protected route (example) +router.get('/profile', (req, res) => { + if (!req.isAuthenticated()) return res.status(401).json({ message: 'Not authenticated' }); + res.json({ user: req.user }); }); module.exports = router; diff --git a/client/.gitignore b/client/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/client/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000..7059a96 --- /dev/null +++ b/client/README.md @@ -0,0 +1,12 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project. diff --git a/client/eslint.config.js b/client/eslint.config.js new file mode 100644 index 0000000..cee1e2c --- /dev/null +++ b/client/eslint.config.js @@ -0,0 +1,29 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +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['recommended-latest'], + 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_]' }], + }, + }, +]) diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..0c589ec --- /dev/null +++ b/client/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + + +
+ + + diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..8d688cf --- /dev/null +++ b/client/package.json @@ -0,0 +1,29 @@ +{ + "name": "client", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.11.0", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-router-dom": "^7.8.2" + }, + "devDependencies": { + "@eslint/js": "^9.33.0", + "@types/react": "^19.1.10", + "@types/react-dom": "^19.1.7", + "@vitejs/plugin-react": "^5.0.0", + "eslint": "^9.33.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.3.0", + "vite": "^7.1.2" + } +} diff --git a/client/public/vite.svg b/client/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/client/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/App.css b/client/src/App.css new file mode 100644 index 0000000..b9d355d --- /dev/null +++ b/client/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/client/src/App.jsx b/client/src/App.jsx new file mode 100644 index 0000000..f67355a --- /dev/null +++ b/client/src/App.jsx @@ -0,0 +1,35 @@ +import { useState } from 'react' +import reactLogo from './assets/react.svg' +import viteLogo from '/vite.svg' +import './App.css' + +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 +

+ + ) +} + +export default App diff --git a/client/src/assets/react.svg b/client/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/client/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/index.css b/client/src/index.css new file mode 100644 index 0000000..08a3ac9 --- /dev/null +++ b/client/src/index.css @@ -0,0 +1,68 @@ +: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; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +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 { + font-size: 3.2em; + line-height: 1.1; +} + +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; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/client/src/main.jsx b/client/src/main.jsx new file mode 100644 index 0000000..b9a1a6d --- /dev/null +++ b/client/src/main.jsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import './index.css' +import App from './App.jsx' + +createRoot(document.getElementById('root')).render( + + + , +) diff --git a/client/vite.config.js b/client/vite.config.js new file mode 100644 index 0000000..8b0f57b --- /dev/null +++ b/client/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/config/passport.js b/config/passport.js new file mode 100644 index 0000000..2ce6848 --- /dev/null +++ b/config/passport.js @@ -0,0 +1,33 @@ +import passport from 'passport'; +import { Strategy as LocalStrategy } from 'passport-local'; +import bcrypt from 'bcryptjs'; +import User from '../models/User.js'; + +export default function configurePassport() { + passport.use( + new LocalStrategy(async (username, password, done) => { + try { + const user = await User.findOne({ username }); + if (!user) return done(null, false, { message: 'Incorrect username.' }); + + const isMatch = await bcrypt.compare(password, user.password); + if (!isMatch) return done(null, false, { message: 'Incorrect password.' }); + + return done(null, user); + } catch (err) { + return done(err); + } + }) + ); + + passport.serializeUser((user, done) => done(null, user.id)); + + passport.deserializeUser(async (id, done) => { + try { + const user = await User.findById(id); + done(null, user); + } catch (err) { + done(err, null); + } + }); +} diff --git a/github-tracker-auth/index.js b/github-tracker-auth/index.js new file mode 100644 index 0000000..3ac3e42 --- /dev/null +++ b/github-tracker-auth/index.js @@ -0,0 +1,54 @@ +const express = require("express"); +const bcrypt = require("bcryptjs"); +const jwt = require("jsonwebtoken"); + +const app = express(); +app.use(express.json()); + +const users = []; // Temporary in-memory storage + +// 🔑 Register route +app.post("/register", async (req, res) => { + const { username, password } = req.body; + + // check if user exists + const existingUser = users.find(u => u.username === username); + if (existingUser) { + return res.status(400).json({ message: "User already exists" }); + } + + // hash password + const hashedPassword = await bcrypt.hash(password, 10); + users.push({ username, password: hashedPassword }); + res.json({ message: "User registered successfully 🚀" }); +}); + +// 🔑 Login route +app.post("/login", async (req, res) => { + const { username, password } = req.body; + + const user = users.find(u => u.username === username); + if (!user) return res.status(400).json({ message: "Invalid credentials" }); + + const isMatch = await bcrypt.compare(password, user.password); + if (!isMatch) return res.status(400).json({ message: "Invalid credentials" }); + + // generate token + const token = jwt.sign({ username }, "secretKey123", { expiresIn: "1h" }); + res.json({ message: "Login successful 🎉", token }); +}); + +// Protected route +app.get("/profile", (req, res) => { + const authHeader = req.headers["authorization"]; + if (!authHeader) return res.status(401).json({ message: "No token provided" }); + + const token = authHeader.split(" ")[1]; + jwt.verify(token, "secretKey123", (err, decoded) => { + if (err) return res.status(403).json({ message: "Invalid token" }); + + res.json({ message: `Welcome ${decoded.username} 🎉`, user: decoded }); + }); +}); + +app.listen(3000, () => console.log("Server is running 🚀")); diff --git a/github-tracker-auth/package.json b/github-tracker-auth/package.json new file mode 100644 index 0000000..08a686e --- /dev/null +++ b/github-tracker-auth/package.json @@ -0,0 +1,19 @@ +{ + "name": "github-tracker-auth", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "bcryptjs": "^3.0.2", + "express": "^5.1.0", + "express-session": "^1.18.2", + "passport": "^0.7.0", + "passport-local": "^1.0.0" + } +} diff --git a/middleware/auth.js b/middleware/auth.js new file mode 100644 index 0000000..28f3675 --- /dev/null +++ b/middleware/auth.js @@ -0,0 +1,13 @@ +export function ensureAuthenticated(req, res, next) { + if (req.isAuthenticated()) { + return next(); + } + res.status(401).json({ msg: 'You must be logged in to access this route' }); +} + +export function forwardAuthenticated(req, res, next) { + if (!req.isAuthenticated()) { + return next(); + } + res.status(403).json({ msg: 'You are already logged in' }); +} diff --git a/models/User.js b/models/User.js new file mode 100644 index 0000000..a194b85 --- /dev/null +++ b/models/User.js @@ -0,0 +1,9 @@ +// models/User.js +import mongoose from 'mongoose'; + +const userSchema = new mongoose.Schema({ + username: { type: String, required: true, unique: true }, + password: { type: String, required: true }, +}); + +export default mongoose.model('User', userSchema); diff --git a/package.json b/package.json index f2d89f5..d8a14ee 100644 --- a/package.json +++ b/package.json @@ -1,56 +1,40 @@ { - "name": "my-react-tailwind-app", - "private": true, - "version": "0.0.0", + "name": "github-tracker-server", + "version": "1.0.0", + "description": "Backend for GitHub Tracker", + "main": "server.js", "type": "module", "scripts": { - "dev": "vite --host", - "build": "vite build", - "lint": "eslint .", - "preview": "vite preview", - "docker:dev": "docker compose --profile dev up --build", - "docker:prod": "docker compose --profile prod up -d --build" + "start": "node server.js", + "dev": "nodemon server.js", + "test": "jasmine" }, "dependencies": { - "@emotion/react": "^11.11.3", - "@emotion/styled": "^11.11.0", - "@mui/icons-material": "^5.15.6", - "@mui/material": "^5.15.6", - "@primer/octicons-react": "^19.15.5", - "@vitejs/plugin-react": "^4.3.3", - "axios": "^1.7.7", - "framer-motion": "^12.23.12", - "lucide-react": "^0.525.0", - "octokit": "^4.0.2", - "postcss": "^8.4.47", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-hot-toast": "^2.4.1", - "react-icons": "^5.3.0", - "react-router-dom": "^6.28.0", - "tailwindcss": "^3.4.14" - }, - "devDependencies": { - "@eslint/js": "^9.13.0", - "@types/jasmine": "^5.1.8", - "@types/node": "^22.10.1", - "@types/react": "^18.3.23", - "@types/react-dom": "^18.3.7", - "@types/react-redux": "^7.1.34", - "@types/react-router-dom": "^5.3.3", - "@vitejs/plugin-react-swc": "^3.5.0", - "autoprefixer": "^10.4.20", "bcryptjs": "^3.0.2", - "eslint": "^9.13.0", - "eslint-plugin-react": "^7.37.2", - "eslint-plugin-react-hooks": "^5.0.0", - "eslint-plugin-react-refresh": "^0.4.14", + "body-parser": "^1.20.2", + "connect-flash": "^0.1.1", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", "express-session": "^1.18.2", - "globals": "^15.11.0", - "jasmine": "^5.9.0", + "mongoose": "^7.5.0", "passport": "^0.7.0", - "passport-local": "^1.0.0", + "passport-local": "^1.0.0" + }, + "devDependencies": { + "jasmine": "^5.9.0", "supertest": "^7.1.4", - "vite": "^5.4.10" - } + "nodemon": "^3.0.1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/gaurivvv/github_tracker.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/gaurivvv/github_tracker/issues" + }, + "homepage": "https://github.com/gaurivvv/github_tracker#readme" } diff --git a/routes/auth.js b/routes/auth.js new file mode 100644 index 0000000..69538f0 --- /dev/null +++ b/routes/auth.js @@ -0,0 +1,55 @@ +// routes/auth.js +import express from 'express'; +import bcrypt from 'bcryptjs'; +import passport from 'passport'; +import User from '../models/User.js'; + +const router = express.Router(); + +// ===== Signup Route ===== +router.post('/signup', async (req, res) => { + const { username, password } = req.body; + + if (!username || !password) { + return res.status(400).json({ msg: 'Please enter all fields' }); + } + + try { + const existingUser = await User.findOne({ username }); + if (existingUser) { + return res.status(400).json({ msg: 'Username already exists' }); + } + + const hashedPassword = await bcrypt.hash(password, 10); + const newUser = new User({ username, password: hashedPassword }); + await newUser.save(); + + res.status(201).json({ msg: 'User registered successfully' }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// ===== Login Route ===== +router.post('/login', (req, res, next) => { + passport.authenticate('local', (err, user, info) => { + if (err) return next(err); + if (!user) return res.status(400).json({ msg: info.message }); + + req.logIn(user, (err) => { + if (err) return next(err); + res.json({ msg: 'Logged in successfully', user: { username: user.username } }); + }); + })(req, res, next); +}); + +// ===== Logout Route ===== +router.get('/logout', (req, res) => { + req.logout((err) => { + if (err) return res.status(500).json({ error: err.message }); + res.json({ msg: 'Logged out successfully' }); + }); +}); + +// Export router as default +export default router; diff --git a/server.js b/server.js new file mode 100644 index 0000000..70b311b --- /dev/null +++ b/server.js @@ -0,0 +1,47 @@ +import express from 'express'; +import mongoose from 'mongoose'; +import session from 'express-session'; +import passport from 'passport'; +import cors from 'cors'; +import dotenv from 'dotenv'; + +import authRoutes from './routes/auth.js'; +import './config/passport.js'; // make sure passport config exports nothing; it just sets up strategies + +dotenv.config(); + +const app = express(); + +// Middleware +app.use(cors()); +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); + +// Sessions +app.use( + session({ + secret: process.env.SESSION_SECRET || 'defaultsecret', + resave: false, + saveUninitialized: false, + }) +); + +// Passport middleware +app.use(passport.initialize()); +app.use(passport.session()); + +// Routes +app.use('/api/auth', authRoutes); + +// Connect to MongoDB +mongoose + .connect(process.env.MONGO_URI, { + useNewUrlParser: true, + useUnifiedTopology: true, + }) + .then(() => console.log('MongoDB connected')) + .catch(err => console.log('MongoDB connection error:', err)); + +// Start server +const PORT = process.env.PORT || 5000; +app.listen(PORT, () => console.log(`Server running on port ${PORT}`)); diff --git a/server/server.js b/server/server.js new file mode 100644 index 0000000..b45eb01 --- /dev/null +++ b/server/server.js @@ -0,0 +1,56 @@ +// server.js +import express from 'express'; +import mongoose from 'mongoose'; +import dotenv from 'dotenv'; +import session from 'express-session'; +import passport from 'passport'; +import cors from 'cors'; +import bodyParser from 'body-parser'; + +import authRoutes from './routes/auth.js'; // Auth routes +import configurePassport from './config/passport.js'; // Passport config + +// Load environment variables +dotenv.config(); + +// Initialize Express app +const app = express(); + +// Middleware +app.use(cors()); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: true })); + +// Session configuration +app.use( + session({ + secret: process.env.SESSION_SECRET || 'secretkey', + resave: false, + saveUninitialized: false, + }) +); + +// Passport configuration +configurePassport(); +app.use(passport.initialize()); +app.use(passport.session()); + +// Routes +app.use('/api/auth', authRoutes); + +// MongoDB connection +const MONGO_URI = process.env.MONGO_URI || 'mongodb://127.0.0.1:27017/github_tracker'; + +mongoose + .connect(MONGO_URI, { + useNewUrlParser: true, + useUnifiedTopology: true, + }) + .then(() => console.log('MongoDB connected')) + .catch((err) => console.error('MongoDB connection error:', err)); + +// Start server +const PORT = process.env.PORT || 5000; +app.listen(PORT, () => { + console.log(`Server running on http://localhost:${PORT}`); +}); diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 0000000..1e7d384 --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,26 @@ +import React from "react"; +import { Routes, Route, Navigate } from "react-router-dom"; +import Signup from "./pages/Signup"; +import Login from "./pages/Login"; +import Dashboard from "./pages/Dashboard"; +import React, { useEffect } from "react"; +import axios from "axios"; + +function App() { + const [user, setUser] = React.useState(null); // store logged-in user + useEffect(() => { + axios.get("http://localhost:5000/api/auth/current", { withCredentials: true }) + .then(res => setUser(res.data.user)) + .catch(err => setUser(null)); + }, []); + return ( + + : } /> + } /> + } /> + : } /> + + ); +} + +export default App; diff --git a/src/App.tsx b/src/App.tsx index b00eba8..bd6aa09 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,7 +2,7 @@ import Navbar from "./components/Navbar"; import Footer from "./components/Footer"; import ScrollProgressBar from "./components/ScrollProgressBar"; import { Toaster } from "react-hot-toast"; -import Router from "./Routes/Router"; +import Router from "./routes/Router"; import ThemeWrapper from "./context/ThemeContext"; function App() { diff --git a/src/Routes/Router.tsx b/src/Routes/Router.tsx index 40a7861..fde5e22 100644 --- a/src/Routes/Router.tsx +++ b/src/Routes/Router.tsx @@ -1,8 +1,8 @@ import { Routes, Route } from "react-router-dom"; import Tracker from "../pages/Tracker/Tracker.tsx"; -import About from "../pages/About/About"; -import Contact from "../pages/Contact/Contact"; -import Contributors from "../pages/Contributors/Contributors"; +import About from "../pages/About/About.tsx"; +import Contact from "../pages/Contact/Contact.tsx"; +import Contributors from "../pages/Contributors/Contributors.tsx"; import Signup from "../pages/Signup/Signup.tsx"; import Login from "../pages/Login/Login.tsx"; import ContributorProfile from "../pages/ContributorProfile/ContributorProfile.tsx"; diff --git a/src/Routes/auth.js b/src/Routes/auth.js new file mode 100644 index 0000000..d95564a --- /dev/null +++ b/src/Routes/auth.js @@ -0,0 +1,49 @@ +import express from 'express'; +import bcrypt from 'bcryptjs'; +import passport from 'passport'; +import User from '../models/User.js'; + +const router = express.Router(); + +// Signup +router.post('/signup', async (req, res) => { + const { username, password } = req.body; + + if (!username || !password) return res.status(400).json({ msg: 'Please enter all fields' }); + + try { + const existingUser = await User.findOne({ username }); + if (existingUser) return res.status(400).json({ msg: 'Username already exists' }); + + const hashedPassword = await bcrypt.hash(password, 10); + const newUser = new User({ username, password: hashedPassword }); + await newUser.save(); + + res.status(201).json({ msg: 'User registered successfully' }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// Login +router.post('/login', (req, res, next) => { + passport.authenticate('local', (err, user, info) => { + if (err) return next(err); + if (!user) return res.status(400).json({ msg: info.message }); + + req.logIn(user, err => { + if (err) return next(err); + res.json({ msg: 'Logged in successfully', user: { username: user.username } }); + }); + })(req, res, next); +}); + +// Logout +router.get('/logout', (req, res) => { + req.logout(err => { + if (err) return res.status(500).json({ error: err.message }); + res.json({ msg: 'Logged out successfully' }); + }); +}); + +export default router; diff --git a/src/Signup.jsx b/src/Signup.jsx new file mode 100644 index 0000000..0bb39d2 --- /dev/null +++ b/src/Signup.jsx @@ -0,0 +1,29 @@ +import { useState } from 'react'; +import axios from 'axios'; +import { useNavigate } from 'react-router-dom'; + +export default function Signup() { + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const navigate = useNavigate(); + + const handleSignup = async (e) => { + e.preventDefault(); + try { + const res = await axios.post('http://localhost:5000/auth/signup', { username, password }, { withCredentials: true }); + alert(res.data.msg); + navigate('/login'); + } catch (err) { + alert(err.response?.data?.msg || 'Signup failed'); + } + }; + + return ( +
+

Signup

+ setUsername(e.target.value)} required /> + setPassword(e.target.value)} required /> + +
+ ); +} diff --git a/src/dashboard.jsx b/src/dashboard.jsx new file mode 100644 index 0000000..7775dd3 --- /dev/null +++ b/src/dashboard.jsx @@ -0,0 +1,36 @@ +import React, { useEffect, useState } from "react"; +import axios from "axios"; +import { useNavigate } from "react-router-dom"; + +const Dashboard = ({ user, setUser }) => { + const [message, setMessage] = useState(""); + const navigate = useNavigate(); + + useEffect(() => { + axios.get("http://localhost:5000/api/auth/dashboard", { withCredentials: true }) + .then(res => setMessage(res.data.message)) + .catch(err => setMessage(err.response.data.message)); + }, []); + + const handleLogout = async () => { + try { + await axios.get("http://localhost:5000/api/auth/logout", { withCredentials: true }); + setUser(null); // clear user in state + navigate("/login"); + } catch (err) { + alert("Logout failed"); + } + }; + + return ( +
+

Dashboard

+

{message}

+ +
+ ); +}; + +export default Dashboard; diff --git a/src/login.jsx b/src/login.jsx new file mode 100644 index 0000000..9f2ba00 --- /dev/null +++ b/src/login.jsx @@ -0,0 +1,29 @@ +import { useState } from 'react'; +import axios from 'axios'; +import { useNavigate } from 'react-router-dom'; + +export default function Login() { + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const navigate = useNavigate(); + + const handleLogin = async (e) => { + e.preventDefault(); + try { + const res = await axios.post('http://localhost:5000/auth/login', { username, password }, { withCredentials: true }); + alert(res.data.msg); + navigate('/dashboard'); + } catch (err) { + alert(err.response?.data?.msg || 'Login failed'); + } + }; + + return ( +
+

Login

+ setUsername(e.target.value)} required /> + setPassword(e.target.value)} required /> + +
+ ); +} diff --git a/src/main.tsx b/src/main.tsx index 4c5b79d..0495981 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,16 +1,20 @@ -import { StrictMode } from "react"; -import { createRoot } from "react-dom/client"; -import App from "./App.tsx"; -import "./index.css"; -import { BrowserRouter } from "react-router-dom"; -import ThemeWrapper from "./context/ThemeContext.tsx"; +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; +import App from './App'; +import Login from './pages/Login'; +import Signup from './pages/Signup'; +import Dashboard from './pages/Dashboard'; -createRoot(document.getElementById("root")!).render( - - - - - - - -); \ No newline at end of file +ReactDOM.createRoot(document.getElementById('root')).render( + + + + } /> + } /> + } /> + } /> + + + +); diff --git a/tailwind.config.js b/tailwind.config.js index 4cdd0f6..76034e5 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,13 +1,11 @@ /** @type {import('tailwindcss').Config} */ -module.exports = { - darkMode: 'class', - content: [ - "./index.html", // For any HTML files in the root - "./src/**/*.{js,jsx,ts,tsx}", // For all JS/JSX/TS/TSX files inside src folder - ], - theme: { - extend: {}, - }, - plugins: [], - } - \ No newline at end of file +export default { + content: [ + "./index.html", + "./src/**/*.{js,jsx,ts,tsx}" + ], + theme: { + extend: {}, + }, + plugins: [], +}