# Todo App Backend
A RESTful API backend for a todo application built with **CodeIgniter 4**.
Supports user authentication (API key + JWT), CRUD for todos/categories/projects,
recurring tasks, activity logging, and a theme marketplace.
---
## Table of Contents
- [Quick Start](#quick-start)
- [API Documentation](#api-documentation)
- [Authentication](#authentication)
- [API Overview](#api-overview)
- [Testing](#testing)
- [Project Structure](#project-structure)
- [Database](#database)
- [Development](#development)
- [Contributing](#contributing)
---
## Quick Start
### Requirements
- PHP ^8.2
- MySQL 8+ (or MariaDB 10.5+)
- Composer
- `ext-intl`, `ext-mbstring`
### Setup
```bash
# 1. Clone and enter the project
cd Todo-App-Backend
# 2. Install dependencies
composer install
# 3. Configure your environment
cp env.example .env
# Edit .env — set database credentials and app.baseURL
# 4. Run database migrations
php spark migrate
# 5. (Optional) Seed sample data
php spark db:seed SampleDataSeeder
# 6. Start the development server
php spark serve
# The API is now available at http://localhost:8080/api/v1
```
### Quick Test
```bash
# Register a new user
curl -s -X POST http://localhost:8080/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"demo@example.com","password":"password123","name":"Demo User"}'
# Save the returned api_key, then:
curl -s http://localhost:8080/api/v1/todos \
-H "X-API-Key: todo_your_key_here"
```
---
## API Documentation
The API is fully documented using the **OpenAPI 3.0** specification.
| Resource | Location |
|----------|----------|
| OpenAPI spec (canonical) | [`openapi/openapi.yaml`](openapi/openapi.yaml) |
| Generated HTML docs | `public/api-docs.html` (generated, see below) |
| Swagger/Postman import | Use `openapi/openapi.yaml` directly |
### Generating API Docs
From the project root:
```bash
# Generate HTML documentation page
php spark generate:api-docs
# Validate spec only (no file written)
php spark generate:api-docs --watch
# Open http://localhost:8080/api-docs.html after generating
```
The generated HTML uses [Redoc](https://redocly.com/redoc) for rendering and is
fully self-contained (the spec is embedded as a base64 data URI).
### Importing into Tools
- **Postman**: File → Import → choose `openapi/openapi.yaml`
- **Insomnia**: Import → From File → choose `openapi/openapi.yaml`
- **Swagger Editor**: Paste the contents of `openapi/openapi.yaml`
- **cURL/HTTPie**: Examples are in the OpenAPI spec under each endpoint
---
## Authentication
The API supports two authentication methods:
### 1. API Key Authentication (Primary)
```
X-API-Key: todo_abc123def456...
```
Used by most protected endpoints. Keys are obtained on registration or can be
created via `POST /user/api-keys`. Keys can be scoped (`read`, `write`) and
optionally expire.
### 2. JWT Bearer Authentication
```
Authorization: Bearer eyJ0eXAiOiJKV1Qi...
```
Available via the `/auth/jwt/*` endpoints. Tokens are valid for 1 hour and can
be refreshed via `/auth/jwt/refresh`.
### Authentication Flow
1. **Register** → receive API key + key prefix
2. **Login** → receive the same or existing API key
3. Include `X-API-Key` header on all protected requests
4. Optionally use JWT endpoints for short-lived bearer tokens
---
## API Overview
All endpoints live under the `/api/v1` prefix.
### Public Endpoints
| Method | Path | Description |
|--------|------|-------------|
| POST | `/auth/register` | Register a new user |
| POST | `/auth/login` | Login and get API key |
| POST | `/auth/api-key` | Create additional API key (legacy) |
| POST | `/auth/jwt/register` | Register and receive JWT + API key |
| POST | `/auth/jwt/login` | Login and receive JWT |
| POST | `/auth/jwt/refresh` | Refresh an existing JWT |
| GET | `/marketplace/themes` | List published themes |
| GET | `/marketplace/themes/{id}` | Get a single theme |
### Protected Endpoints (API key required)
#### User
| Method | Path | Description |
|--------|------|-------------|
| GET | `/user/profile` | Get your profile |
| PUT | `/user/profile` | Update your profile |
| GET | `/user/api-keys` | List your API keys |
| POST | `/user/api-keys` | Create a new API key |
| DELETE | `/user/api-keys/{id}` | Revoke an API key |
#### Categories
| Method | Path | Description |
|--------|------|-------------|
| GET | `/categories` | List categories (paginated, sortable) |
| POST | `/categories` | Create a category |
| GET | `/categories/{id}` | Get a category |
| PUT | `/categories/{id}` | Update a category |
| DELETE | `/categories/{id}` | Delete a category |
#### Projects
| Method | Path | Description |
|--------|------|-------------|
| GET | `/projects` | List projects (paginated, sortable) |
| POST | `/projects` | Create a project |
| GET | `/projects/{id}` | Get a project |
| PUT | `/projects/{id}` | Update a project |
| DELETE | `/projects/{id}` | Delete a project |
#### Todos
| Method | Path | Description |
|--------|------|-------------|
| GET | `/todos` | List todos (paginated, sortable, filterable) |
| POST | `/todos` | Create a todo |
| GET | `/todos/{id}` | Get a todo |
| PUT | `/todos/{id}` | Update a todo |
| DELETE | `/todos/{id}` | Delete a todo |
| POST | `/todos/{id}/categories` | Link a category |
| DELETE | `/todos/{id}/categories/{catId}` | Unlink a category |
#### Recurring Tasks
| Method | Path | Description |
|--------|------|-------------|
| GET | `/recurring-tasks` | List recurring tasks (paginated, sortable, filterable) |
| POST | `/recurring-tasks` | Create a recurring task |
| GET | `/recurring-tasks/{id}` | Get a recurring task |
| PUT | `/recurring-tasks/{id}` | Update a recurring task |
| DELETE | `/recurring-tasks/{id}` | Delete a recurring task |
| POST | `/recurring-tasks/{id}/categories` | Link a category |
| DELETE | `/recurring-tasks/{id}/categories/{catId}` | Unlink a category |
#### Activity Logs
| Method | Path | Description |
|--------|------|-------------|
| GET | `/activity-logs` | List activity logs |
| GET | `/activity-logs/{id}` | Get a single log entry |
#### User Themes
| Method | Path | Description |
|--------|------|-------------|
| GET | `/user/themes` | List installed themes |
| POST | `/user/themes` | Install a theme |
| PUT | `/user/themes/{id}` | Update theme settings |
| DELETE | `/user/themes/{id}` | Uninstall a theme |
### Common Query Parameters
| Parameter | Type | Description | Example |
|-----------|------|-------------|---------|
| `page` | int | Page number (default: 1) | `?page=2` |
| `per_page` | int | Items per page (default: 50, max: 200) | `?per_page=10` |
| `sort` | string | Sort fields, `-` for descending, comma-separated | `?sort=-created_at,title` |
| `status` | string | Filter by status (todos) | `?status=open` |
| `favorite` | bool | Filter favorites (categories) | `?favorite=1` |
| `limit` | int | Max items (activity logs, default: 50) | `?limit=100` |
### Sorting
Sortable fields vary per resource. Prefix a field with `-` for descending:
```
GET /api/v1/todos?sort=-created_at,title
GET /api/v1/categories?sort=name
GET /api/v1/projects?sort=-created_at
GET /api/v1/recurring-tasks?sort=title,-created_at
```
### Response Format
All responses follow a consistent envelope:
**Success:**
```json
{
"success": true,
"message": "Todos retrieved successfully",
"data": [ ... ],
"pagination": {
"page": 1,
"per_page": 50,
"total": 123,
"last_page": 3,
"has_more": true
}
}
```
**Error:**
```json
{
"success": false,
"message": "Validation failed",
"errors": {
"title": "The todo title is required."
}
}
```
### HTTP Status Codes
| Code | Meaning |
|------|---------|
| 200 | Success |
| 201 | Created |
| 400 | Bad request |
| 401 | Unauthorized (missing/invalid API key) |
| 403 | Forbidden (insufficient scope) |
| 404 | Not found |
| 409 | Conflict (duplicate) |
| 422 | Validation failed |
| 500 | Server error |
### Pagination
Paginated responses include a `pagination` object:
```json
{
"pagination": {
"page": 1,
"per_page": 50,
"total": 123,
"last_page": 3,
"has_more": true
}
}
```
### Todo Status Values
- `open`
- `in_progress`
- `completed`
- `archived`
### Recurring Task Schedule Values
- `daily`
- `weekly`
- `monthly`
- `custom` (requires `custom_days` array, e.g. `["mon","wed","fri"]`)
---
## Testing
### Running Tests
```bash
# Run all tests via composer
composer run test
# Or use phpunit directly
./vendor/bin/phpunit
# Run only API tests
./vendor/bin/phpunit tests/api
# With code coverage
./vendor/bin/phpunit --coverage-text
```
### Database Setup
Integration tests use your configured MySQL database. Make sure migrations are applied first:
```bash
php spark migrate
```
For a dedicated test database, uncomment the test DB config in `phpunit.xml.dist`:
```xml
```
Then create it and migrate:
```bash
mysql -e "CREATE DATABASE IF NOT EXISTS todo_app_test;"
php spark migrate
```
### Test Suite
| Directory | Description |
|-----------|-------------|
| `tests/api/ApiTest.php` | Full API integration tests (auth, CRUD, filtering, error handling) |
| `tests/unit/` | Unit tests for individual components |
| `tests/database/` | Database migration and seed tests |
| `tests/session/` | Session-related tests |
The API test suite covers:
- Registration and login
- Authentication errors (missing key, invalid credentials)
- Full CRUD for all resources (categories, projects, todos, recurring tasks)
- Category-todo / category-recurring-task linking
- Status and sort filtering
- Activity logging verification
- Ownership isolation (cross-user access denied)
- Validation error responses
- Pagination structure
---
## Project Structure
```
├── app/
│ ├── Commands/ # Spark CLI commands
│ │ ├── TestModels.php # Model testing command
│ │ └── GenerateApiDocs.php # OpenAPI → HTML docs generator
│ ├── Config/ # Application configuration
│ ├── Controllers/
│ │ ├── Api/
│ │ │ ├── BaseController.php # Shared API helpers (pagination, JWT, responses)
│ │ │ └── V1/
│ │ │ ├── AuthController.php
│ │ │ ├── CategoryController.php
│ │ │ ├── ProjectController.php
│ │ │ ├── TodoController.php
│ │ │ ├── RecurringTaskController.php
│ │ │ ├── UserController.php
│ │ │ ├── ActivityLogController.php
│ │ │ ├── MarketplaceController.php
│ │ │ └── UserThemeController.php
│ │ ├── BaseController.php
│ │ ├── Home.php
│ │ └── ThemeStore.php
│ ├── Database/
│ │ ├── Migrations/ # 16 migration files (users → api_auth_keys)
│ │ └── Seeds/ # Sample data, themes, AI providers
│ ├── Filters/
│ │ └── ApiAuthFilter.php # API key authentication filter
│ ├── Models/ # Database models (TodoModel, CategoryModel, etc.)
│ └── Views/ # Error pages, welcome message, theme store
├── openapi/
│ └── openapi.yaml # Canonical OpenAPI 3.0 specification
├── public/
│ ├── api-docs.html # Generated API documentation (gitignored?)
│ ├── index.php # Front controller
│ └── themes/ # Uploaded theme CSS files
├── tests/
│ ├── api/ApiTest.php # Full API integration tests
│ ├── unit/ # Unit tests
│ ├── database/ # Database tests
│ └── _support/ # Test helpers, models, seeds
├── writable/ # Logs, cache, uploads
├── composer.json
├── env.example
└── README.md
```
---
## Database
### Schema Overview
The database consists of 12 tables:
| Table | Description |
|-------|-------------|
| `users` | User accounts (email, password hash, settings) |
| `api_auth_keys` | API keys (hashed, scoped, expirable) |
| `categories` | User-defined categories (with hex color) |
| `projects` | User-defined projects |
| `todos` | Tasks with status, due dates, project links |
| `todo_categories` | Many-to-many: todos ↔ categories |
| `recurring_tasks` | Recurring task templates (daily/weekly/etc.) |
| `recurring_task_categories` | Many-to-many: recurring_tasks ↔ categories |
| `activity_logs` | Audit trail (CRUD events, login, etc.) |
| `marketplace_themes` | Published theme definitions |
| `user_themes` | Per-user theme installations |
| `ai_chats / ai_messages / ai_providers / user_ai_settings / user_api_keys` | AI assistant features |
### Migrations
```bash
# Run all pending migrations
php spark migrate
# Roll back all migrations
php spark migrate:rollback
# Seed sample data
php spark db:seed SampleDataSeeder
```
---
## Development
### Adding a New Endpoint
1. Add the route in `app/Config/Routes.php`
2. Create the controller method (extends `App\Controllers\Api\BaseController`)
3. Create the model (extends `CodeIgniter\Model`)
4. Write migration if needed
5. Update `openapi/openapi.yaml` with the new endpoint
6. Run `php spark generate:api-docs` to regenerate HTML docs
7. Write tests in `tests/api/ApiTest.php`
### Updating Documentation
The **single source of truth** is `openapi/openapi.yaml`. After any API change:
1. Update the YAML spec
2. Run `php spark generate:api-docs`
3. Commit both files
### Available Spark Commands
```bash
php spark list # List all available commands
php spark generate:api-docs # Generate HTML docs from OpenAPI spec
php spark generate:api-docs --watch # Validate spec only
php spark migrate # Run database migrations
php spark db:seed # Seed the database
composer run test # Run all tests
php spark test:models # Test models
```
### Common Workflows
**New todo with category:**
```bash
KEY="todo_your_key_here"
# Create category
CAT=$(curl -s -X POST http://localhost:8080/api/v1/categories \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"name":"Work","color":"#3B82F6"}')
CAT_ID=$(echo $CAT | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
# Create todo with that category
curl -s -X POST http://localhost:8080/api/v1/todos \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d "{\"title\":\"Finish report\",\"status\":\"open\",\"category_id\":\"$CAT_ID\"}"
```
**Filter and sort todos:**
```bash
curl -s "http://localhost:8080/api/v1/todos?status=open&sort=-due_date,title&per_page=5" \
-H "X-API-Key: $KEY"
```
---
## Contributing
1. Keep the OpenAPI spec (`openapi/openapi.yaml`) in sync with code changes
2. Run `php spark generate:api-docs --watch` to validate your YAML changes
3. Write tests for new endpoints
4. Run the full test suite before pushing
5. Follow CodeIgniter 4 conventions
---
## License
MIT — see [LICENSE](LICENSE).