Skip to content

TODO REST API - PHP

A secure REST API for managing TODO lists and tasks with JWT authentication.

Table of Contents


Quick Start

Prerequisites

# Install PHP 8.1+ and Composer
brew install php composer

Installation

cd ./docs/homework4/php-version
composer install

Configuration

  1. Generate a JWT secret:
openssl rand -base64 32
  1. Update .env:
JWT_SECRET=your-generated-secret-here
JWT_EXPIRY=3600
DEBUG_MODE=true

Run Server

Option 1: Using Composer (recommended)

composer serve

Option 2: Using PHP built-in server

php -S localhost:8000 -t public

API available at: http://localhost:8000

Verify Server

# Test with a simple GET request
curl http://localhost:8000/api/v1/lists
# Should return: []

Testing

  1. Download Bruno from https://www.usebruno.com/
  2. Open Bruno and import collection from bruno/ folder
  3. Select "local" environment
  4. Run requests in order:
  5. Auth > Signup (copy the token)
  6. Auth > Get Profile (paste token)
  7. Lists > Create List (copy list ID)
  8. Tasks > Create Task (use list ID)

Option 2: curl Examples

Authentication Flow

1. Create Account:

curl -X POST http://localhost:8000/api/v1/auth/signup \
  -H "Content-Type: application/json" \
  -d '{
    "username": "alice",
    "email": "alice@example.com",
    "password": "password123"
  }'

Expected Response (201):

{
  "token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
  "user": {
    "id": "a1b2c3d4-...",
    "username": "alice",
    "email": "alice@example.com",
    "createdAt": "2025-11-06T10:00:00Z"
  }
}

Save the token for next steps!

2. Login:

curl -X POST http://localhost:8000/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "username": "alice",
    "password": "password123"
  }'

3. Get Profile (Protected):

TOKEN="your-token-here"
curl -X GET http://localhost:8000/api/v1/users/profile \
  -H "Authorization: Bearer $TOKEN"

4. Logout (Blacklist Token):

curl -X POST http://localhost:8000/api/v1/auth/logout \
  -H "Authorization: Bearer $TOKEN"

After logout, the token cannot be used again!

List and Task Operations

1. Create List:

curl -X POST http://localhost:8000/api/v1/lists \
  -H "Content-Type: application/json" \
  -d '{"name":"Groceries","description":"Weekly shopping"}'

Save the returned list ID!

2. Get All Lists:

curl http://localhost:8000/api/v1/lists

3. Create Task:

# Replace LIST_ID with actual ID
curl -X POST http://localhost:8000/api/v1/lists/LIST_ID/tasks \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Buy milk",
    "priority": "high",
    "dueDate": "2025-11-07T18:00:00Z"
  }'

4. Update Task:

# Replace TASK_ID with actual ID
curl -X PATCH http://localhost:8000/api/v1/tasks/TASK_ID \
  -H "Content-Type: application/json" \
  -d '{"completed":true}'

5. Delete Task:

curl -X DELETE http://localhost:8000/api/v1/tasks/TASK_ID

6. Delete List:

curl -X DELETE http://localhost:8000/api/v1/lists/LIST_ID

Automated Test Script

Save as test_api.sh:

#!/bin/bash

echo "=== Testing TODO REST API ==="

# 1. Sign up
echo "1. Signing up..."
SIGNUP=$(curl -s -X POST http://localhost:8000/api/v1/auth/signup \
  -H "Content-Type: application/json" \
  -d '{"username":"alice","email":"alice@test.com","password":"pass123456"}')

TOKEN=$(echo $SIGNUP | grep -o '"token":"[^"]*"' | cut -d'"' -f4)
echo "Token: ${TOKEN:0:20}..."

# 2. Create list
echo "2. Creating list..."
LIST=$(curl -s -X POST http://localhost:8000/api/v1/lists \
  -H "Content-Type: application/json" \
  -d '{"name":"Test List"}')

LIST_ID=$(echo $LIST | grep -o '"id":"[^"]*"' | cut -d'"' -f4)
echo "List ID: $LIST_ID"

# 3. Create task
echo "3. Creating task..."
TASK=$(curl -s -X POST http://localhost:8000/api/v1/lists/$LIST_ID/tasks \
  -H "Content-Type: application/json" \
  -d '{"title":"Test Task","priority":"high"}')

TASK_ID=$(echo $TASK | grep -o '"id":"[^"]*"' | cut -d'"' -f4)
echo "Task ID: $TASK_ID"

# 4. Update task
echo "4. Updating task..."
curl -s -X PATCH http://localhost:8000/api/v1/tasks/$TASK_ID \
  -H "Content-Type: application/json" \
  -d '{"completed":true}' | jq

# 5. Cleanup
echo "5. Cleaning up..."
curl -s -X DELETE http://localhost:8000/api/v1/tasks/$TASK_ID
curl -s -X DELETE http://localhost:8000/api/v1/lists/$LIST_ID

echo "=== Test Complete ==="

Run with:

chmod +x test_api.sh
./test_api.sh

Run Unit Tests

composer test

Expected: 39 tests should pass


API Endpoints

See API.md for complete endpoint documentation.

Quick Reference

Method Endpoint Protected Description
Health
GET /api/v1/health No System health check
Authentication
POST /api/v1/auth/signup No Create account
POST /api/v1/auth/login No Login
POST /api/v1/auth/logout Yes Logout (blacklist token)
GET /api/v1/users/profile Yes Get profile
Lists
GET /api/v1/lists No Get all lists
GET /api/v1/lists/:id No Get list by ID
POST /api/v1/lists No Create list
PATCH /api/v1/lists/:id No Update list
DELETE /api/v1/lists/:id No Delete list
Tasks
GET /api/v1/lists/:listId/tasks No Get tasks in list
GET /api/v1/tasks/:id No Get task by ID
POST /api/v1/lists/:listId/tasks No Create task
PATCH /api/v1/tasks/:id No Update task
DELETE /api/v1/tasks/:id No Delete task

Protected endpoints require: Authorization: Bearer <token> header


Features

Security

  • Bcrypt password hashing (cost: 12)
  • JWT authentication with 1-hour expiry
  • Token blacklisting on logout
  • SQL injection prevention (prepared statements)
  • XSS protection (HTML escaping)
  • Input validation on all endpoints

Database

  • SQLite file-based database
  • Foreign key constraints
  • Cascade deletes
  • Automatic timestamps

Logging

  • Request/response logging
  • Error tracking
  • Debug mode toggle

Project Structure

php-version/
├── public/index.php      # Entry point
├── src/
│   ├── Controllers/      # Request handlers
│   ├── Models/           # Database operations
│   └── Services/         # Business logic
├── bruno/                # API test collection (14 requests)
├── tests/                # Unit tests (39 tests)
├── data/                 # SQLite database
├── logs/                 # API logs
└── .env                  # Configuration

Development

View Logs

# Real-time monitoring
tail -f logs/api.log

# View last 50 lines
tail -n 50 logs/api.log

# Search for errors
grep ERROR logs/api.log

Log Format:

[2025-11-06T10:00:00Z] INFO: GET /api/v1/lists - 200 OK - 15ms
[2025-11-06T10:01:00Z] ERROR: Failed to fetch list - Error details...

Inspect Database

sqlite3 data/todo.db

# View tables
.tables

# View lists
SELECT * FROM lists;

# View tasks
SELECT * FROM tasks;

# View users
SELECT * FROM users;

# View blacklisted tokens
SELECT * FROM token_blacklist;

# Exit
.quit

Reset Database

rm data/todo.db
# Will be recreated on next request

Deployment

For Production

  1. Update .env:
DEBUG_MODE=false
LOG_LEVEL=error
JWT_SECRET=strong-random-secret-here
  1. Generate strong JWT secret:
openssl rand -base64 32
  1. Configure Nginx:
server {
    listen 80;
    server_name your-domain.com;
    root /path/to/php-version/public;
    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
        fastcgi_index index.php;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
}
  1. Use HTTPS (required for production)

  2. Set proper permissions:

chmod 755 logs data
chmod 644 .env

AWS EC2 Deployment

The project includes automated AWS EC2 deployment scripts in the deployment/ directory.

Prerequisites

  • AWS CLI configured with credentials (aws configure)
  • SSH access to create EC2 instances

Launch EC2 Instance

cd deployment
./launch-ec2.sh

This script will: - Create a Free Tier eligible t3.micro instance - Set up security group (ports 22, 80, 443) - Generate SSH key pair (php-todo-api-key.pem) - Return the public IP address

Deploy Application

After the instance is running, SSH into it:

ssh -i php-todo-api-key.pem ubuntu@<PUBLIC_IP>

Upload and run the deployment script:

# From your local machine
scp -i deployment/php-todo-api-key.pem deployment/deploy-ec2-setup.sh ubuntu@<PUBLIC_IP>:~/
scp -i deployment/php-todo-api-key.pem deployment/nginx-production.conf ubuntu@<PUBLIC_IP>:~/

# On the EC2 instance
chmod +x deploy-ec2-setup.sh
./deploy-ec2-setup.sh

The setup script installs: - Nginx web server - PHP 8.1 with required extensions - Composer - Git

Then manually deploy your code:

# Clone or upload your code to /var/www/php-todo-api
cd /var/www/php-todo-api
composer install --no-dev --optimize-autoloader

# Configure nginx
sudo cp ~/nginx-production.conf /etc/nginx/sites-available/php-todo-api
sudo ln -sf /etc/nginx/sites-available/php-todo-api /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default

# Set permissions
sudo chown -R www-data:www-data /var/www/php-todo-api
sudo chmod -R 755 /var/www/php-todo-api
sudo chmod -R 775 /var/www/php-todo-api/data /var/www/php-todo-api/logs

# Test and restart nginx
sudo nginx -t
sudo systemctl restart nginx
sudo systemctl restart php8.1-fpm

Test Deployment

# Check health endpoint
curl http://<PUBLIC_IP>/api/v1/health

# Test API
curl http://<PUBLIC_IP>/api/v1/lists

Cleanup AWS Resources

To remove all AWS resources and avoid charges:

cd deployment
./cleanup-aws.sh

This will terminate the EC2 instance, delete the security group, and remove the key pair.

Important Notes: - Keep your .pem file secure (it's gitignored) - Free Tier instances are free for 750 hours/month for 12 months - Monitor your AWS billing dashboard - The cleanup script will ask for confirmation before deletion


Environment Variables

Variable Default Description
DEBUG_MODE true Show detailed errors
LOG_LEVEL debug Logging level (debug/info/warning/error)
DATABASE_PATH data/todo.db SQLite database path
API_VERSION v1 API version
JWT_SECRET Required Secret key for JWT signing
JWT_EXPIRY 3600 Token lifetime in seconds

Troubleshooting

Installation Issues

Issue: Composer not found

brew install composer

Issue: PHP not found

brew install php

Issue: SQLite extension not loaded

# Verify SQLite is available
php -m | grep sqlite

# If missing, reinstall PHP
brew reinstall php

Issue: Permission denied on logs/ or data/

chmod 755 logs data

Server Issues

Issue: Port 8000 in use

# Use a different port
php -S localhost:8080 -t public

# Update Bruno environment to http://localhost:8080

Issue: Database locked

# Check for other processes using the database
lsof data/todo.db

# If needed, restart the server

Authentication Issues

Issue: Token validation fails

Causes and solutions:

  1. JWT_SECRET not set in .env
  2. Verify .env file exists
  3. Check JWT_SECRET is not empty

  4. Token expired

  5. Tokens expire after 1 hour
  6. Login again to get a new token

  7. Token blacklisted

  8. Tokens are blacklisted after logout
  9. Login again to get a new token

  10. Wrong Authorization header format

  11. Must be: Authorization: Bearer TOKEN
  12. Not: Authorization: TOKEN

Issue: 401 Unauthorized

# Ensure token is in Authorization header
curl -H "Authorization: Bearer YOUR_TOKEN" ...

Issue: 409 Conflict (duplicate username/email)

Use a different username or email

Testing Issues

Issue: Unit tests fail

# Reset test database
rm data/test_todo.db
composer test

Issue: curl commands don't work

# Verify server is running
curl http://localhost:8000/api/v1/lists

# Check for syntax errors in curl command
# Ensure proper escaping of quotes

Testing Checklist

Authentication

  • [ ] Sign up with valid data
  • [ ] Sign up with duplicate username (should fail)
  • [ ] Sign up with invalid email (should fail)
  • [ ] Sign up with short password (should fail)
  • [ ] Login with correct credentials
  • [ ] Login with wrong password (should fail)
  • [ ] Access profile with valid token
  • [ ] Access profile without token (should fail)
  • [ ] Access profile with expired token (should fail)
  • [ ] Logout and verify token is blacklisted

Lists & Tasks

  • [ ] Create a list
  • [ ] Get all lists
  • [ ] Update a list
  • [ ] Create a task in the list
  • [ ] Mark task as completed
  • [ ] Delete task
  • [ ] Delete list (should cascade delete tasks)

Error Cases

  • [ ] Invalid UUID format (should return 400)
  • [ ] Missing required field (should return 400)
  • [ ] Invalid Content-Type (should return 415)
  • [ ] Non-existent resource (should return 404)
  • [ ] Invalid enum value (should return 400)

Documentation

  • API.md - Complete API reference with all endpoints
  • TOKEN_BLACKLIST.md - Token blacklisting explanation and JWT_SECRET clarification
  • IMPLEMENTATION_SUMMARY.md - Project implementation summary

Support

  • API Reference: API.md
  • Logs: logs/api.log
  • Database: data/todo.db
  • Unit Tests: composer test

License

NKU-640 Coursework Project