TODO REST API - PHP
A secure REST API for managing TODO lists and tasks with JWT authentication.
Table of Contents
- Quick Start
- Installation
- Configuration
- Testing
- API Endpoints
- Features
- Development
- Deployment
- Troubleshooting
Quick Start
Prerequisites
# Install PHP 8.1+ and Composer
brew install php composer
Installation
cd ./docs/homework4/php-version
composer install
Configuration
- Generate a JWT secret:
openssl rand -base64 32
- 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
Option 1: Bruno (Recommended)
- Download Bruno from https://www.usebruno.com/
- Open Bruno and import collection from
bruno/folder - Select "local" environment
- Run requests in order:
- Auth > Signup (copy the token)
- Auth > Get Profile (paste token)
- Lists > Create List (copy list ID)
- 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
- Update
.env:
DEBUG_MODE=false
LOG_LEVEL=error
JWT_SECRET=strong-random-secret-here
- Generate strong JWT secret:
openssl rand -base64 32
- 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;
}
}
-
Use HTTPS (required for production)
-
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:
- JWT_SECRET not set in
.env - Verify
.envfile exists -
Check
JWT_SECRETis not empty -
Token expired
- Tokens expire after 1 hour
-
Login again to get a new token
-
Token blacklisted
- Tokens are blacklisted after logout
-
Login again to get a new token
-
Wrong Authorization header format
- Must be:
Authorization: Bearer TOKEN - 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