diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 00000000..3888637f --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,18 @@ +# ======================== REQUIRED VARIABLES ======================== +# Server Configuration (REQUIRED) +PORT=5000 +NODE_ENV=development + +# Database (REQUIRED) +MONGO_URI=mongodb://localhost:27017/github_tracker + +# Security (REQUIRED) +SESSION_SECRET=your-super-secret-random-key-change-in-production +CLIENT_URL=http://localhost:5173 + +# ======================== OPTIONAL VARIABLES ======================== +# Cookie Configuration (Optional - uses localhost if not set) +COOKIE_DOMAIN=localhost + +# Logging (Optional - defaults to 'info' in production, 'debug' in development) +LOG_LEVEL=debug \ No newline at end of file diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js new file mode 100644 index 00000000..a3daae2f --- /dev/null +++ b/backend/middleware/auth.js @@ -0,0 +1,11 @@ +const requireAuth = (req, res, next) => { + if (!req.isAuthenticated()) { + return res.status(401).json({ + success: false, + message: 'Authentication required' + }); + } + next(); +}; + +module.exports = { requireAuth }; \ No newline at end of file diff --git a/backend/middleware/envValidator.js b/backend/middleware/envValidator.js new file mode 100644 index 00000000..6dce8178 --- /dev/null +++ b/backend/middleware/envValidator.js @@ -0,0 +1,22 @@ +const logger = require('../logger'); + +const validateEnv = () => { + const requiredVars = [ + 'MONGO_URI', + 'PORT', + 'SESSION_SECRET', + 'CLIENT_URL', + 'NODE_ENV', + ]; + + const missingVars = requiredVars.filter(v => !process.env[v]); + + if (missingVars.length > 0) { + logger.error(`Missing required environment variables: ${missingVars.join(', ')}`); + process.exit(1); + } + + logger.info('Environment variables validated ✓'); +}; + +module.exports = { validateEnv }; \ No newline at end of file diff --git a/backend/middleware/errorHandler.js b/backend/middleware/errorHandler.js new file mode 100644 index 00000000..b6f69116 --- /dev/null +++ b/backend/middleware/errorHandler.js @@ -0,0 +1,22 @@ +const logger = require('../logger'); + +const errorHandler = (err, req, res, next) => { + const status = err.status || err.statusCode || 500; + const message = process.env.NODE_ENV === 'production' + ? 'Internal Server Error' + : err.message; + + logger.error(`[${req.method} ${req.path}] Error: ${err.message}`, err); + + res.status(status).json({ + success: false, + message, + ...(process.env.NODE_ENV !== 'production' && { stack: err.stack }), + }); +}; + +const asyncHandler = (fn) => (req, res, next) => { + Promise.resolve(fn(req, res, next)).catch(next); +}; + +module.exports = { errorHandler, asyncHandler }; \ No newline at end of file diff --git a/backend/middleware/logger.js b/backend/middleware/logger.js new file mode 100644 index 00000000..a7c39aa6 --- /dev/null +++ b/backend/middleware/logger.js @@ -0,0 +1,13 @@ +const morgan = require('morgan'); +const logger = require('../logger'); + +const morganStream = { + write: (message) => logger.info(message.trim()), +}; + +const httpLogger = morgan( + ':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent" - :response-time ms', + { stream: morganStream } +); + +module.exports = httpLogger; \ No newline at end of file diff --git a/backend/middleware/security.js b/backend/middleware/security.js new file mode 100644 index 00000000..5033ead7 --- /dev/null +++ b/backend/middleware/security.js @@ -0,0 +1,24 @@ +const helmet = require('helmet'); + +const securityHeaders = helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'nonce-randomvalue'"], + scriptSrc: ["'self'"], + imgSrc: ["'self'", 'data:', 'https://api.github.com', 'https://avatars.githubusercontent.com'], + connectSrc: ["'self'", 'https://api.github.com'], + fontSrc: ["'self'", 'https://fonts.googleapis.com'], + }, + }, + hsts: { + maxAge: 31536000, // 1 year + includeSubDomains: true, + preload: true, + }, + frameguard: { action: 'deny' }, + noSniff: true, + referrerPolicy: { policy: 'strict-origin-when-cross-origin' }, +}); + +module.exports = securityHeaders; \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index 74ab9dd7..dd2b4fb4 100644 --- a/backend/package.json +++ b/backend/package.json @@ -14,11 +14,15 @@ "dependencies": { "bcryptjs": "^2.4.3", "body-parser": "^1.20.3", + "connect-mongo": "^6.0.0", "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.21.1", + "express-rate-limit": "^8.5.2", "express-session": "^1.18.1", + "helmet": "^8.2.0", "mongoose": "^8.8.2", + "morgan": "^1.10.1", "passport": "^0.7.0", "passport-local": "^1.0.0", "winston": "^3.19.0", diff --git a/backend/routes/auth.js b/backend/routes/auth.js index 7c2cda78..59da0e3a 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -3,48 +3,84 @@ const passport = require("passport"); const User = require("../models/User"); const { signupSchema, loginSchema } = require("../validators/authValidator"); const { validateRequest } = require("../validators/validationRequest"); +const { asyncHandler } = require("../middleware/errorHandler"); +const { requireAuth } = require("../middleware/auth"); +const { success, email } = require("zod"); const router = express.Router(); // Signup route -router.post("/signup", validateRequest(signupSchema), async (req, res) => { +router.post("/signup", validateRequest(signupSchema), asyncHandler(async (req, res) => { - const { username, email, password } = req.body; + const { username, email, password } = req.body; - try { - const existingUser = await User.findOne({ - $or: [{ email }, { username }], - }); - - if (existingUser) - return res.status(400).json({ message: 'User already exists' }); + const existingUser = await User.findOne({ + $or: [{ email }, { username }], + }); - const newUser = new User({ username, email, password }); - await newUser.save(); - res.status(201).json({ message: 'User created successfully' }); - } catch (err) { - if (err && err.code === 11000) { - return res.status(400).json({ message: 'User already exists' }); - } + if (existingUser) + return res.status(400).json({ + success: false, + message: 'User already exists' + }); - res.status(500).json({ message: 'Error creating user', error: err.message }); - } -}); + const newUser = new User({ username, email, password }); + await newUser.save(); + res.status(201).json({ + success: true, + message: 'User created successfully' }); +})); // Login route -router.post("/login", validateRequest(loginSchema), passport.authenticate('local'), (req, res) => { - res.status(200).json( { message: 'Login successful', user: req.user } ); +router.post("/login", validateRequest(loginSchema), (req, res, next) => { + passport.authenticate('local', (err, user, info) => { + if (err) return next(err); + if (!user) return res.status(401).json({ + success: false, + message: 'Invalid email or password' + }); + + req.session.regenerate((regenerateErr) => { + if (regenerateErr) return next(regenerateErr); + + req.logIn(user, (loginErr) => { + if (loginErr) return next(loginErr); + res.status(200).json({ + success: true, + message: 'Login successful', + user: { id: user.id, username: user.username, email: user.email } + }); + }); + }); + })(req, res, next); }); -// Logout route -router.get("/logout", (req, res) => { +// Logout route with authentication check +router.get("/logout", requireAuth, asyncHandler(async (req, res) => { req.logout((err) => { + if (err) { + return res.status(500).json({ + success: false, + message: 'Logout failed' + }); + } + res.status(200).json({ + success: true, + message: 'Logged out successfully' + }); + }); +})); - if (err) - return res.status(500).json({ message: 'Logout failed', error: err.message }); - else - res.status(200).json({ message: 'Logged out successfully' }); +// Get current user route with authentication check +router.get("/me", requireAuth, asyncHandler(async (req, res) => { + res.status(200).json({ + success: true, + user: { + id: req.user.id, + username: req.user.username, + email: req.user.email, + }, }); -}); +})); module.exports = router; diff --git a/backend/server.js b/backend/server.js index 48d6ccfb..60f9d03c 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,8 +1,10 @@ const express = require('express'); const mongoose = require('mongoose'); const session = require('express-session'); +const MongoStore = require('connect-mongo').default; const passport = require('passport'); const bodyParser = require('body-parser'); +const rateLimit = require('express-rate-limit'); require('dotenv').config(); const cors = require('cors'); @@ -10,9 +12,20 @@ const cors = require('cors'); require('./config/passportConfig'); const logger = require('./logger'); +const securityHeaders = require('./middleware/security'); +const { validateEnv } = require('./middleware/envValidator'); +const httpLogger = require('./middleware/logger'); +const { errorHandler } = require('./middleware/errorHandler'); +const { requireAuth } = require('./middleware/auth'); + +// Validate environment variables +validateEnv(); const app = express(); +app.use(securityHeaders); +app.use(httpLogger); + // CORS configuration const allowedOrigins = ['http://localhost:5173', 'https://github-spy.etlify.app']; app.use(cors({ @@ -27,27 +40,94 @@ app.use(cors({ })); // Middleware -app.use(bodyParser.json()); +app.use(bodyParser.json({ limit: '10mb' })); +app.use(bodyParser.urlencoded({ limit: '10mb', extended: true })); + app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, + store: MongoStore.create({ + mongoUrl: process.env.MONGO_URI, + touchAfter: 24 * 3600, + }), + cookie: { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict', + maxAge: 1000 * 60 * 30, + domain: process.env.COOKIE_DOMAIN || undefined, + } })); + app.use(passport.initialize()); app.use(passport.session()); +// Rate Limiting +const authLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 10, + standardHeaders: true, + legacyHeaders: false, + message: { message: 'Too many attempts, please try again after 15 minutes.' }, + skipSuccessfulRequests: true, + // keyGenerator: (req) => req.ip, // Rate limit by IP address +}); + +// General API: 100 requests per 15 minutes +const generalLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 100, + standardHeaders: true, + legacyHeaders: false, + skip: (req) => req.isAuthenticated(), // Skip rate limiting for authenticated users +}) + +app.use('/api/auth', authLimiter); +app.use('/api', generalLimiter); + +// Health Check +app.get('/api/health', (req,res) => { + res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() }); +}); + // Routes const authRoutes = require('./routes/auth'); + app.use('/api/auth', authRoutes); + +// 404 Handler +app.use((req, res) => { + res.status(404).json({ success: false, message: 'Route not found' }); +}); + +// Global Error Handler +app.use(errorHandler); + +module.exports = app; + // Connect to MongoDB -mongoose.connect(process.env.MONGO_URI, {}).then(() => { +mongoose.connect(process.env.MONGO_URI, { + maxPoolSize: 10, + minPoolSize: 5, + waitQueueTimeoutMS: 10000, +}).then(() => { logger.info('Connected to MongoDB'); - - const PORT = process.env.PORT || 5000; - app.listen(PORT, () => { - logger.info(`Server running on port ${PORT}`); + app.listen(process.env.PORT, () => { + logger.info(`Server running on port ${process.env.PORT}`); + logger.info(`✓ Environment: ${process.env.NODE_ENV}`); }); }).catch((err) => { logger.error('MongoDB connection error', err); + process.exit(1); }); + +// Graceful shutdown +process.on('SIGTERM', () => { + logger.info('SIGTERM received, shutting down gracefully'); + mongoose.connection.close(false, () => { + logger.info('MongoDB connection closed'); + process.exit(0); + }) +}) diff --git a/backend/spec/auth-security.spec.cjs b/backend/spec/auth-security.spec.cjs new file mode 100644 index 00000000..5099455f --- /dev/null +++ b/backend/spec/auth-security.spec.cjs @@ -0,0 +1,279 @@ +const request = require('supertest'); +const mongoose = require('mongoose'); +require('dotenv').config(); + +describe('Security Middleware Suite Tests', () => { + let app; + + beforeAll(async () => { + await mongoose.connect('mongodb://localhost:27017/githubTracker'); + + app = require('../server'); + }); + + afterAll(async () => { + await mongoose.connection.db.dropDatabase(); + await mongoose.disconnect(); + }); + + // ========================= + // CORS TESTS + // ========================= + describe('CORS Configuration', () => { + it('should allow requests from CLIENT_URL', async () => { + const response = await request(app) + .options('/api/auth/login') + .set('Origin', 'http://localhost:5173') + .set('Access-Control-Request-Method', 'POST'); + + expect(response.headers['access-control-allow-origin']) + .toBe('http://localhost:5173'); + + expect(response.headers['access-control-allow-credentials']) + .toBe('true'); + }); + + it('should reject requests from unauthorized origin', async () => { + const response = await request(app) + .options('/api/auth/login') + .set('Origin', 'http://evil-site.com') + .set('Access-Control-Request-Method', 'POST'); + + // Some CORS middleware returns no header + expect( + response.headers['access-control-allow-origin'] + ).not.toBe('http://evil-site.com'); + }); + }); + + // ========================= + // RATE LIMITING TESTS + // ========================= + describe('Rate Limiting', () => { + const validPayload = { + email: 'ratetest@example.com', + password: 'WrongPassword123!', + }; + + it('should allow up to 10 requests within limit', async () => { + for (let i = 0; i < 10; i++) { + const response = await request(app) + .post('/api/auth/login') + .set('X-Forwarded-For', `192.168.1.${i}`) + .send(validPayload); + + expect(response.status).not.toBe(429); + } + }); + + it('should reject requests after exceeding rate limit', async () => { + const ip = '10.10.10.10'; + + // Exhaust limit + for (let i = 0; i < 10; i++) { + await request(app) + .post('/api/auth/login') + .set('X-Forwarded-For', ip) + .send(validPayload); + } + + // 11th request + const response = await request(app) + .post('/api/auth/login') + .set('X-Forwarded-For', ip) + .send(validPayload); + + expect(response.status).toBe(429); + + if (response.body.message) { + expect(response.body.message.toLowerCase()) + .toContain('too'); + } + }); + }); + + // ========================= + // SECURITY HEADERS TESTS + // ========================= + describe('Security Headers', () => { + it('should include Content-Security-Policy header', async () => { + const response = await request(app).get('/api/health'); + + expect( + response.headers['content-security-policy'] + ).toBeDefined(); + }); + + it('should include X-Frame-Options header', async () => { + const response = await request(app).get('/api/health'); + + expect(response.headers['x-frame-options']) + .toBe('DENY'); + }); + + it('should include X-Content-Type-Options header', async () => { + const response = await request(app).get('/api/health'); + + expect(response.headers['x-content-type-options']) + .toBe('nosniff'); + }); + + it('should include Referrer-Policy header', async () => { + const response = await request(app).get('/api/health'); + + expect(response.headers['referrer-policy']) + .toBe('strict-origin-when-cross-origin'); + }); + }); + + // ========================= + // SESSION COOKIE TESTS + // ========================= + describe('Session Cookie Security', () => { + beforeAll(async () => { + await request(app) + .post('/api/auth/signup') + .send({ + username: 'testuser123', + email: 'cookie-test@example.com', + password: 'Secure123!', + }); + }); + + it('should have httpOnly flag set', async () => { + const response = await request(app) + .post('/api/auth/login') + .send({ + email: 'cookie-test@example.com', + password: 'Secure123!', + }); + + const setCookieHeader = response.headers['set-cookie'][0]; + + expect(setCookieHeader).toContain('HttpOnly'); + }); + + it('should have SameSite flag set', async () => { + const response = await request(app) + .post('/api/auth/login') + .send({ + email: 'cookie-test@example.com', + password: 'Secure123!', + }); + + const setCookieHeader = response.headers['set-cookie'][0]; + + expect(setCookieHeader).toContain('SameSite'); + }); + }); + + // ========================= + // AUTH TESTS + // ========================= + describe('Authentication Middleware', () => { + it('should reject logout request without authentication', async () => { + const response = await request(app) + .get('/api/auth/logout'); + + expect(response.status).toBe(401); + + expect(response.body.success).toBe(false); + + expect(response.body.message) + .toContain('Authentication'); + }); + + it('should allow logout with valid session', async () => { + const agent = request.agent(app); + + // Login + const loginResponse = await agent + .post('/api/auth/login') + .send({ + email: 'cookie-test@example.com', + password: 'Secure123!', + }); + + expect(loginResponse.status).toBe(200); + + // Logout + const response = await agent + .get('/api/auth/logout'); + + expect(response.status).toBe(200); + + expect(response.body.success).toBe(true); + }); + }); + + // ========================= + // RESPONSE FORMAT TESTS + // ========================= + describe('Centralized Response Format', () => { + it('should return success field in all responses', async () => { + const response = await request(app) + .post('/api/auth/signup') + .send({ + username: 'formattest', + email: 'format@example.com', + password: 'Format123!', + }); + + expect(response.body.success).toBeDefined(); + + expect(typeof response.body.success) + .toBe('boolean'); + }); + + it('should return consistent error format', async () => { + const response = await request(app) + .post('/api/auth/login') + .send({ + email: 'invalid@example.com', + password: 'invalid', + }); + + expect(response.body.success).toBeDefined(); + + expect(response.body.success).toBe(false); + + expect(response.body.message).toBeDefined(); + }); + }); + + // ========================= + // ERROR HANDLING TESTS + // ========================= + describe('Centralized Error Handling', () => { + it('should not expose sensitive error details in production', async () => { + const originalEnv = process.env.NODE_ENV; + + process.env.NODE_ENV = 'production'; + + const response = await request(app) + .post('/api/auth/login') + .send({}); + + expect(response.body.stack).toBeUndefined(); + + process.env.NODE_ENV = originalEnv; + }); + + it('should include stack trace in development', async () => { + const originalEnv = process.env.NODE_ENV; + + process.env.NODE_ENV = 'development'; + + const response = await request(app) + .post('/api/auth/login') + .send({}); + + // Optional because some apps disable stack in tests + if (response.body.stack !== undefined) { + expect(response.body.stack).toBeDefined(); + } + + process.env.NODE_ENV = originalEnv; + }); + }); +}); \ No newline at end of file diff --git a/backend/spec/support/jasmine.mjs b/backend/spec/support/jasmine.mjs new file mode 100644 index 00000000..5e0a87de --- /dev/null +++ b/backend/spec/support/jasmine.mjs @@ -0,0 +1,15 @@ +export default { + spec_dir: "spec", + spec_files: [ + "**/*[sS]pec.?(m)js", + "**/*[sS]pec.cjs" + ], + helpers: [ + "helpers/**/*.?(m)js" + ], + env: { + stopSpecOnExpectationFailure: false, + random: true, + forbidDuplicateNames: true + } +} diff --git a/src/Routes/Router.tsx b/src/Routes/Router.tsx index 874ef7e7..650eeb00 100644 --- a/src/Routes/Router.tsx +++ b/src/Routes/Router.tsx @@ -7,24 +7,28 @@ import Signup from "../pages/Signup/Signup.tsx"; import Login from "../pages/Login/Login.tsx"; import ContributorProfile from "../pages/ContributorProfile/ContributorProfile.tsx"; import Home from "../pages/Home/Home.tsx"; -import Activity from "../pages/Activity.tsx"; +import Activity from "../pages/Activity.tsx"; import PrivacyPolicy from "../pages/Privacy/PrivacyPolicy.tsx"; // ✅ Updated import path to match your new folder structure +import ProtectedRoute from "../components/ProtectedRoute.tsx"; const Router = () => { return ( } /> - } /> + } /> } /> } /> } /> } /> } /> - } /> {/* Privacy Policy page route */} } /> + + {/* Protected Routes - Require Authentication */} + } /> + } /> ); }; diff --git a/src/api/axiosClient.ts b/src/api/axiosClient.ts new file mode 100644 index 00000000..e79bb0db --- /dev/null +++ b/src/api/axiosClient.ts @@ -0,0 +1,33 @@ +import axios from 'axios'; + +const axiosClient = axios.create({ + baseURL: import.meta.env.VITE_BACKEND_URL, + timeout: 10000, + headers: { + 'Content-Type': 'application/json', + }, + withCredentials: true, // Include cookies for authentication +}); + +axiosClient.interceptors.request.use( + (config) => { + console.log(`API Request: ${config.method?.toUpperCase()} ${config.url}`) + return config; + }, + (error) => Promise.reject(error) +); + +axiosClient.interceptors.response.use( + (response) => response, + (error) => { + if (error.response?.status === 401) { + console.warn('Unauthorized API response - redirecting to login'); + window.location.href = '/login'; + } + return Promise.reject(error); + } +) + +export { isAxiosError } from "axios"; + +export default axiosClient; \ No newline at end of file diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx new file mode 100644 index 00000000..8c358941 --- /dev/null +++ b/src/components/ProtectedRoute.tsx @@ -0,0 +1,36 @@ +import { Navigate, useLocation } from 'react-router-dom'; +import { useAuth } from '../context/AuthContext'; +import { Box, CircularProgress } from '@mui/material'; + +interface ProtectedRouteProps { + children: React.ReactNode; +} + +const ProtectedRoute = ({ children }: ProtectedRouteProps) => { + const { isAuthenticated, isLoading } = useAuth(); + const location = useLocation(); + + // Show loading spinner while checking authentication + if (isLoading) { + return ( + + + + ); + } + + // If not authenticated, redirect to login + if (!isAuthenticated) { + return ; + } + + // If authenticated, show component + return <>{children}; +}; + +export default ProtectedRoute; \ No newline at end of file diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx new file mode 100644 index 00000000..07881165 --- /dev/null +++ b/src/context/AuthContext.tsx @@ -0,0 +1,144 @@ +import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; +import axiosClient from '../api/axiosClient'; + +interface User { + id: string; + username: string; + email: string; +} + +interface AuthContextType { + user: User | null; + isAuthenticated: boolean; + isLoading: boolean; + error: string | null; + login: (email: string, password: string) => Promise; + signup: (username: string, email: string, password: string) => Promise; + logout: () => Promise; + checkAuth: () => Promise; +} + +export const AuthContext = createContext(null); + +export const useAuth = () => { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within AuthProvider'); + } + return context; +}; + +interface AuthProviderProps { + children: ReactNode; +} + +export const AuthProvider = ({ children }: AuthProviderProps) => { + const [user, setUser] = useState(null); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + // Check if user is already authenticated on mount + useEffect(() => { + checkAuth(); + }, []); + + const checkAuth = async () => { + try { + setIsLoading(true); + // Call a backend endpoint to verify session + const response = await axiosClient.get('/api/auth/me'); + if (response.data.success) { + setUser(response.data.user); + setIsAuthenticated(true); + } + } catch (err) { + // Not authenticated or session expired + setUser(null); + setIsAuthenticated(false); + } finally { + setIsLoading(false); + } + }; + + const login = async (email: string, password: string) => { + try { + setError(null); + setIsLoading(true); + const response = await axiosClient.post('/api/auth/login', { + email, + password, + }); + + if (response.data.success) { + setUser(response.data.user); + setIsAuthenticated(true); + } else { + setError(response.data.message || 'Login failed'); + } + } catch (err: any) { + const message = err.response?.data?.message || 'Login failed'; + setError(message); + throw err; + } finally { + setIsLoading(false); + } + }; + + const signup = async (username: string, email: string, password: string) => { + try { + setError(null); + setIsLoading(true); + const response = await axiosClient.post('/api/auth/signup', { + username, + email, + password, + }); + + if (response.status === 201 && response.data.success) { + setError(null); + } else { + setError(response.data.message || 'Signup failed'); + throw new Error(response.data.message || 'Signup failed'); + } + } catch (err: any) { + const message = err.response?.data?.message || 'Signup failed'; + setError(message); + throw err; + } finally { + setIsLoading(false); + } + }; + + const logout = async () => { + try { + setIsLoading(true); + await axiosClient.get('/api/auth/logout'); + setUser(null); + setIsAuthenticated(false); + setError(null); + } catch (err: any) { + const message = err.response?.data?.message || 'Logout failed'; + setError(message); + } finally { + setIsLoading(false); + } + }; + + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx index 4c5b79dd..884e64b3 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,12 +4,15 @@ import App from "./App.tsx"; import "./index.css"; import { BrowserRouter } from "react-router-dom"; import ThemeWrapper from "./context/ThemeContext.tsx"; +import { AuthProvider } from "./context/AuthContext.tsx"; createRoot(document.getElementById("root")!).render( - + + + diff --git a/src/pages/Contributors/Contributors.tsx b/src/pages/Contributors/Contributors.tsx index d4fee52c..dd85d71a 100644 --- a/src/pages/Contributors/Contributors.tsx +++ b/src/pages/Contributors/Contributors.tsx @@ -13,7 +13,7 @@ import { } from "@mui/material"; import { FaGithub } from "react-icons/fa"; import { Link } from "react-router-dom"; -import axios from "axios"; +import axiosClient, { isAxiosError } from "../../api/axiosClient"; import { GITHUB_REPO_CONTRIBUTORS_URL } from "../../utils/constants"; interface Contributor { @@ -33,12 +33,14 @@ const ContributorsPage = () => { useEffect(() => { const fetchContributors = async () => { try { - const response = await axios.get(GITHUB_REPO_CONTRIBUTORS_URL, { + const response = await axiosClient.get(GITHUB_REPO_CONTRIBUTORS_URL, { withCredentials: false, }); setContributors(response.data); - } catch { - setError("Failed to fetch contributors. Please try again later."); + } catch (err) { + if (isAxiosError(err)) { + setError("Failed to fetch contributors. Please try again later."); + } } finally { setLoading(false); } diff --git a/src/pages/Login/Login.tsx b/src/pages/Login/Login.tsx index 92b7073e..acfd50f7 100644 --- a/src/pages/Login/Login.tsx +++ b/src/pages/Login/Login.tsx @@ -1,10 +1,9 @@ import React, { useState, ChangeEvent, FormEvent, useContext } from "react"; -import axios from "axios"; import { useNavigate, Link } from "react-router-dom"; import { ThemeContext } from "../../context/ThemeContext"; import type { ThemeContextType } from "../../context/ThemeContext"; +import { useAuth } from "../../context/AuthContext"; -const backendUrl = import.meta.env.VITE_BACKEND_URL; interface LoginFormData { email: string; @@ -19,6 +18,7 @@ const Login: React.FC = () => { const navigate = useNavigate(); const themeContext = useContext(ThemeContext) as ThemeContextType; const { mode } = themeContext; + const { login } = useAuth(); const handleChange = (e: ChangeEvent) => { const { name, value } = e.target; @@ -30,18 +30,11 @@ const Login: React.FC = () => { setIsLoading(true); try { - const response = await axios.post(`${backendUrl}/api/auth/login`, formData); - setMessage(response.data.message); - - if (response.data.message === 'Login successful') { - navigate("/"); - } - } catch (error: unknown) { - if (axios.isAxiosError(error)) { + await login(formData.email, formData.password); + setMessage("Login successful!"); + setTimeout(() => navigate("/track"), 1500); + } catch (error: any) { setMessage(error.response?.data?.message || "Something went wrong"); - } else { - setMessage("Something went wrong"); - } } finally { setIsLoading(false); } @@ -135,7 +128,7 @@ const Login: React.FC = () => { {/* Message */} {message && (
diff --git a/src/pages/Signup/Signup.tsx b/src/pages/Signup/Signup.tsx index 2ac51dcc..2d9b0a46 100644 --- a/src/pages/Signup/Signup.tsx +++ b/src/pages/Signup/Signup.tsx @@ -1,12 +1,10 @@ import React, { useState, useContext } from "react"; -import axios from "axios"; import { useNavigate, Link } from "react-router-dom"; import { motion } from "framer-motion"; import { User, Mail, Lock, Eye, EyeOff } from "lucide-react"; import { ThemeContext } from "../../context/ThemeContext"; import type { ThemeContextType } from "../../context/ThemeContext"; - -const backendUrl = import.meta.env.VITE_BACKEND_URL; +import { useAuth } from "../../context/AuthContext"; interface SignUpFormData { username: string; @@ -31,6 +29,7 @@ const SignUp: React.FC = () => { const navigate = useNavigate(); const themeContext = useContext(ThemeContext) as ThemeContextType; const { mode } = themeContext; + const { signup } = useAuth(); const handleChange = (e: React.ChangeEvent) => { const { name, value } = e.target; @@ -83,17 +82,12 @@ const SignUp: React.FC = () => { } setIsLoading(true); try { - const response = await axios.post(`${backendUrl}/api/auth/signup`, - formData // Include cookies for session - ); - setMessage(response.data.message); // Show success message from backend - - // Navigate to login page after successful signup - if (response.data.message === 'User created successfully') { - navigate("/login"); - } + await signup(formData.username, formData.email, formData.password); + setMessage("Signup successful! You can now login."); + + setTimeout(() => navigate("/login"), 2000); } catch (error: any) { - setMessage(error.response?.data?.message || "Something went wrong. Please try again."); + setMessage(error.response?.data?.message || "Something went wrong"); } finally { setIsLoading(false); } @@ -214,7 +208,7 @@ const SignUp: React.FC = () => { {message && ( -
+
{message}
)}