diff --git a/server/src/controllers/authController.ts b/server/src/controllers/authController.ts new file mode 100644 index 0000000..1c37775 --- /dev/null +++ b/server/src/controllers/authController.ts @@ -0,0 +1,57 @@ +import { Request, Response } from "express"; +import { User } from "../models/User"; +import { generateToken } from "../utils/generateToken"; + + +export const register = async (req: Request, res: Response): Promise => { + try { + const {name, email, password} = req.body + if (!name || !email || !password) { + res.status(400).json({message: 'All fields are required'}); + return; + } + + const existingUser = await User.findOne({email}); + if (existingUser) { + res.status(409).json({message:'Email already registered'}); + return; + } + + const user = await User.create({ name, email, password }); + + const token = generateToken(user._id, user.email); + res.status(201).json({ + token, + user, + }) + } catch (error) { + console.error('Register error:', error) + res.status(500).json({ message: 'Server error during registration' }) + } +}; + +export const login = async (req: Request, res: Response ): Promise => { + try { + const {email, password} = req.body; + if (!email || !password) { + res.status(400).json({ + message: 'Email and password are required' + }); + return; + } + + const user = await User.findOne({ email }); + if (!user) { + res.status(401).json({message:'Invalid Email or password'}) + return; + } + + const token = generateToken(user._id, user.email) + + res.json({token, user}) + } catch(error) { + console.error('Login error:', error) + res.status(500).json({ message: 'Server error during login' }); + + } +} \ No newline at end of file diff --git a/server/src/middleware/authMiddleware.ts b/server/src/middleware/authMiddleware.ts new file mode 100644 index 0000000..b7328ae --- /dev/null +++ b/server/src/middleware/authMiddleware.ts @@ -0,0 +1,36 @@ +import { Request, Response, NextFunction } from 'express'; +import jwt from 'jsonwebtoken'; +import { User } from '../models/User'; +import { ENV } from '../config/env'; + +interface JwtPayload { + id: string; + email: string; +} + +export const protect = async ( + req: Request, + res: Response, + next: NextFunction +): Promise => { + try { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + res.status(401).json({ message: 'No token Provided' }); + return; + } + const token = authHeader.split(' ')[1]; + const decode =jwt.verify(token, ENV.JWT_SECRET) as JwtPayload; + const user = await User.findById(decode.id) + if (!user) { + res.status(401).json({ message: 'User no longer exists' }) + return; + } + + req.user = user; + next() + + } catch(error) { + res.status(401).json({ message: 'Invalid or expired Token' }) + } +} \ No newline at end of file diff --git a/server/src/models/User.ts b/server/src/models/User.ts new file mode 100644 index 0000000..4492c0f --- /dev/null +++ b/server/src/models/User.ts @@ -0,0 +1,61 @@ +import { Document, model, Schema, Types } from "mongoose"; +import bcrypt from 'bcryptjs'; + + +export interface IUser { + name: string; + email: string; + password: string; + createdAt?: Date; +} + +export interface IUserDocument extends IUser, Document { + _id: Types.ObjectId + comparePassword(candidatePassword: string): Promise; +} + +const userSchema = new Schema({ + name: { + type: String, + required: [true, 'Name is required'], + trim: true, + minLength: [2, 'Name must be at least 2 characters'], + }, + email: { + type: String, + required: [true, 'Email is required'], + unique: true, + lowercase: true, // always store as lowercase + trim: true, + match: [/^\S+@\S+\.\S+$/, 'Please enter a valid email'], + }, + password: { + type: String, + required: [true, 'Password is required'], + minlength: [6, 'Password must be at least 6 characters'], + }, + }, + { timestamps: true } // auto-adds createdAt and updatedAt +); + +userSchema.pre('save', async function () { + if (!this.isModified('password')) return ; + + const salt = await bcrypt.genSalt(12); + this.password = await bcrypt.hash(this.password, salt); +}); + +userSchema.methods.comparePassword = async function ( + candidatePassword: string +) : Promise { + return bcrypt.compare(candidatePassword, this.password) +} + +userSchema.set('toJSON', { + transform: (_doc, ret) => { + delete ret.password; + return ret + } +}) + +export const User = model('User', userSchema); diff --git a/server/src/routes/authRoutes.ts b/server/src/routes/authRoutes.ts new file mode 100644 index 0000000..700c082 --- /dev/null +++ b/server/src/routes/authRoutes.ts @@ -0,0 +1,10 @@ +import { Router } from 'express'; +import { register, login } from '../controllers/authController'; + +const router = Router(); + +router.post('/register', register); +router.post ('/login', login) + +export default router; + diff --git a/server/src/server.ts b/server/src/server.ts index a52b267..69fdc73 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -2,6 +2,7 @@ import express, {Application, Request, Response} from 'express'; import cors from 'cors'; import {ENV} from './config/env'; import {connectDB, readyState} from './config/db'; +import authRoutes from './routes/authRoutes'; const app: Application = express() @@ -27,6 +28,10 @@ app.get('/health/db', (req: Request, res: Response)=> { }); +//api routes +app.use('/api/auth', authRoutes); + + //start Server const start = async (): Promise => { diff --git a/server/src/types/express.d.ts b/server/src/types/express.d.ts new file mode 100644 index 0000000..1b5d53f --- /dev/null +++ b/server/src/types/express.d.ts @@ -0,0 +1,9 @@ +import { IUserDocument } from '../model/User'; + +declare global { + namespace Express { + interface Request { + user?: IUserDocument + } + } +} \ No newline at end of file diff --git a/server/src/utils/generateToken.ts b/server/src/utils/generateToken.ts new file mode 100644 index 0000000..5a965a8 --- /dev/null +++ b/server/src/utils/generateToken.ts @@ -0,0 +1,21 @@ +import jwt from 'jsonwebtoken'; +import { Types } from 'mongoose'; +import { ENV } from '../config/env'; + +interface TokenPayload { + id: string; + email: string; +} + +export const generateToken = ( + userId: Types.ObjectId, + email: string, +): string => { + const payload: TokenPayload = { + id: userId.toString(), + email, + }; + return jwt.sign(payload, ENV.JWT_SECRET, { + expiresIn: '7d' + }); +}; \ No newline at end of file