diff --git a/README.md b/README.md index 58c15f8..d4118e3 100644 --- a/README.md +++ b/README.md @@ -1,122 +1,331 @@ -# Week 20: Task Management API with Authentication +# Task Management System -## RESTful API Development and Authentication Implementation +A full-featured task management system built with Node.js, Express.js, and PostgreSQL. Features user authentication, CRUD operations for tasks and subtasks, and a robust API. -## Introduction +## Features -- You have learned the basics of Node.js and Express.js, now let's test your knowledge of how to implement authentication with this Task Management project using JWT tokens and bcrypt password hashing. +- ๐Ÿ” **User Authentication**: JWT-based authentication with secure password hashing +- ๐Ÿ“ **Task Management**: Create, read, update, and delete tasks +- โœ… **Subtask Support**: Organize tasks with subtasks +- ๐ŸŽฏ **Priority & Status**: Set task priorities and track status +- ๐Ÿ“… **Due Date Tracking**: Set and manage task deadlines +- ๐Ÿ”’ **Secure API**: Protected routes with middleware authentication +- ๐Ÿ—„๏ธ **Database**: PostgreSQL with Prisma ORM -### Task 1: Project Setup +## Tech Stack -1. Fork and Clone this project repository in your terminal -2. CD into the project base directory `cd Week20_Task_Management_NodeJS_Auth` -3. Install dependencies: +- **Backend**: Node.js, Express.js +- **Database**: PostgreSQL +- **ORM**: Prisma +- **Authentication**: JWT, bcryptjs +- **Validation**: Built-in input validation + +## Prerequisites + +- Node.js (v16 or higher) +- PostgreSQL database +- npm or yarn package manager + +## Installation + +1. **Clone the repository** + ```bash + git clone + cd Week20_Task_Management_NodeJS_Auth + ``` + +2. **Install dependencies** ```bash npm install ``` -4. Create a `.env` file in the root directory with your database URL and JWT secret -5. Complete the authentication middleware and routes (see tasks below) -6. Generate Prisma client and push schema to database: + +3. **Set up environment variables** + Create a `.env` file in the root directory: + ```env + DATABASE_URL="postgresql://username:password@localhost:5432/task_management_db" + JWT_SECRET="your-super-secret-jwt-key-change-this-in-production" + PORT=3000 + NODE_ENV=development + ``` + +4. **Set up the database** ```bash + # Generate Prisma client npm run db:generate + + # Push schema to database npm run db:push ``` -7. Start the server: + +5. **Start the server** ```bash + # Development mode with auto-reload npm run dev + + # Production mode + npm start ``` -8. The server will run on `http://localhost:3000` -### Task 2: MVP Requirements (Minimum Viable Product) +The server will start on `http://localhost:3000` + +## API Endpoints + +### Authentication + +#### Register User +```http +POST /api/auth/register +Content-Type: application/json + +{ + "name": "John Doe", + "email": "john@example.com", + "password": "securepassword123" +} +``` + +#### Login User +```http +POST /api/auth/login +Content-Type: application/json + +{ + "email": "john@example.com", + "password": "securepassword123" +} +``` + +#### Get User Profile +```http +GET /api/auth/me +Authorization: Bearer +``` + +### Tasks + +#### Get All Tasks +```http +GET /api/tasks +Authorization: Bearer +``` -You need to complete the following: +#### Get Task by ID +```http +GET /api/tasks/:id +Authorization: Bearer +``` -#### 1. Complete Authentication Middleware - `middleware/auth.js` +#### Create Task +```http +POST /api/tasks +Authorization: Bearer +Content-Type: application/json + +{ + "title": "Complete Project", + "description": "Finish the task management system", + "status": "pending", + "priority": "high", + "dueDate": "2024-01-31T23:59:59.000Z", + "assignedTo": "team-member" +} +``` -**Location:** `middleware/auth.js` +#### Update Task +```http +PUT /api/tasks/:id +Authorization: Bearer +Content-Type: application/json -The middleware file needs to be implemented to verify JWT tokens: +{ + "status": "in_progress", + "priority": "urgent" +} +``` -**Requirements:** +#### Delete Task +```http +DELETE /api/tasks/:id +Authorization: Bearer +``` -- Extract JWT token from `Authorization: Bearer ` header -- Verify the token using the JWT_SECRET -- Find the user in the database using the decoded userId -- Set `req.user` with the user data (excluding password) -- Handle token validation errors properly +### Subtasks -#### 2. Complete User Registration - `routes/auth.js` +#### Get Subtasks for Task +```http +GET /api/tasks/:taskId/subtasks +Authorization: Bearer +``` -**Location:** `routes/auth.js` - POST /register endpoint +#### Create Subtask +```http +POST /api/tasks/:taskId/subtasks +Authorization: Bearer +Content-Type: application/json -The registration endpoint needs to be implemented: +{ + "title": "Research Phase", + "description": "Gather requirements and research solutions" +} +``` -**Requirements:** +#### Update Subtask +```http +PUT /api/subtasks/:id +Authorization: Bearer +Content-Type: application/json -- Validate that email, password, and name are provided -- Check if a user with the email already exists -- Hash the password using bcrypt (12 salt rounds) -- Create the user in the database -- Generate a JWT token with userId and email -- Return user data (excluding password) and token -- Handle duplicate email errors +{ + "completed": true +} +``` -#### 3. Complete User Login - `routes/auth.js` +#### Delete Subtask +```http +DELETE /api/subtasks/:id +Authorization: Bearer +``` -**Location:** `routes/auth.js` - POST /login endpoint +## Data Models -The login endpoint needs to be implemented: +### Task Status +- `pending` - Task is waiting to be started +- `in_progress` - Task is currently being worked on +- `completed` - Task has been finished +- `cancelled` - Task has been cancelled -**Requirements:** +### Priority Levels +- `low` - Low priority task +- `medium` - Normal priority task +- `high` - High priority task +- `urgent` - Critical priority task -- Validate that email and password are provided -- Find the user by email in the database -- Check if the user exists -- Compare the provided password with the hashed password using bcrypt -- Generate a JWT token with userId and email -- Return user data (excluding password) and token -- Handle invalid credentials errors +## Usage Examples -#### 5. Configure Environment Variables - `.env` +### Using cURL -**Location:** `.env` +1. **Register a new user** + ```bash + curl -X POST http://localhost:3000/api/auth/register \ + -H "Content-Type: application/json" \ + -d '{"name":"John Doe","email":"john@example.com","password":"password123"}' + ``` -Create a `.env` file with: +2. **Login and get token** + ```bash + curl -X POST http://localhost:3000/api/auth/login \ + -H "Content-Type: application/json" \ + -d '{"email":"john@example.com","password":"password123"}' + ``` + +3. **Create a task (use token from login)** + ```bash + curl -X POST http://localhost:3000/api/tasks \ + -H "Authorization: Bearer YOUR_JWT_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"title":"Learn Node.js","description":"Study Express and middleware","status":"pending","priority":"high"}' + ``` -```env -DATABASE_URL="your-supabase-database-connection-url" -JWT_SECRET="your-super-secret-jwt-key" -PORT=3000 +### Using JavaScript/Fetch + +```javascript +// Login +const loginResponse = await fetch('http://localhost:3000/api/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: 'john@example.com', + password: 'password123' + }) +}); + +const { data: { token } } = await loginResponse.json(); + +// Create task +const taskResponse = await fetch('http://localhost:3000/api/tasks', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify({ + title: 'New Task', + description: 'Task description', + status: 'pending', + priority: 'medium' + }) +}); ``` -### Task 3: Testing Your Implementation +## Error Handling + +The API returns consistent error responses: -You can use Postman or curl to test your endpoints, make sure everything is working correctly. +```json +{ + "success": false, + "message": "Error description", + "error": "Detailed error information (in development)" +} +``` -## Task 4: Stretch Goals +Common HTTP status codes: +- `200` - Success +- `201` - Created +- `400` - Bad Request +- `401` - Unauthorized +- `404` - Not Found +- `500` - Internal Server Error -### Add Password Reset Functionality +## Development -**Objective:** Implement password reset with email verification. +### Available Scripts + +- `npm start` - Start production server +- `npm run dev` - Start development server with nodemon +- `npm run db:generate` - Generate Prisma client +- `npm run db:push` - Push schema changes to database +- `npm run db:migrate` - Run database migrations +- `npm run db:studio` - Open Prisma Studio + +### Project Structure + +``` +โ”œโ”€โ”€ lib/ +โ”‚ โ””โ”€โ”€ prisma.js # Prisma client configuration +โ”œโ”€โ”€ middleware/ +โ”‚ โ””โ”€โ”€ auth.js # JWT authentication middleware +โ”œโ”€โ”€ prisma/ +โ”‚ โ””โ”€โ”€ schema.prisma # Database schema +โ”œโ”€โ”€ routes/ +โ”‚ โ”œโ”€โ”€ auth.js # Authentication routes +โ”‚ โ””โ”€โ”€ tasks.js # Task management routes +โ”œโ”€โ”€ services/ +โ”‚ โ””โ”€โ”€ taskServices.js # Business logic for tasks +โ”œโ”€โ”€ server.js # Main application file +โ””โ”€โ”€ package.json # Dependencies and scripts +``` -**Requirements:** +## Security Features -1. **Create password reset endpoint** that generates a reset token -2. **Send reset email** with reset link (simulate with console.log) -3. **Create reset password endpoint** that validates reset token -4. **Update user password** in database +- **Password Hashing**: Passwords are hashed using bcrypt with 12 salt rounds +- **JWT Tokens**: Secure token-based authentication +- **Input Validation**: Server-side validation for all inputs +- **SQL Injection Protection**: Prisma ORM prevents SQL injection +- **CORS Enabled**: Cross-origin requests are handled securely -### Add Email Verification +## Contributing -**Objective:** Implement email verification for new registrations. +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Add tests if applicable +5. Submit a pull request -**Requirements:** +## License -1. **Add email verification field** to User model -2. **Generate verification token** during registration -3. **Send verification email** (simulate with console.log) -4. **Create verification endpoint** to confirm email -5. **Update user verification status** +This project is licensed under the MIT License. +## Support -Good luck with your implementation! ๐Ÿš€ +For questions or support, please open an issue in the repository. diff --git a/middleware/auth.js b/middleware/auth.js index 7deb650..cbe6b9a 100644 --- a/middleware/auth.js +++ b/middleware/auth.js @@ -5,16 +5,45 @@ const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key"; export const authenticateToken = async (req, res, next) => { try { - // TODO: Implement the authentication middleware - // 1. Get the token from the request header - // 2. Verify the token - // 3. Get the user from the database - // 4. If the user doesn't exist, throw an error - // 5. Attach the user to the request object - // 6. Call the next middleware + // Get the token from the request header + const authHeader = req.headers['authorization']; + const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN + if (!token) { + return res.status(401).json({ + success: false, + message: "Access token required", + }); + } + + // Verify the token + const decoded = jwt.verify(token, JWT_SECRET); + // Get the user from the database + const user = await prisma.user.findUnique({ + where: { id: decoded.userId }, + select: { + id: true, + email: true, + name: true, + createdAt: true, + updatedAt: true + } + }); + + // If the user doesn't exist, throw an error + if (!user) { + return res.status(401).json({ + success: false, + message: "User not found", + }); + } + + // Attach the user to the request object + req.user = user; + // Call the next middleware + next(); } catch (error) { if (error.name === "JsonWebTokenError") { return res.status(401).json({ diff --git a/routes/auth.js b/routes/auth.js index 7a78cfc..6380603 100644 --- a/routes/auth.js +++ b/routes/auth.js @@ -5,21 +5,69 @@ import prisma from "../lib/prisma.js"; import { authenticateToken } from "../middleware/auth.js"; const router = express.Router(); -const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key"; +const JWT_SECRET = process.env.JWT_SECRET || "hduyeuhdudueeudskjshd"; // POST /api/auth/register - Register a new user router.post("/register", async (req, res) => { try { - // TODO: Implement the registration logic - // 1. Validate the input - // 2. Check if the user already exists - // 3. Hash the password - // 4. Create the user - // 5. Generate a JWT token - // 6. Return the user data and token + const { email, password, name } = req.body; + // Validate the input + if (!email || !password || !name) { + return res.status(400).json({ + success: false, + message: "Email, password, and name are required", + }); + } + // Check if the user already exists + const existingUser = await prisma.user.findUnique({ + where: { email }, + }); + + if (existingUser) { + return res.status(400).json({ + success: false, + message: "User with this email already exists", + }); + } + + // Hash the password + const saltRounds = 12; + const hashedPassword = await bcrypt.hash(password, saltRounds); + + // Create the user + const newUser = await prisma.user.create({ + data: { + email, + password: hashedPassword, + name, + }, + select: { + id: true, + email: true, + name: true, + createdAt: true, + updatedAt: true, + }, + }); + // Generate a JWT token + const token = jwt.sign( + { userId: newUser.id, email: newUser.email }, + JWT_SECRET, + { expiresIn: "24h" } + ); + + // Return the user data and token + res.status(201).json({ + success: true, + message: "User registered successfully", + data: { + user: newUser, + token, + }, + }); } catch (error) { console.error("Registration error:", error); res.status(500).json({ @@ -33,14 +81,55 @@ router.post("/register", async (req, res) => { // POST /api/auth/login - Login user router.post("/login", async (req, res) => { try { - // TODO: Implement the login logic - // 1. Validate the input - // 2. Check if the user exists - // 3. Compare the password - // 4. Generate a JWT token - // 5. Return the user data and token - - + const { email, password } = req.body; + + // Validate the input + if (!email || !password) { + return res.status(400).json({ + success: false, + message: "Email and password are required", + }); + } + + // Check if the user exists + const user = await prisma.user.findUnique({ + where: { email }, + }); + + if (!user) { + return res.status(401).json({ + success: false, + message: "Invalid email or password", + }); + } + + // Compare the password + const isPasswordValid = await bcrypt.compare(password, user.password); + + if (!isPasswordValid) { + return res.status(401).json({ + success: false, + message: "Invalid email or password", + }); + } + + // Generate a JWT token + const token = jwt.sign( + { userId: user.id, email: user.email }, + JWT_SECRET, + { expiresIn: "24h" } + ); + + // Return the user data and token + const { password: _, ...userWithoutPassword } = user; + res.json({ + success: true, + message: "Login successful", + data: { + user: userWithoutPassword, + token, + }, + }); } catch (error) { console.error("Login error:", error); res.status(500).json({ diff --git a/test-api.js b/test-api.js new file mode 100644 index 0000000..0a86ba0 --- /dev/null +++ b/test-api.js @@ -0,0 +1,221 @@ +import fetch from 'node-fetch'; + +const BASE_URL = 'http://localhost:3000/api'; + +// Test data +const testUser = { + name: 'Test User', + email: 'test@example.com', + password: 'testpassword123' +}; + +const testTask = { + title: 'Test Task', + description: 'This is a test task', + status: 'pending', + priority: 'medium' +}; + +let authToken = ''; +let createdTaskId = ''; + +// Helper function to make authenticated requests +const authenticatedRequest = async (url, options = {}) => { + if (authToken) { + options.headers = { + ...options.headers, + 'Authorization': `Bearer ${authToken}` + }; + } + return fetch(url, options); +}; + +// Test 1: User Registration +console.log('๐Ÿงช Testing User Registration...'); +try { + const registerResponse = await fetch(`${BASE_URL}/auth/register`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(testUser) + }); + + const registerData = await registerResponse.json(); + + if (registerResponse.ok) { + console.log('โœ… Registration successful:', registerData.message); + authToken = registerData.data.token; + } else { + console.log('โŒ Registration failed:', registerData.message); + } +} catch (error) { + console.log('โŒ Registration error:', error.message); +} + +// Test 2: User Login +console.log('\n๐Ÿงช Testing User Login...'); +try { + const loginResponse = await fetch(`${BASE_URL}/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: testUser.email, + password: testUser.password + }) + }); + + const loginData = await loginResponse.json(); + + if (loginResponse.ok) { + console.log('โœ… Login successful:', loginData.message); + authToken = loginData.data.token; + } else { + console.log('โŒ Login failed:', loginData.message); + } +} catch (error) { + console.log('โŒ Login error:', error.message); +} + +// Test 3: Get User Profile +console.log('\n๐Ÿงช Testing Get User Profile...'); +try { + const profileResponse = await authenticatedRequest(`${BASE_URL}/auth/me`); + const profileData = await profileResponse.json(); + + if (profileResponse.ok) { + console.log('โœ… Profile retrieved successfully'); + console.log(' User:', profileData.data.name); + } else { + console.log('โŒ Profile retrieval failed:', profileData.message); + } +} catch (error) { + console.log('โŒ Profile error:', error.message); +} + +// Test 4: Create Task +console.log('\n๐Ÿงช Testing Task Creation...'); +try { + const createTaskResponse = await authenticatedRequest(`${BASE_URL}/tasks`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(testTask) + }); + + const createTaskData = await createTaskResponse.json(); + + if (createTaskResponse.ok) { + console.log('โœ… Task created successfully'); + createdTaskId = createTaskData.data.id; + console.log(' Task ID:', createdTaskId); + } else { + console.log('โŒ Task creation failed:', createTaskData.message); + } +} catch (error) { + console.log('โŒ Task creation error:', error.message); +} + +// Test 5: Get All Tasks +console.log('\n๐Ÿงช Testing Get All Tasks...'); +try { + const tasksResponse = await authenticatedRequest(`${BASE_URL}/tasks`); + const tasksData = await tasksResponse.json(); + + if (tasksResponse.ok) { + console.log('โœ… Tasks retrieved successfully'); + console.log(' Total tasks:', tasksData.count); + } else { + console.log('โŒ Tasks retrieval failed:', tasksData.message); + } +} catch (error) { + console.log('โŒ Tasks error:', error.message); +} + +// Test 6: Get Task by ID +if (createdTaskId) { + console.log('\n๐Ÿงช Testing Get Task by ID...'); + try { + const taskResponse = await authenticatedRequest(`${BASE_URL}/tasks/${createdTaskId}`); + const taskData = await taskResponse.json(); + + if (taskResponse.ok) { + console.log('โœ… Task retrieved successfully'); + console.log(' Task title:', taskData.data.title); + } else { + console.log('โŒ Task retrieval failed:', taskData.message); + } + } catch (error) { + console.log('โŒ Task retrieval error:', error.message); + } +} + +// Test 7: Update Task +if (createdTaskId) { + console.log('\n๐Ÿงช Testing Task Update...'); + try { + const updateTaskResponse = await authenticatedRequest(`${BASE_URL}/tasks/${createdTaskId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status: 'in_progress' }) + }); + + const updateTaskData = await updateTaskResponse.json(); + + if (updateTaskResponse.ok) { + console.log('โœ… Task updated successfully'); + console.log(' New status:', updateTaskData.data.status); + } else { + console.log('โŒ Task update failed:', updateTaskData.message); + } + } catch (error) { + console.log('โŒ Task update error:', error.message); + } +} + +// Test 8: Create Subtask +if (createdTaskId) { + console.log('\n๐Ÿงช Testing Subtask Creation...'); + try { + const createSubtaskResponse = await authenticatedRequest(`${BASE_URL}/tasks/${createdTaskId}/subtasks`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + title: 'Test Subtask', + description: 'This is a test subtask' + }) + }); + + const createSubtaskData = await createSubtaskResponse.json(); + + if (createSubtaskResponse.ok) { + console.log('โœ… Subtask created successfully'); + console.log(' Subtask title:', createSubtaskData.data.title); + } else { + console.log('โŒ Subtask creation failed:', createSubtaskData.message); + } + } catch (error) { + console.log('โŒ Subtask creation error:', error.message); + } +} + +// Test 9: Delete Task +if (createdTaskId) { + console.log('\n๐Ÿงช Testing Task Deletion...'); + try { + const deleteTaskResponse = await authenticatedRequest(`${BASE_URL}/tasks/${createdTaskId}`, { + method: 'DELETE' + }); + + const deleteTaskData = await deleteTaskResponse.json(); + + if (deleteTaskResponse.ok) { + console.log('โœ… Task deleted successfully'); + } else { + console.log('โŒ Task deletion failed:', deleteTaskData.message); + } + } catch (error) { + console.log('โŒ Task deletion error:', error.message); + } +} + +console.log('\n๐ŸŽ‰ API testing completed!'); +console.log('\n๐Ÿ“ Note: Make sure your server is running on http://localhost:3000'); +console.log('๐Ÿ“ Note: Make sure you have set up your .env file with DATABASE_URL and JWT_SECRET');