Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,19 @@
"dependencies": {
"@prisma/client": "^6.16.1",
"@vercel/node": "^3.0.31",
"bcryptjs": "^3.0.2",
"cors": "^2.8.5",
"dotenv": "^17.2.2",
"express": "^5.1.0",
"helmet": "^8.1.0",
"jsonwebtoken": "^9.0.2",
"morgan": "^1.10.1"
},
"devDependencies": {
"@types/bcryptjs": "^3.0.0",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.3",
"@types/jsonwebtoken": "^9.0.10",
"@types/morgan": "^1.9.10",
"@types/node": "^24.5.0",
"nodemon": "^3.1.10",
Expand Down
4 changes: 4 additions & 0 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import cors from "cors";
import express, { type Express } from "express";
import helmet from "helmet";
import morgan from "morgan";
import authRoutes from "@/routes/auth";

const app: Express = express();

Expand Down Expand Up @@ -33,6 +34,9 @@ app.get("/health", (_req, res) => {
});
});

// Auth routes
app.use("/auth", authRoutes);

// API routes
app.use("/api", (_req, res) => {
res.json({ message: "API is working" });
Expand Down
148 changes: 148 additions & 0 deletions backend/src/controllers/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { Request, Response } from 'express';
import { PrismaClient } from '@prisma/client';
import { generateToken, hashPassword, comparePassword } from '@/utils/auth';
import { RegisterRequest, LoginRequest, AuthResponse } from '@/types/auth';

const prisma = new PrismaClient();

/**
* Register new user
* POST /auth/register
*/
export const register = async (req: Request, res: Response): Promise<void> => {
try {
const { email, password }: RegisterRequest = req.body;

// Validate input
if (!email || !password) {
res.status(400).json({
error: 'Validation error',
message: 'Email and password are required'
});
return;
}

if (password.length < 6) {
res.status(400).json({
error: 'Validation error',
message: 'Password must be at least 6 characters long'
});
return;
}

// Check if user already exists
const existingUser = await prisma.user.findUnique({
where: { email }
});

if (existingUser) {
res.status(409).json({
error: 'User already exists',
message: 'A user with this email already exists'
});
return;
}

// Hash password and create user
const hashedPassword = await hashPassword(password);

const user = await prisma.user.create({
data: {
email,
password: hashedPassword,
name: null
}
});

// Generate token
const token = generateToken({
userId: user.id,
email: user.email
});

const response: AuthResponse = {
user: {
id: user.id,
email: user.email,
name: user.name,
createdAt: user.createdAt
},
token
};

res.status(201).json(response);
} catch (error) {
console.error('Registration error:', error);
res.status(500).json({
error: 'Internal server error',
message: 'Failed to create user'
});
}
};

/**
* Login user
* POST /auth/login
*/
export const login = async (req: Request, res: Response): Promise<void> => {
try {
const { email, password }: LoginRequest = req.body;

// Validate input
if (!email || !password) {
res.status(400).json({
error: 'Validation error',
message: 'Email and password are required'
});
return;
}

// Find user
const user = await prisma.user.findUnique({
where: { email }
});

if (!user) {
res.status(401).json({
error: 'Authentication failed',
message: 'Invalid email or password'
});
return;
}

// Verify password
const isValidPassword = await comparePassword(password, user.password);

if (!isValidPassword) {
res.status(401).json({
error: 'Authentication failed',
message: 'Invalid email or password'
});
return;
}

// Generate token
const token = generateToken({
userId: user.id,
email: user.email
});

const response: AuthResponse = {
user: {
id: user.id,
email: user.email,
name: user.name,
createdAt: user.createdAt
},
token
};

res.status(200).json(response);
} catch (error) {
console.error('Login error:', error);
res.status(500).json({
error: 'Internal server error',
message: 'Failed to authenticate user'
});
}
};
65 changes: 65 additions & 0 deletions backend/src/middleware/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Request, Response, NextFunction } from 'express';
import { verifyToken, extractTokenFromHeader, JWTPayload } from '@/utils/auth';

// Extend Request interface to include user
declare global {
namespace Express {
interface Request {
user?: JWTPayload;
}
}
}

/**
* Middleware to verify JWT token and authenticate user
*/
export const authenticateToken = (
req: Request,
res: Response,
next: NextFunction
): void => {
try {
const token = extractTokenFromHeader(req.headers.authorization);

if (!token) {
res.status(401).json({
error: 'Authentication required',
message: 'No token provided'
});
return;
}

const decoded = verifyToken(token);
req.user = decoded;

next();
} catch (error) {
res.status(401).json({
error: 'Authentication failed',
message: error instanceof Error ? error.message : 'Invalid token'
});
}
};

/**
* Optional auth middleware - doesn't fail if no token
*/
export const optionalAuth = (
req: Request,
res: Response,
next: NextFunction
): void => {
try {
const token = extractTokenFromHeader(req.headers.authorization);

if (token) {
const decoded = verifyToken(token);
req.user = decoded;
}

next();
} catch (error) {
// Ignore auth errors in optional auth
next();
}
};
12 changes: 12 additions & 0 deletions backend/src/routes/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { login, register } from "@/controllers/auth";
import { Router } from "express";

const router: Router = Router();

// POST /auth/register
router.post("/register", register);

// POST /auth/login
router.post("/login", login);

export default router;
25 changes: 25 additions & 0 deletions backend/src/types/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export interface RegisterRequest {
email: string;
password: string;
}

export interface LoginRequest {
email: string;
password: string;
}

export interface AuthResponse {
user: {
id: number;
email: string;
name: string | null;
createdAt: Date;
};
token: string;
}

export interface ApiError {
error: string;
message: string;
details?: string[];
}
58 changes: 58 additions & 0 deletions backend/src/utils/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';
import { config } from '@/config/config';

export interface JWTPayload {
userId: number;
email: string;
}

/**
* Generate JWT token for user
*/
export const generateToken = (payload: JWTPayload): string => {
return jwt.sign(payload, config.jwtSecret, {
expiresIn: '24h',
});
};

/**
* Verify and decode JWT token
*/
export const verifyToken = (token: string): JWTPayload => {
try {
const decoded = jwt.verify(token, config.jwtSecret) as JWTPayload;
return decoded;
} catch (error) {
throw new Error('Invalid or expired token');
}
};

/**
* Hash password using bcrypt
*/
export const hashPassword = async (password: string): Promise<string> => {
const saltRounds = 12;
return await bcrypt.hash(password, saltRounds);
};

/**
* Compare password with hash
*/
export const comparePassword = async (
password: string,
hash: string
): Promise<boolean> => {
return await bcrypt.compare(password, hash);
};

/**
* Extract token from Authorization header
*/
export const extractTokenFromHeader = (authHeader?: string): string | null => {
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return null;
}

return authHeader.substring(7); // Remove 'Bearer ' prefix
};