Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Authentication System Implementation and Frontend Integration #32

Merged
merged 8 commits into from
Nov 29, 2024
3 changes: 3 additions & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@types/multer": "^1.4.12",
"@types/node": "^22.9.1",
"bcrypt": "^5.1.1",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.21.1",
"jsonwebtoken": "^9.0.2",
Expand All @@ -28,6 +29,8 @@
"typescript": "^5.6.3"
},
"devDependencies": {
"@types/bcrypt": "^5.0.2",
"@types/cors": "^2.8.17",
"@types/jsonwebtoken": "^9.0.7",
"@types/pg": "^8.11.10",
"ts-node-dev": "^2.0.0"
Expand Down
16 changes: 15 additions & 1 deletion api/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
import express from 'express';
import cors from 'cors';
import imageRoutes from "./routes/imageRoutes";
import authRoutes from "./routes/authRoutes";
import { authenticateToken } from './middlewares/authMiddleware';

const app = express();

// Enable CORS for frontend requests
app.use(cors({
origin: 'http://localhost:3000', // Your frontend URL
credentials: true
}));

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.use("/api/images", imageRoutes);
// Public routes
app.use("/api/auth", authRoutes);

// Protected routes - ensure authenticateToken is applied to all image routes
app.use("/api/images", authenticateToken); // Add authentication middleware first
app.use("/api/images", imageRoutes); // Then add the routes

export default app;
60 changes: 60 additions & 0 deletions api/src/controllers/authController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Request, Response } from 'express';
import { AuthService } from '../services/authService';

export class AuthController {
static async register(req: Request, res: Response): Promise<Response> {
try {
const { username, email, password } = req.body;

// Validate input
if (!username || !email || !password) {
return res.status(400).json({ message: 'All fields are required' });
}

// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return res.status(400).json({ message: 'Invalid email format' });
}

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

const user = await AuthService.registerUser(username, email, password);
return res.status(201).json({
message: 'User registered successfully',
user
});
} catch (error: unknown) {
if (error instanceof Error) {
return res.status(400).json({ message: error.message });
}
return res.status(500).json({ message: 'An unknown error occurred' });
}
}

static async login(req: Request, res: Response): Promise<Response> {
try {
const { email, password } = req.body;

if (!email || !password) {
return res.status(400).json({ message: 'Email and password are required' });
}

const { user, token } = await AuthService.loginUser(email, password);

return res.status(200).json({
message: 'Login successful',
user,
token
});
} catch (error: unknown) {
if (error instanceof Error) {
return res.status(401).json({ message: error.message });
}
return res.status(500).json({ message: 'An unknown error occurred' });
}
}
}
31 changes: 31 additions & 0 deletions api/src/middlewares/authMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Request, Response, NextFunction } from 'express';
import { AuthService } from '../services/authService';

declare global {
namespace Express {
interface Request {
user?: {
userId: number;
email: string;
};
}
}
}

export const authenticateToken = (req: Request, res: Response, next: NextFunction): void => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN

if (!token) {
res.status(401).json({ message: 'Authentication token is required' });
return;
}

try {
const user = AuthService.verifyToken(token);
req.user = user;
next();
} catch (error) {
res.status(403).json({ message: 'Invalid or expired token' });
}
};
14 changes: 14 additions & 0 deletions api/src/models/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export interface User {
id: number;
username: string;
email: string;
password: string;
createdAt: Date;
}

export interface UserResponse {
id: number;
username: string;
email: string;
createdAt: Date;
}
15 changes: 15 additions & 0 deletions api/src/routes/authRoutes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Router, Request, Response } from 'express';
import { AuthController } from '../controllers/authController';

const router = Router();

// Explicitly type the route handlers as RequestHandler
router.post('/register', async (req: Request, res: Response) => {
await AuthController.register(req, res);
});

router.post('/login', async (req: Request, res: Response) => {
await AuthController.login(req, res);
});

export default router;
78 changes: 78 additions & 0 deletions api/src/services/authService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import pool from '../config/db';
import { User, UserResponse } from '../models/user';

export class AuthService {
private static readonly JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
private static readonly SALT_ROUNDS = 10;

static async registerUser(username: string, email: string, password: string): Promise<UserResponse> {
// Check if user already exists
const existingUser = await pool.query(
'SELECT * FROM users WHERE email = $1 OR username = $2',
[email, username]
);

if (existingUser.rows.length > 0) {
throw new Error('User with this email or username already exists');
}

// Hash password
const hashedPassword = await bcrypt.hash(password, this.SALT_ROUNDS);

// Insert new user
const query = `
INSERT INTO users (username, email, password, created_at)
VALUES ($1, $2, $3, NOW())
RETURNING id, username, email, created_at as "createdAt"
`;

const result = await pool.query(query, [username, email, hashedPassword]);
return result.rows[0];
}

static async loginUser(email: string, password: string): Promise<{ user: UserResponse; token: string }> {
// Find user
const result = await pool.query(
'SELECT * FROM users WHERE email = $1',
[email]
);

const user = result.rows[0];
if (!user) {
throw new Error('User not found');
}

// Verify password
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
throw new Error('Invalid password');
}

// Generate JWT token
const token = jwt.sign(
{ userId: user.id, email: user.email },
this.JWT_SECRET,
{ expiresIn: '24h' }
);

// Return user data (excluding password) and token
const userResponse: UserResponse = {
id: user.id,
username: user.username,
email: user.email,
createdAt: user.created_at
};

return { user: userResponse, token };
}

static verifyToken(token: string): { userId: number; email: string } {
try {
return jwt.verify(token, this.JWT_SECRET) as { userId: number; email: string };
} catch (error) {
throw new Error('Invalid token');
}
}
}
26 changes: 23 additions & 3 deletions app/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 30 additions & 3 deletions app/src/Pages/Annotate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,66 @@ import React, { useState, useRef } from 'react';
import Button from '../Components/Button';
import { Upload, Loader } from 'lucide-react';

const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:5000/api';

const Annotate = () => {
const [selectedImage, setSelectedImage] = useState<string | null>(null);
const [uploadedImage, setUploadedImage] = useState<string | null>(null);
const [annotations, setAnnotations] = useState<Array<{ x: number; y: number; width: number; height: number; label: string }>>([]);
const [isUploading, setIsUploading] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
const [error, setError] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const imageRef = useRef<HTMLImageElement>(null);
const selectedFileRef = useRef<File | null>(null);

const handleImageSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
selectedFileRef.current = file;
const reader = new FileReader();
reader.onload = (e) => {
setSelectedImage(e.target?.result as string);
setUploadedImage(null);
setAnnotations([]);
setError(null);
};
reader.readAsDataURL(file);
}
};

const handleUpload = async () => {
if (!selectedImage) return;
if (!selectedImage || !selectedFileRef.current) return;

setIsUploading(true);
setError(null);

try {
// TODO: Implement actual backend upload
await new Promise(resolve => setTimeout(resolve, 1500)); // Simulate upload
const formData = new FormData();
formData.append('image', selectedFileRef.current);

const token = localStorage.getItem('token'); // Get the auth token if you have authentication

const response = await fetch(`${API_URL}/images/upload`, {
method: 'POST',
headers: {
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: formData,
});

if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.message || 'Upload failed');
}

const data = await response.json();
setUploadedImage(selectedImage);
setSelectedImage(null);
selectedFileRef.current = null;
} catch (error) {
console.error('Upload failed:', error);
setError(error instanceof Error ? error.message : 'Upload failed');
} finally {
setIsUploading(false);
}
Expand Down
Loading