Skip to content

Commit

Permalink
Merge pull request #32 from AhmedFatthy1040/issue-19-integration
Browse files Browse the repository at this point in the history
Authentication System Implementation and Frontend Integration
  • Loading branch information
AhmedFatthy1040 authored Nov 29, 2024
2 parents f672aaf + 0a935d8 commit 1b2a9ec
Show file tree
Hide file tree
Showing 14 changed files with 591 additions and 83 deletions.
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

0 comments on commit 1b2a9ec

Please sign in to comment.