Merge main into feature/marketplace

This commit is contained in:
Cametendo
2026-05-13 16:23:30 +02:00
29 changed files with 2940 additions and 59 deletions

825
API_DOCUMENTATION.md Normal file
View File

@@ -0,0 +1,825 @@
# Todo App API Documentation
## Version: 1.0
Base URL: `http://localhost:8080/api/v1`
## Overview
This API provides access to the Todo App functionality with versioned endpoints. The API uses API key authentication for protected endpoints, while some endpoints (like the marketplace) are publicly accessible.
## Authentication
### API Key Authentication
Most endpoints require an API key for authentication. The API key should be included in the `X-API-Key` header.
**Header:**
```
X-API-Key: todo_your_api_key_here
```
### Register a New User
**Endpoint:** `POST /api/v1/auth/register`
**Request Body:**
```json
{
"email": "user@example.com",
"password": "your_password",
"name": "John Doe",
"avatar_url": "https://example.com/avatar.jpg",
"settings": {
"theme": "dark",
"language": "en"
}
}
```
**Response:**
```json
{
"success": true,
"message": "User registered successfully",
"data": {
"user": {
"id": "user-uuid",
"email": "user@example.com",
"name": "John Doe",
"avatar_url": "https://example.com/avatar.jpg",
"settings": {"theme": "dark"},
"created_at": "2025-01-01 00:00:00",
"updated_at": "2025-01-01 00:00:00"
},
"api_key": "todo_abc123...",
"key_prefix": "todo_abc1"
}
}
```
**Important:** Store the API key securely. You won't be able to retrieve it again.
### Login
**Endpoint:** `POST /api/v1/auth/login`
**Request Body:**
```json
{
"email": "user@example.com",
"password": "your_password"
}
```
**Response (New API Key Created):**
```json
{
"success": true,
"message": "Login successful",
"data": {
"user": {
"id": "user-uuid",
"email": "user@example.com",
"name": "John Doe"
},
"api_key": "todo_abc123...",
"key_prefix": "todo_abc1"
}
}
```
**Response (Using Existing API Key):**
```json
{
"success": true,
"message": "Login successful",
"data": {
"user": {
"id": "user-uuid",
"email": "user@example.com",
"name": "John Doe"
},
"api_key_prefix": "todo_abc1",
"message": "Using existing API key"
}
}
```
**Note:** If you already have an active API key, the login will return the key prefix only (not the full key for security). You should store your API key securely after the first login.
### Creating an API Key (Legacy)
To create an additional API key, you can use this endpoint:
**Endpoint:** `POST /api/v1/auth/api-key`
**Request Body:**
```json
{
"email": "user@example.com",
"password": "your_password",
"name": "My App Key",
"scopes": ["read", "write"],
"expires_at": "2026-12-31 23:59:59"
}
```
**Response:**
```json
{
"success": true,
"message": "API key created successfully",
"data": {
"key": "todo_abc123...",
"prefix": "todo_abc1",
"name": "My App Key",
"scopes": ["read", "write"],
"expires_at": "2026-12-31 23:59:59"
}
}
```
### Scopes
API keys can have the following scopes:
- `read` - Read-only access to data
- `write` - Full access to create, update, and delete data
If no scopes are specified, the key will have full access.
## Public Endpoints
These endpoints do not require authentication.
### Marketplace Themes
#### Get All Themes
**Endpoint:** `GET /api/v1/marketplace/themes`
**Response:**
```json
{
"success": true,
"message": "Marketplace themes retrieved successfully",
"data": [
{
"id": "theme-id-1",
"name": "Dark Theme",
"description": "A dark theme for the app",
"preview_url": "https://example.com/preview.png",
"price": 0,
"is_free": true,
"created_at": "2025-01-01 00:00:00"
}
]
}
```
#### Get Theme by ID
**Endpoint:** `GET /api/v1/marketplace/themes/{id}`
**Response:**
```json
{
"success": true,
"message": "Theme retrieved successfully",
"data": {
"id": "theme-id-1",
"name": "Dark Theme",
"description": "A dark theme for the app",
"preview_url": "https://example.com/preview.png",
"price": 0,
"is_free": true,
"created_at": "2025-01-01 00:00:00"
}
}
```
## Protected Endpoints
These endpoints require an API key in the `X-API-Key` header.
### User Management
#### Get User Profile
**Endpoint:** `GET /api/v1/user/profile`
**Response:**
```json
{
"success": true,
"message": "Profile retrieved successfully",
"data": {
"id": "user-id",
"email": "user@example.com",
"name": "John Doe",
"avatar_url": null,
"settings": {"theme": "dark"},
"created_at": "2025-01-01 00:00:00",
"updated_at": "2025-01-01 00:00:00"
}
}
```
#### Update User Profile
**Endpoint:** `PUT /api/v1/user/profile`
**Request Body:**
```json
{
"name": "Jane Doe",
"avatar_url": "https://example.com/avatar.jpg",
"settings": {"theme": "light", "language": "en"}
}
```
#### List API Keys
**Endpoint:** `GET /api/v1/user/api-keys`
**Response:**
```json
{
"success": true,
"message": "API keys retrieved successfully",
"data": [
{
"id": "key-id",
"key_prefix": "todo_abc1",
"name": "My App Key",
"scopes": ["read", "write"],
"is_active": true,
"last_used_at": "2025-01-01 12:00:00",
"created_at": "2025-01-01 00:00:00"
}
]
}
```
#### Create API Key
**Endpoint:** `POST /api/v1/user/api-keys`
**Request Body:**
```json
{
"name": "New App Key",
"scopes": ["read"],
"expires_at": "2026-12-31 23:59:59"
}
```
#### Revoke API Key
**Endpoint:** `DELETE /api/v1/user/api-keys/{id}`
### Categories
#### Get All Categories
**Endpoint:** `GET /api/v1/categories`
**Response:**
```json
{
"success": true,
"message": "Categories retrieved successfully",
"data": [
{
"id": "cat-id-1",
"user_id": "user-id",
"name": "Work",
"color": "#3B82F6",
"favorite": true,
"created_at": "2025-01-01 00:00:00"
}
]
}
```
#### Create Category
**Endpoint:** `POST /api/v1/categories`
**Request Body:**
```json
{
"name": "Personal",
"color": "#10B981",
"favorite": false
}
```
#### Get Category
**Endpoint:** `GET /api/v1/categories/{id}`
#### Update Category
**Endpoint:** `PUT /api/v1/categories/{id}`
**Request Body:**
```json
{
"name": "Updated Name",
"color": "#FF5733",
"favorite": true
}
```
#### Delete Category
**Endpoint:** `DELETE /api/v1/categories/{id}`
### Projects
#### Get All Projects
**Endpoint:** `GET /api/v1/projects`
**Response:**
```json
{
"success": true,
"message": "Projects retrieved successfully",
"data": [
{
"id": "proj-id-1",
"user_id": "user-id",
"name": "Web Redesign",
"description": "Redesign the company website",
"color": "#8B5CF6",
"created_at": "2025-01-01 00:00:00"
}
]
}
```
#### Create Project
**Endpoint:** `POST /api/v1/projects`
**Request Body:**
```json
{
"name": "New Project",
"description": "Project description",
"color": "#EC4899"
}
```
#### Get Project
**Endpoint:** `GET /api/v1/projects/{id}`
#### Update Project
**Endpoint:** `PUT /api/v1/projects/{id}`
**Request Body:**
```json
{
"name": "Updated Project",
"description": "Updated description",
"color": "#14B8A6"
}
```
#### Delete Project
**Endpoint:** `DELETE /api/v1/projects/{id}`
### Todos
#### Get All Todos
**Endpoint:** `GET /api/v1/todos`
**Response:**
```json
{
"success": true,
"message": "Todos retrieved successfully",
"data": [
{
"id": "todo-id-1",
"user_id": "user-id",
"title": "Complete task",
"description": "Task description",
"status": "open",
"due_date": "2025-01-15",
"due_time": "10:30:00",
"sync_enabled": true,
"reminder_enabled": false,
"recurring_enabled": false,
"project_id": "proj-id-1",
"created_at": "2025-01-01 00:00:00",
"updated_at": "2025-01-01 00:00:00",
"categories": [
{
"id": "cat-id-1",
"name": "Work",
"color": "#3B82F6"
}
]
}
]
}
```
#### Create Todo
**Endpoint:** `POST /api/v1/todos`
**Request Body:**
```json
{
"title": "New Task",
"description": "Task description",
"status": "open",
"due_date": "2025-01-15",
"due_time": "10:30:00",
"sync_enabled": true,
"reminder_enabled": false,
"recurring_enabled": false,
"project_id": "proj-id-1"
}
```
**Status options:** `open`, `in_progress`, `completed`, `archived`
#### Get Todo
**Endpoint:** `GET /api/v1/todos/{id}`
#### Update Todo
**Endpoint:** `PUT /api/v1/todos/{id}`
**Request Body:**
```json
{
"title": "Updated Task",
"status": "in_progress",
"due_date": "2025-01-20"
}
```
#### Delete Todo
**Endpoint:** `DELETE /api/v1/todos/{id}`
#### Add Category to Todo
**Endpoint:** `POST /api/v1/todos/{id}/categories`
**Request Body:**
```json
{
"category_id": "cat-id-1"
}
```
#### Remove Category from Todo
**Endpoint:** `DELETE /api/v1/todos/{id}/categories/{categoryId}`
### Recurring Tasks
#### Get All Recurring Tasks
**Endpoint:** `GET /api/v1/recurring-tasks`
**Response:**
```json
{
"success": true,
"message": "Recurring tasks retrieved successfully",
"data": [
{
"id": "rt-id-1",
"user_id": "user-id",
"title": "Weekly Review",
"description": "Plan next week's tasks",
"schedule": "weekly",
"custom_days": [],
"favorite": true,
"created_at": "2025-01-01 00:00:00",
"updated_at": "2025-01-01 00:00:00",
"categories": [
{
"id": "cat-id-1",
"name": "Work",
"color": "#3B82F6"
}
]
}
]
}
```
#### Create Recurring Task
**Endpoint:** `POST /api/v1/recurring-tasks`
**Request Body:**
```json
{
"title": "Daily Standup",
"description": "Team meeting every morning",
"schedule": "daily",
"custom_days": [],
"favorite": true
}
```
**Schedule options:** `daily`, `weekly`, `monthly`, `custom`
For `custom` schedule, provide days in `custom_days` array: `["mon", "wed", "fri"]`
#### Get Recurring Task
**Endpoint:** `GET /api/v1/recurring-tasks/{id}`
#### Update Recurring Task
**Endpoint:** `PUT /api/v1/recurring-tasks/{id}`
**Request Body:**
```json
{
"title": "Updated Task",
"schedule": "weekly",
"custom_days": ["mon"]
}
```
#### Delete Recurring Task
**Endpoint:** `DELETE /api/v1/recurring-tasks/{id}`
#### Add Category to Recurring Task
**Endpoint:** `POST /api/v1/recurring-tasks/{id}/categories`
**Request Body:**
```json
{
"category_id": "cat-id-1"
}
```
#### Remove Category from Recurring Task
**Endpoint:** `DELETE /api/v1/recurring-tasks/{id}/categories/{categoryId}`
### Activity Logs
#### Get Activity Logs
**Endpoint:** `GET /api/v1/activity-logs?limit=50`
**Response:**
```json
{
"success": true,
"message": "Activity logs retrieved successfully",
"data": [
{
"id": "log-id-1",
"user_id": "user-id",
"action": "todo_created",
"entity_type": "todo",
"entity_id": "todo-id-1",
"details": {
"title": "New Task"
},
"ip_address": "127.0.0.1",
"user_agent": "Mozilla/5.0...",
"created_at": "2025-01-01 12:00:00"
}
]
}
```
#### Get Activity Log
**Endpoint:** `GET /api/v1/activity-logs/{id}`
### User Themes
#### Get User Themes
**Endpoint:** `GET /api/v1/user/themes`
**Response:**
```json
{
"success": true,
"message": "User themes retrieved successfully",
"data": [
{
"id": "ut-id-1",
"user_id": "user-id",
"theme_id": "theme-id-1",
"is_active": true,
"custom_settings": {"primary_color": "#3B82F6"},
"created_at": "2025-01-01 00:00:00"
}
]
}
```
#### Create User Theme
**Endpoint:** `POST /api/v1/user/themes`
**Request Body:**
```json
{
"theme_id": "theme-id-1",
"is_active": true,
"custom_settings": {
"primary_color": "#3B82F6",
"font_size": "medium"
}
}
```
#### Update User Theme
**Endpoint:** `PUT /api/v1/user/themes/{id}`
**Request Body:**
```json
{
"is_active": false,
"custom_settings": {
"primary_color": "#EC4899"
}
}
```
#### Delete User Theme
**Endpoint:** `DELETE /api/v1/user/themes/{id}`
## Error Responses
All error responses follow this format:
```json
{
"success": false,
"message": "Error message",
"errors": {
"field": "Validation error message"
}
}
```
### Common HTTP Status Codes
- `200 OK` - Request successful
- `201 Created` - Resource created successfully
- `400 Bad Request` - Invalid request data
- `401 Unauthorized` - Missing or invalid API key
- `403 Forbidden` - Insufficient permissions
- `404 Not Found` - Resource not found
- `409 Conflict` - Resource already exists
- `422 Unprocessable Entity` - Validation failed
- `500 Internal Server Error` - Server error
## Rate Limiting
Currently, there is no rate limiting implemented. Consider adding rate limiting for production use.
## CORS
If you need to enable CORS for frontend applications, configure it in `app/Config/Filters.php`.
## Example Usage
### cURL Examples
**Register a New User:**
```bash
curl -X POST http://localhost:8080/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "newuser@example.com",
"password": "securepassword",
"name": "New User"
}'
```
**Login:**
```bash
curl -X POST http://localhost:8080/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "password123"
}'
```
**Create API Key (additional key):**
```bash
curl -X POST http://localhost:8080/api/v1/auth/api-key \
-H "Content-Type: application/json" \
-d '{
"email": "demo@example.com",
"password": "password123",
"name": "My App Key"
}'
```
**Get Todos (with API key):**
```bash
curl -X GET http://localhost:8080/api/v1/todos \
-H "X-API-Key: todo_your_api_key_here"
```
**Create Todo:**
```bash
curl -X POST http://localhost:8080/api/v1/todos \
-H "X-API-Key: todo_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"title": "New Task",
"status": "open"
}'
```
### JavaScript/Fetch Examples
**Register a New User:**
```javascript
fetch('http://localhost:8080/api/v1/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: 'newuser@example.com',
password: 'securepassword',
name: 'New User'
})
})
.then(response => response.json())
.then(data => {
console.log('API Key:', data.data.api_key);
console.log('User:', data.data.user);
});
```
**Login:**
```javascript
fetch('http://localhost:8080/api/v1/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: 'user@example.com',
password: 'password123'
})
})
.then(response => response.json())
.then(data => {
if (data.data.api_key) {
console.log('New API Key:', data.data.api_key);
} else {
console.log('Using existing key with prefix:', data.data.api_key_prefix);
}
});
```
**Create API Key (additional key):**
```javascript
fetch('http://localhost:8080/api/v1/auth/api-key', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: 'demo@example.com',
password: 'password123',
name: 'My App Key'
})
})
.then(response => response.json())
.then(data => console.log(data));
```
**Get Todos:**
```javascript
fetch('http://localhost:8080/api/v1/todos', {
headers: {
'X-API-Key': 'todo_your_api_key_here'
}
})
.then(response => response.json())
.then(data => console.log(data));
```
**Create Todo:**
```javascript
fetch('http://localhost:8080/api/v1/todos', {
method: 'POST',
headers: {
'X-API-Key': 'todo_your_api_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'New Task',
status: 'open'
})
})
.then(response => response.json())
.then(data => console.log(data));
```
## Testing
To test the API, you can use tools like:
- Postman
- Insomnia
- cURL
- HTTPie
## Versioning
The API is versioned using the URL path. The current version is `v1`. Future versions will be numbered incrementally (v2, v3, etc.).
## Support
For issues or questions, please refer to the project documentation or contact the development team.

View File

@@ -34,7 +34,11 @@ class Cors extends BaseConfig
* - ['http://localhost:8080'] * - ['http://localhost:8080']
* - ['https://www.example.com'] * - ['https://www.example.com']
*/ */
<<<<<<< HEAD
'allowedOrigins' => ['http://localhost:5173', 'http://127.0.0.1:5173'], 'allowedOrigins' => ['http://localhost:5173', 'http://127.0.0.1:5173'],
=======
'allowedOrigins' => ['http://localhost:5173', 'http://127.0.0.1:5173', 'http://localhost'],
>>>>>>> main
/** /**
* Origin regex patterns for the `Access-Control-Allow-Origin` header. * Origin regex patterns for the `Access-Control-Allow-Origin` header.
@@ -68,7 +72,11 @@ class Cors extends BaseConfig
* *
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers
*/ */
<<<<<<< HEAD
'allowedHeaders' => ['*'], 'allowedHeaders' => ['*'],
=======
'allowedHeaders' => ['Content-Type', 'Authorization', 'X-API-Key'],
>>>>>>> main
/** /**
* Set headers to expose. * Set headers to expose.
@@ -93,7 +101,11 @@ class Cors extends BaseConfig
* *
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods
*/ */
<<<<<<< HEAD
'allowedMethods' => ['*'], 'allowedMethods' => ['*'],
=======
'allowedMethods' => ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
>>>>>>> main
/** /**
* Set how many seconds the results of a preflight request can be cached. * Set how many seconds the results of a preflight request can be cached.

View File

@@ -34,6 +34,7 @@ class Filters extends BaseFilters
'forcehttps' => ForceHTTPS::class, 'forcehttps' => ForceHTTPS::class,
'pagecache' => PageCache::class, 'pagecache' => PageCache::class,
'performance' => PerformanceMetrics::class, 'performance' => PerformanceMetrics::class,
'apiauth' => \App\Filters\ApiAuthFilter::class,
]; ];
/** /**
@@ -72,6 +73,7 @@ class Filters extends BaseFilters
*/ */
public array $globals = [ public array $globals = [
'before' => [ 'before' => [
'cors',
// 'honeypot', // 'honeypot',
// 'csrf', // 'csrf',
// 'invalidchars', // 'invalidchars',

View File

@@ -7,6 +7,92 @@ use CodeIgniter\Router\RouteCollection;
*/ */
$routes->get('/', 'Home::index'); $routes->get('/', 'Home::index');
$routes->get('/themes', 'ThemeStore::index'); $routes->get('/themes', 'ThemeStore::index');
<<<<<<< HEAD
=======
$routes->post('/themes/upload', 'ThemeStore::upload');
$routes->get('/themes/preview/(:segment)', 'ThemeStore::preview/$1');
// ============================================================================
// API Routes - Version 1.0
// ============================================================================
// Catch-all CORS preflight handler for all API routes
$routes->options('api/v1/(:any)', function () {
$response = service('response');
return $response->setStatusCode(200)
->setHeader('Access-Control-Allow-Origin', '*')
->setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
->setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key');
});
// Public endpoints (no authentication required)
$routes->group('api/v1', ['namespace' => 'App\Controllers\Api\V1', 'filter' => 'cors'], function ($routes) {
// Authentication
$routes->options('auth/register', 'AuthController::options');
$routes->post('auth/register', 'AuthController::register');
$routes->options('auth/login', 'AuthController::options');
$routes->post('auth/login', 'AuthController::login');
$routes->options('auth/api-key', 'AuthController::options');
$routes->post('auth/api-key', 'AuthController::createApiKey');
// Marketplace - Public access
$routes->get('marketplace/themes', 'MarketplaceController::index');
$routes->get('marketplace/themes/(:num)', 'MarketplaceController::show/$1');
});
// Protected endpoints (API key authentication required)
$routes->group('api/v1', ['namespace' => 'App\Controllers\Api\V1', 'filter' => ['cors', 'apiauth']], function ($routes) {
// User endpoints
$routes->get('user/profile', 'UserController::profile');
$routes->put('user/profile', 'UserController::updateProfile');
$routes->get('user/api-keys', 'UserController::listApiKeys');
$routes->post('user/api-keys', 'UserController::createApiKey');
$routes->delete('user/api-keys/(:segment)', 'UserController::revokeApiKey/$1');
// Categories
$routes->get('categories', 'CategoryController::index');
$routes->post('categories', 'CategoryController::create');
$routes->get('categories/(:segment)', 'CategoryController::show/$1');
$routes->put('categories/(:segment)', 'CategoryController::update/$1');
$routes->delete('categories/(:segment)', 'CategoryController::delete/$1');
// Projects
$routes->get('projects', 'ProjectController::index');
$routes->post('projects', 'ProjectController::create');
$routes->get('projects/(:segment)', 'ProjectController::show/$1');
$routes->put('projects/(:segment)', 'ProjectController::update/$1');
$routes->delete('projects/(:segment)', 'ProjectController::delete/$1');
// Todos
$routes->get('todos', 'TodoController::index');
$routes->post('todos', 'TodoController::create');
$routes->get('todos/(:segment)', 'TodoController::show/$1');
$routes->put('todos/(:segment)', 'TodoController::update/$1');
$routes->delete('todos/(:segment)', 'TodoController::delete/$1');
$routes->post('todos/(:segment)/categories', 'TodoController::addCategory/$1');
$routes->delete('todos/(:segment)/categories/(:segment)', 'TodoController::removeCategory/$1/$2');
// Recurring Tasks
$routes->get('recurring-tasks', 'RecurringTaskController::index');
$routes->post('recurring-tasks', 'RecurringTaskController::create');
$routes->get('recurring-tasks/(:segment)', 'RecurringTaskController::show/$1');
$routes->put('recurring-tasks/(:segment)', 'RecurringTaskController::update/$1');
$routes->delete('recurring-tasks/(:segment)', 'RecurringTaskController::delete/$1');
$routes->post('recurring-tasks/(:segment)/categories', 'RecurringTaskController::addCategory/$1');
$routes->delete('recurring-tasks/(:segment)/categories/(:segment)', 'RecurringTaskController::removeCategory/$1/$2');
// Activity Logs
$routes->get('activity-logs', 'ActivityLogController::index');
$routes->get('activity-logs/(:segment)', 'ActivityLogController::show/$1');
// User Themes
$routes->get('user/themes', 'UserThemeController::index');
$routes->post('user/themes', 'UserThemeController::create');
$routes->put('user/themes/(:segment)', 'UserThemeController::update/$1');
$routes->delete('user/themes/(:segment)', 'UserThemeController::delete/$1');
});
$routes->get('/themes', 'ThemeStore::index');
>>>>>>> main
$routes->options('/themes', static function () { $routes->options('/themes', static function () {
header('Access-Control-Allow-Origin: http://localhost:5173'); header('Access-Control-Allow-Origin: http://localhost:5173');
header('Access-Control-Allow-Methods: GET, OPTIONS'); header('Access-Control-Allow-Methods: GET, OPTIONS');
@@ -23,8 +109,11 @@ $routes->options('/themes/upload', static function () {
return response()->setStatusCode(204); return response()->setStatusCode(204);
}); });
$routes->get('/themes/preview/(:segment)', 'ThemeStore::preview/$1'); $routes->get('/themes/preview/(:segment)', 'ThemeStore::preview/$1');
<<<<<<< HEAD
$routes->post('/themes/install/(:segment)', 'ThemeStore::install/$1'); $routes->post('/themes/install/(:segment)', 'ThemeStore::install/$1');
$routes->post('/themes/activate/(:segment)', 'ThemeStore::activate/$1'); $routes->post('/themes/activate/(:segment)', 'ThemeStore::activate/$1');
$routes->delete('/themes/uninstall/(:segment)', 'ThemeStore::uninstall/$1'); $routes->delete('/themes/uninstall/(:segment)', 'ThemeStore::uninstall/$1');
$routes->get('/themes/my-themes', 'ThemeStore::myThemes'); $routes->get('/themes/my-themes', 'ThemeStore::myThemes');
$routes->get('/themes/(:segment)', 'ThemeStore::serveCss/$1'); $routes->get('/themes/(:segment)', 'ThemeStore::serveCss/$1');
=======
>>>>>>> main

View File

@@ -0,0 +1,88 @@
<?php
namespace App\Controllers\Api;
use CodeIgniter\RESTful\ResourceController;
class BaseController extends ResourceController
{
/**
* Get the authenticated user from the request
*/
protected function getUser(): ?array
{
return $this->request->user ?? null;
}
/**
* Get the authenticated user ID
*/
protected function getUserId(): ?string
{
$user = $this->getUser();
return $user['id'] ?? null;
}
/**
* Success response
*/
protected function successResponse($data = null, string $message = 'Success', int $statusCode = 200)
{
return $this->response
->setStatusCode($statusCode)
->setHeader('Access-Control-Allow-Origin', '*')
->setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
->setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key')
->setJSON([
'success' => true,
'message' => $message,
'data' => $data,
]);
}
/**
* Error response
*/
protected function errorResponse(string $message, int $statusCode = 400, $errors = null)
{
$response = [
'success' => false,
'message' => $message,
];
if ($errors !== null) {
$response['errors'] = $errors;
}
return $this->response
->setStatusCode($statusCode)
->setHeader('Access-Control-Allow-Origin', '*')
->setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
->setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key')
->setJSON($response);
}
/**
* Validate request data
*/
protected function validateRequest(array $rules): bool
{
$validation = \Config\Services::validation();
// Handle both old format (string) and new format (array with rules/errors)
foreach ($rules as $field => $rule) {
if (is_array($rule) && isset($rule['rules'])) {
$validation->setRules([$field => $rule['rules']], $rule['errors'] ?? []);
} else {
$validation->setRule($field, $field, $rule);
}
}
if (!$validation->withRequest($this->request)->run()) {
$this->errorResponse('Validation failed', 422, $validation->getErrors());
return false;
}
return true;
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Controllers\Api\V1;
use App\Controllers\Api\BaseController;
use App\Models\ActivityLogModel;
class ActivityLogController extends BaseController
{
protected $activityLogModel;
public function __construct()
{
$this->activityLogModel = new ActivityLogModel();
}
/**
* Get activity logs for the authenticated user
* GET /api/v1/activity-logs
*/
public function index()
{
$userId = $this->getUserId();
$limit = (int)($this->request->getVar('limit') ?? 50);
$logs = $this->activityLogModel->getByUser($userId, $limit);
return $this->successResponse($logs, 'Activity logs retrieved successfully');
}
/**
* Get a specific activity log
* GET /api/v1/activity-logs/{id}
*/
public function show($id = null)
{
$userId = $this->getUserId();
$log = $this->activityLogModel->where('id', $id)->where('user_id', $userId)->first();
if (!$log) {
return $this->errorResponse('Activity log not found', 404);
}
return $this->successResponse($log, 'Activity log retrieved successfully');
}
}

View File

@@ -0,0 +1,238 @@
<?php
namespace App\Controllers\Api\V1;
use App\Controllers\Api\BaseController;
use App\Models\UserModel;
use App\Models\ApiAuthKeyModel;
class AuthController extends BaseController
{
protected $userModel;
protected $apiAuthKeyModel;
public function __construct()
{
$this->userModel = new UserModel();
$this->apiAuthKeyModel = new ApiAuthKeyModel();
}
/**
* Handle CORS preflight requests
* OPTIONS /api/v1/auth/*
*/
public function options()
{
return $this->response
->setStatusCode(200)
->setHeader('Access-Control-Allow-Origin', '*')
->setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
->setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key');
}
/**
* Register a new user
* POST /api/v1/auth/register
*/
public function register()
{
$json = $this->request->getJSON(true);
$rules = [
'email' => [
'rules' => 'required|valid_email|is_unique[users.email]',
'errors' => [
'required' => 'Email is required',
'valid_email' => 'Please provide a valid email address',
'is_unique' => 'This email is already registered'
]
],
'password' => [
'rules' => 'required|min_length[8]',
'errors' => [
'required' => 'Password is required',
'min_length' => 'Password must be at least 8 characters long'
]
],
'name' => [
'rules' => 'required|max_length[255]',
'errors' => [
'required' => 'Name is required',
'max_length' => 'Name must not exceed 255 characters'
]
],
];
if (!$this->validateRequest($rules)) {
return;
}
try {
// Generate UUID for user
$userId = $this->generateUuid();
// Create user
$userData = [
'id' => $userId,
'email' => $json['email'],
'password_hash' => password_hash($json['password'], PASSWORD_BCRYPT),
'name' => $json['name'],
'avatar_url' => $json['avatar_url'] ?? null,
'settings' => isset($json['settings']) && $json['settings'] ? json_encode($json['settings']) : json_encode(['theme' => 'light']),
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
];
$this->userModel->insert($userData);
// Create API key for the new user
$apiKey = $this->apiAuthKeyModel->createKey(
$userId,
'Default API Key',
['read', 'write'],
null
);
// Remove sensitive data from response
unset($userData['password_hash']);
return $this->successResponse([
'user' => $userData,
'api_key' => $apiKey['key'],
'key_prefix' => $apiKey['prefix'],
], 'User registered successfully', 201);
} catch (\CodeIgniter\Database\Exceptions\DatabaseException $e) {
return $this->errorResponse('Database error: ' . $e->getMessage(), 500);
} catch (\Exception $e) {
return $this->errorResponse('An error occurred: ' . $e->getMessage(), 500);
}
}
/**
* Login user and return API key
* POST /api/v1/auth/login
*/
public function login()
{
$json = $this->request->getJSON(true);
$rules = [
'email' => 'required|valid_email',
'password' => 'required',
];
if (!$this->validateRequest($rules)) {
return;
}
try {
// Authenticate user
$user = $this->userModel->where('email', $json['email'])->first();
if (!$user || !password_verify($json['password'], $user['password_hash'])) {
return $this->errorResponse('Invalid email or password', 401);
}
// Check if user has an existing active API key
$existingKey = $this->apiAuthKeyModel
->where('user_id', $user['id'])
->where('is_active', true)
->first();
if ($existingKey) {
// Return existing key
return $this->successResponse([
'user' => [
'id' => $user['id'],
'email' => $user['email'],
'name' => $user['name'],
],
'api_key_prefix' => $existingKey['key_prefix'],
'message' => 'Using existing API key',
], 'Login successful');
}
// Create new API key
$apiKey = $this->apiAuthKeyModel->createKey(
$user['id'],
'Login API Key',
['read', 'write'],
null
);
return $this->successResponse([
'user' => [
'id' => $user['id'],
'email' => $user['email'],
'name' => $user['name'],
],
'api_key' => $apiKey['key'],
'key_prefix' => $apiKey['prefix'],
], 'Login successful');
} catch (\CodeIgniter\Database\Exceptions\DatabaseException $e) {
return $this->errorResponse('Database error: ' . $e->getMessage(), 500);
} catch (\Exception $e) {
return $this->errorResponse('An error occurred: ' . $e->getMessage(), 500);
}
}
/**
* Create an API key using email and password (legacy endpoint)
* POST /api/v1/auth/api-key
*/
public function createApiKey()
{
$json = $this->request->getJSON(true);
$rules = [
'email' => 'required|valid_email',
'password' => 'required|min_length[6]',
];
if (!$this->validateRequest($rules)) {
return;
}
// Authenticate user
$user = $this->userModel->where('email', $json['email'])->first();
if (!$user || !password_verify($json['password'], $user['password_hash'])) {
return $this->errorResponse('Invalid email or password', 401);
}
// Create API key
$name = $json['name'] ?? 'API Key';
$scopes = $json['scopes'] ?? ['read', 'write'];
$expiresAt = $json['expires_at'] ?? null;
$apiKey = $this->apiAuthKeyModel->createKey(
$user['id'],
$name,
$scopes,
$expiresAt
);
return $this->successResponse([
'key' => $apiKey['key'],
'prefix' => $apiKey['prefix'],
'name' => $apiKey['name'],
'scopes' => $apiKey['scopes'],
'expires_at' => $apiKey['expires_at'],
], 'API key created successfully');
}
/**
* Generate UUID
*/
private function generateUuid(): string
{
return sprintf(
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);
}
}

View File

@@ -0,0 +1,157 @@
<?php
namespace App\Controllers\Api\V1;
use App\Controllers\Api\BaseController;
use App\Models\CategoryModel;
class CategoryController extends BaseController
{
protected $categoryModel;
public function __construct()
{
$this->categoryModel = new CategoryModel();
}
/**
* Get all categories for the authenticated user
* GET /api/v1/categories
*/
public function index()
{
$userId = $this->getUserId();
$categories = $this->categoryModel->where('user_id', $userId)->findAll();
return $this->successResponse($categories, 'Categories retrieved successfully');
}
/**
* Create a new category
* POST /api/v1/categories
*/
public function create()
{
$userId = $this->getUserId();
$json = $this->request->getJSON(true);
$rules = [
'name' => 'required|max_length[255]',
'color' => 'required|max_length[7]',
];
if (!$this->validateRequest($rules)) {
return;
}
// Check for duplicate name per user
$existing = $this->categoryModel
->where('user_id', $userId)
->where('name', $json['name'])
->first();
if ($existing) {
return $this->errorResponse('A category with this name already exists.', 409);
}
$data = [
'id' => $this->generateUuid(),
'user_id' => $userId,
'name' => $json['name'],
'color' => $json['color'],
'favorite' => $json['favorite'] ?? false,
];
$this->categoryModel->insert($data);
$category = $this->categoryModel->find($data['id']);
return $this->successResponse($category, 'Category created successfully', 201);
}
/**
* Get a specific category
* GET /api/v1/categories/{id}
*/
public function show($id = null)
{
$userId = $this->getUserId();
$category = $this->categoryModel->where('id', $id)->where('user_id', $userId)->first();
if (!$category) {
return $this->errorResponse('Category not found', 404);
}
return $this->successResponse($category, 'Category retrieved successfully');
}
/**
* Update a category
* PUT /api/v1/categories/{id}
*/
public function update($id = null)
{
$userId = $this->getUserId();
$category = $this->categoryModel->where('id', $id)->where('user_id', $userId)->first();
if (!$category) {
return $this->errorResponse('Category not found', 404);
}
$json = $this->request->getJSON(true);
// Check for duplicate name on rename (excluding current category)
if (!empty($json['name']) && strtolower($json['name']) !== strtolower($category['name'])) {
$existing = $this->categoryModel
->where('user_id', $userId)
->where('name', $json['name'])
->where('id !=', $id)
->first();
if ($existing) {
return $this->errorResponse('A category with this name already exists.', 409);
}
}
$allowedFields = ['name', 'color', 'favorite'];
$updateData = array_intersect_key($json, array_flip($allowedFields));
if (empty($updateData)) {
return $this->errorResponse('No valid fields to update');
}
$this->categoryModel->update($id, $updateData);
$category = $this->categoryModel->find($id);
return $this->successResponse($category, 'Category updated successfully');
}
/**
* Delete a category
* DELETE /api/v1/categories/{id}
*/
public function delete($id = null)
{
$userId = $this->getUserId();
$category = $this->categoryModel->where('id', $id)->where('user_id', $userId)->first();
if (!$category) {
return $this->errorResponse('Category not found', 404);
}
$this->categoryModel->delete($id);
return $this->successResponse(null, 'Category deleted successfully');
}
private function generateUuid(): string
{
return sprintf(
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Controllers\Api\V1;
use App\Controllers\Api\BaseController;
use App\Models\MarketplaceThemeModel;
class MarketplaceController extends BaseController
{
protected $marketplaceThemeModel;
public function __construct()
{
$this->marketplaceThemeModel = new MarketplaceThemeModel();
}
/**
* Get all marketplace themes
* GET /api/v1/marketplace/themes
*/
public function index()
{
$themes = $this->marketplaceThemeModel->getPublished();
return $this->successResponse($themes, 'Marketplace themes retrieved successfully');
}
/**
* Get a specific marketplace theme
* GET /api/v1/marketplace/themes/{id}
*/
public function show($id = null)
{
$theme = $this->marketplaceThemeModel->find($id);
if (!$theme) {
return $this->errorResponse('Theme not found', 404);
}
return $this->successResponse($theme, 'Theme retrieved successfully');
}
}

View File

@@ -0,0 +1,133 @@
<?php
namespace App\Controllers\Api\V1;
use App\Controllers\Api\BaseController;
use App\Models\ProjectModel;
class ProjectController extends BaseController
{
protected $projectModel;
public function __construct()
{
$this->projectModel = new ProjectModel();
}
/**
* Get all projects for the authenticated user
* GET /api/v1/projects
*/
public function index()
{
$userId = $this->getUserId();
$projects = $this->projectModel->where('user_id', $userId)->findAll();
return $this->successResponse($projects, 'Projects retrieved successfully');
}
/**
* Create a new project
* POST /api/v1/projects
*/
public function create()
{
$userId = $this->getUserId();
$json = $this->request->getJSON(true);
$rules = [
'name' => 'required|max_length[255]',
'color' => 'required|max_length[7]',
];
if (!$this->validateRequest($rules)) {
return;
}
$data = [
'id' => $this->generateUuid(),
'user_id' => $userId,
'name' => $json['name'],
'description' => $json['description'] ?? null,
'color' => $json['color'],
];
$this->projectModel->insert($data);
$project = $this->projectModel->find($data['id']);
return $this->successResponse($project, 'Project created successfully', 201);
}
/**
* Get a specific project
* GET /api/v1/projects/{id}
*/
public function show($id = null)
{
$userId = $this->getUserId();
$project = $this->projectModel->where('id', $id)->where('user_id', $userId)->first();
if (!$project) {
return $this->errorResponse('Project not found', 404);
}
return $this->successResponse($project, 'Project retrieved successfully');
}
/**
* Update a project
* PUT /api/v1/projects/{id}
*/
public function update($id = null)
{
$userId = $this->getUserId();
$project = $this->projectModel->where('id', $id)->where('user_id', $userId)->first();
if (!$project) {
return $this->errorResponse('Project not found', 404);
}
$json = $this->request->getJSON(true);
$allowedFields = ['name', 'description', 'color'];
$updateData = array_intersect_key($json, array_flip($allowedFields));
if (empty($updateData)) {
return $this->errorResponse('No valid fields to update');
}
$this->projectModel->update($id, $updateData);
$project = $this->projectModel->find($id);
return $this->successResponse($project, 'Project updated successfully');
}
/**
* Delete a project
* DELETE /api/v1/projects/{id}
*/
public function delete($id = null)
{
$userId = $this->getUserId();
$project = $this->projectModel->where('id', $id)->where('user_id', $userId)->first();
if (!$project) {
return $this->errorResponse('Project not found', 404);
}
$this->projectModel->delete($id);
return $this->successResponse(null, 'Project deleted successfully');
}
private function generateUuid(): string
{
return sprintf(
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);
}
}

View File

@@ -0,0 +1,202 @@
<?php
namespace App\Controllers\Api\V1;
use App\Controllers\Api\BaseController;
use App\Models\RecurringTaskModel;
use App\Models\RecurringTaskCategoryModel;
class RecurringTaskController extends BaseController
{
protected $recurringTaskModel;
protected $recurringTaskCategoryModel;
public function __construct()
{
$this->recurringTaskModel = new RecurringTaskModel();
$this->recurringTaskCategoryModel = new RecurringTaskCategoryModel();
}
/**
* Get all recurring tasks for the authenticated user
* GET /api/v1/recurring-tasks
*/
public function index()
{
$userId = $this->getUserId();
$tasks = $this->recurringTaskModel->getByUserWithCategories($userId);
return $this->successResponse($tasks, 'Recurring tasks retrieved successfully');
}
/**
* Create a new recurring task
* POST /api/v1/recurring-tasks
*/
public function create()
{
$userId = $this->getUserId();
$json = $this->request->getJSON(true);
$rules = [
'title' => 'required|max_length[255]',
'schedule' => 'required|in_list[daily,weekly,monthly,custom]',
];
if (!$this->validateRequest($rules)) {
return;
}
$data = [
'id' => $this->generateUuid(),
'user_id' => $userId,
'title' => $json['title'],
'description' => $json['description'] ?? null,
'schedule' => $json['schedule'],
'custom_days' => $json['custom_days'] ? json_encode($json['custom_days']) : json_encode([]),
'favorite' => $json['favorite'] ?? false,
];
$this->recurringTaskModel->insert($data);
$task = $this->recurringTaskModel->getByUserWithCategories($userId, $data['id']);
return $this->successResponse($task, 'Recurring task created successfully', 201);
}
/**
* Get a specific recurring task
* GET /api/v1/recurring-tasks/{id}
*/
public function show($id = null)
{
$userId = $this->getUserId();
$task = $this->recurringTaskModel->getByUserWithCategories($userId, $id);
if (!$task) {
return $this->errorResponse('Recurring task not found', 404);
}
return $this->successResponse($task, 'Recurring task retrieved successfully');
}
/**
* Update a recurring task
* PUT /api/v1/recurring-tasks/{id}
*/
public function update($id = null)
{
$userId = $this->getUserId();
$task = $this->recurringTaskModel->where('id', $id)->where('user_id', $userId)->first();
if (!$task) {
return $this->errorResponse('Recurring task not found', 404);
}
$json = $this->request->getJSON(true);
$allowedFields = ['title', 'description', 'schedule', 'custom_days', 'favorite'];
$updateData = array_intersect_key($json, array_flip($allowedFields));
if (isset($updateData['custom_days'])) {
$updateData['custom_days'] = json_encode($updateData['custom_days']);
}
if (empty($updateData)) {
return $this->errorResponse('No valid fields to update');
}
$this->recurringTaskModel->update($id, $updateData);
$task = $this->recurringTaskModel->getByUserWithCategories($userId, $id);
return $this->successResponse($task, 'Recurring task updated successfully');
}
/**
* Delete a recurring task
* DELETE /api/v1/recurring-tasks/{id}
*/
public function delete($id = null)
{
$userId = $this->getUserId();
$task = $this->recurringTaskModel->where('id', $id)->where('user_id', $userId)->first();
if (!$task) {
return $this->errorResponse('Recurring task not found', 404);
}
$this->recurringTaskModel->delete($id);
return $this->successResponse(null, 'Recurring task deleted successfully');
}
/**
* Add a category to a recurring task
* POST /api/v1/recurring-tasks/{id}/categories
*/
public function addCategory($taskId = null)
{
$userId = $this->getUserId();
$json = $this->request->getJSON(true);
$rules = ['category_id' => 'required'];
if (!$this->validateRequest($rules)) {
return;
}
// Verify task belongs to user
$task = $this->recurringTaskModel->where('id', $taskId)->where('user_id', $userId)->first();
if (!$task) {
return $this->errorResponse('Recurring task not found', 404);
}
// Check if link already exists
$existing = $this->recurringTaskCategoryModel
->where('recurring_task_id', $taskId)
->where('category_id', $json['category_id'])
->first();
if ($existing) {
return $this->errorResponse('Category already linked to this task', 409);
}
$this->recurringTaskCategoryModel->insert([
'recurring_task_id' => $taskId,
'category_id' => $json['category_id'],
]);
return $this->successResponse(null, 'Category added to recurring task successfully', 201);
}
/**
* Remove a category from a recurring task
* DELETE /api/v1/recurring-tasks/{id}/categories/{categoryId}
*/
public function removeCategory($taskId = null, $categoryId = null)
{
$userId = $this->getUserId();
// Verify task belongs to user
$task = $this->recurringTaskModel->where('id', $taskId)->where('user_id', $userId)->first();
if (!$task) {
return $this->errorResponse('Recurring task not found', 404);
}
$this->recurringTaskCategoryModel
->where('recurring_task_id', $taskId)
->where('category_id', $categoryId)
->delete();
return $this->successResponse(null, 'Category removed from recurring task successfully');
}
private function generateUuid(): string
{
return sprintf(
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);
}
}

View File

@@ -0,0 +1,297 @@
<?php
namespace App\Controllers\Api\V1;
use App\Controllers\Api\BaseController;
use App\Models\TodoModel;
use App\Models\TodoCategoryModel;
class TodoController extends BaseController
{
protected $todoModel;
protected $todoCategoryModel;
public function __construct()
{
$this->todoModel = new TodoModel();
$this->todoCategoryModel = new TodoCategoryModel();
}
/**
* Get all todos for the authenticated user
* GET /api/v1/todos
*/
public function index()
{
$userId = $this->getUserId();
$todos = $this->todoModel->getByUserWithCategories($userId);
return $this->successResponse($todos, 'Todos retrieved successfully');
}
/**
* Create a new todo
* POST /api/v1/todos
*/
public function create()
{
$userId = $this->getUserId();
$json = $this->request->getJSON(true);
$rules = [
'title' => 'required|max_length[255]',
'status' => 'permit_empty|in_list[open,in_progress,completed,archived]',
];
if (!$this->validateRequest($rules)) {
return;
}
$data = [
'id' => $this->generateUuid(),
'user_id' => $userId,
'title' => $json['title'],
'description' => $json['description'] ?? null,
'status' => $json['status'] ?? 'open',
'due_date' => $json['due_date'] ?? null,
'due_time' => $json['due_time'] ?? null,
'sync_enabled' => $json['sync_enabled'] ?? true,
'reminder_enabled' => $json['reminder_enabled'] ?? false,
'recurring_enabled' => $json['recurring_enabled'] ?? false,
'project_id' => $json['project_id'] ?? null,
];
$this->todoModel->insert($data);
// Link category if provided
if (!empty($json['category_id'])) {
$this->linkCategory($data['id'], $json['category_id']);
}
// Manually log the activity
try {
$activityLogModel = new \App\Models\ActivityLogModel();
$activityLogModel->logActivity([
'user_id' => $userId,
'action' => 'todo_created',
'entity_type' => 'todo',
'entity_id' => $data['id'],
'details' => json_encode(['action' => 'created', 'title' => $data['title']]),
'ip_address' => $this->request->getIPAddress(),
'user_agent' => $this->request->getUserAgent()->getAgentString(),
]);
} catch (\Exception $e) {
log_message('error', 'Failed to log activity: ' . $e->getMessage());
}
$todos = $this->todoModel->getByUserWithCategories($userId, $data['id']);
return $this->successResponse($todos[0] ?? null, 'Todo created successfully', 201);
}
/**
* Get a specific todo
* GET /api/v1/todos/{id}
*/
public function show($id = null)
{
$userId = $this->getUserId();
$todos = $this->todoModel->getByUserWithCategories($userId, $id);
if (empty($todos)) {
return $this->errorResponse('Todo not found', 404);
}
return $this->successResponse($todos[0], 'Todo retrieved successfully');
}
/**
* Update a todo
* PUT /api/v1/todos/{id}
*/
public function update($id = null)
{
$userId = $this->getUserId();
$todo = $this->todoModel->where('id', $id)->where('user_id', $userId)->first();
if (!$todo) {
return $this->errorResponse('Todo not found', 404);
}
$json = $this->request->getJSON(true);
// Handle category update separately (not a column in todos table)
$hasCategoryUpdate = array_key_exists('category_id', $json);
if ($hasCategoryUpdate) {
$categoryId = $json['category_id'];
// Remove all existing category links
$this->todoCategoryModel->where('todo_id', $id)->delete();
// Link the new category
if (!empty($categoryId)) {
$this->linkCategory($id, $categoryId);
}
}
// Update todo fields
$allowedFields = ['title', 'description', 'status', 'due_date', 'due_time', 'sync_enabled', 'reminder_enabled', 'recurring_enabled', 'project_id'];
$updateData = array_intersect_key($json, array_flip($allowedFields));
if (!empty($updateData)) {
$this->todoModel->update($id, $updateData);
}
// Manually log the activity
try {
$activityLogModel = new \App\Models\ActivityLogModel();
$activityLogModel->logActivity([
'user_id' => $userId,
'action' => 'todo_updated',
'entity_type' => 'todo',
'entity_id' => $id,
'details' => json_encode(['action' => 'updated', 'title' => $todo['title'] ?? 'Unknown']),
'ip_address' => $this->request->getIPAddress(),
'user_agent' => $this->request->getUserAgent()->getAgentString(),
]);
} catch (\Exception $e) {
log_message('error', 'Failed to log activity: ' . $e->getMessage());
}
$updated = $this->todoModel->getByUserWithCategories($userId, $id);
return $this->successResponse($updated[0] ?? null, 'Todo updated successfully');
}
/**
* Delete a todo
* DELETE /api/v1/todos/{id}
*/
public function delete($id = null)
{
$userId = $this->getUserId();
$todo = $this->todoModel->where('id', $id)->where('user_id', $userId)->first();
if (!$todo) {
return $this->errorResponse('Todo not found', 404);
}
$this->todoModel->delete($id);
// Manually log the activity
try {
$activityLogModel = new \App\Models\ActivityLogModel();
$activityLogModel->logActivity([
'user_id' => $userId,
'action' => 'todo_deleted',
'entity_type' => 'todo',
'entity_id' => $id,
'details' => json_encode(['action' => 'deleted', 'title' => $todo['title'] ?? 'Unknown']),
'ip_address' => $this->request->getIPAddress(),
'user_agent' => $this->request->getUserAgent()->getAgentString(),
]);
} catch (\Exception $e) {
log_message('error', 'Failed to log activity: ' . $e->getMessage());
}
return $this->successResponse(null, 'Todo deleted successfully');
}
/**
* Add a category to a todo
* POST /api/v1/todos/{id}/categories
*/
public function addCategory($todoId = null)
{
$userId = $this->getUserId();
$json = $this->request->getJSON(true);
$rules = ['category_id' => 'required'];
if (!$this->validateRequest($rules)) {
return;
}
// Verify todo belongs to user
$todo = $this->todoModel->where('id', $todoId)->where('user_id', $userId)->first();
if (!$todo) {
return $this->errorResponse('Todo not found', 404);
}
// Check if link already exists
$existing = $this->todoCategoryModel
->where('todo_id', $todoId)
->where('category_id', $json['category_id'])
->first();
if ($existing) {
return $this->errorResponse('Category already linked to this todo', 409);
}
$this->todoCategoryModel->insert([
'todo_id' => $todoId,
'category_id' => $json['category_id'],
]);
return $this->successResponse(null, 'Category added to todo successfully', 201);
}
/**
* Remove a category from a todo
* DELETE /api/v1/todos/{id}/categories/{categoryId}
*/
public function removeCategory($todoId = null, $categoryId = null)
{
$userId = $this->getUserId();
// Verify todo belongs to user
$todo = $this->todoModel->where('id', $todoId)->where('user_id', $userId)->first();
if (!$todo) {
return $this->errorResponse('Todo not found', 404);
}
$this->todoCategoryModel
->where('todo_id', $todoId)
->where('category_id', $categoryId)
->delete();
return $this->successResponse(null, 'Category removed from todo successfully');
}
/**
* Link a category to a todo
*/
private function linkCategory(string $todoId, string $categoryId): void
{
$userId = $this->getUserId();
// Verify category belongs to user
$categoryModel = new \App\Models\CategoryModel();
$category = $categoryModel->where('id', $categoryId)->where('user_id', $userId)->first();
if (!$category) {
return;
}
// Check if link already exists
$existing = $this->todoCategoryModel
->where('todo_id', $todoId)
->where('category_id', $categoryId)
->first();
if (!$existing) {
$this->todoCategoryModel->insert([
'todo_id' => $todoId,
'category_id' => $categoryId,
]);
}
}
private function generateUuid(): string
{
return sprintf(
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);
}
}

View File

@@ -0,0 +1,126 @@
<?php
namespace App\Controllers\Api\V1;
use App\Controllers\Api\BaseController;
use App\Models\UserModel;
use App\Models\ApiAuthKeyModel;
class UserController extends BaseController
{
protected $userModel;
protected $apiAuthKeyModel;
public function __construct()
{
$this->userModel = new UserModel();
$this->apiAuthKeyModel = new ApiAuthKeyModel();
}
/**
* Get user profile
* GET /api/v1/user/profile
*/
public function profile()
{
$userId = $this->getUserId();
$user = $this->userModel->find($userId);
if (!$user) {
return $this->errorResponse('User not found', 404);
}
// Remove sensitive data
unset($user['password_hash']);
return $this->successResponse($user, 'Profile retrieved successfully');
}
/**
* Update user profile
* PUT /api/v1/user/profile
*/
public function updateProfile()
{
$userId = $this->getUserId();
$json = $this->request->getJSON(true);
$allowedFields = ['name', 'avatar_url', 'settings'];
$updateData = array_intersect_key($json, array_flip($allowedFields));
if (empty($updateData)) {
return $this->errorResponse('No valid fields to update');
}
$this->userModel->update($userId, $updateData);
$user = $this->userModel->find($userId);
unset($user['password_hash']);
return $this->successResponse($user, 'Profile updated successfully');
}
/**
* List user's API keys
* GET /api/v1/user/api-keys
*/
public function listApiKeys()
{
$userId = $this->getUserId();
$apiKeys = $this->apiAuthKeyModel->getByUser($userId);
// Remove sensitive data
foreach ($apiKeys as &$key) {
unset($key['key_hash']);
}
return $this->successResponse($apiKeys, 'API keys retrieved successfully');
}
/**
* Create a new API key
* POST /api/v1/user/api-keys
*/
public function createApiKey()
{
$userId = $this->getUserId();
$json = $this->request->getJSON(true);
$name = $json['name'] ?? 'API Key';
$scopes = $json['scopes'] ?? ['read', 'write'];
$expiresAt = $json['expires_at'] ?? null;
$apiKey = $this->apiAuthKeyModel->createKey(
$userId,
$name,
$scopes,
$expiresAt
);
return $this->successResponse([
'id' => $apiKey['id'],
'key' => $apiKey['key'],
'prefix' => $apiKey['prefix'],
'name' => $apiKey['name'],
'scopes' => $apiKey['scopes'],
'expires_at' => $apiKey['expires_at'],
], 'API key created successfully');
}
/**
* Revoke an API key
* DELETE /api/v1/user/api-keys/{id}
*/
public function revokeApiKey($id)
{
$userId = $this->getUserId();
$apiKey = $this->apiAuthKeyModel->find($id);
if (!$apiKey || $apiKey['user_id'] !== $userId) {
return $this->errorResponse('API key not found', 404);
}
$this->apiAuthKeyModel->revokeKey($id);
return $this->successResponse(null, 'API key revoked successfully');
}
}

View File

@@ -0,0 +1,120 @@
<?php
namespace App\Controllers\Api\V1;
use App\Controllers\Api\BaseController;
use App\Models\UserThemeModel;
class UserThemeController extends BaseController
{
protected $userThemeModel;
public function __construct()
{
$this->userThemeModel = new UserThemeModel();
}
/**
* Get all themes for the authenticated user
* GET /api/v1/user/themes
*/
public function index()
{
$userId = $this->getUserId();
$themes = $this->userThemeModel->getByUser($userId);
return $this->successResponse($themes, 'User themes retrieved successfully');
}
/**
* Create a new user theme
* POST /api/v1/user/themes
*/
public function create()
{
$userId = $this->getUserId();
$json = $this->request->getJSON(true);
$rules = [
'theme_id' => 'required',
];
if (!$this->validateRequest($rules)) {
return;
}
$data = [
'id' => $this->generateUuid(),
'user_id' => $userId,
'theme_id' => $json['theme_id'],
'is_active' => $json['is_active'] ?? false,
'custom_settings' => $json['custom_settings'] ? json_encode($json['custom_settings']) : null,
];
$this->userThemeModel->insert($data);
$theme = $this->userThemeModel->find($data['id']);
return $this->successResponse($theme, 'User theme created successfully', 201);
}
/**
* Update a user theme
* PUT /api/v1/user/themes/{id}
*/
public function update($id = null)
{
$userId = $this->getUserId();
$theme = $this->userThemeModel->where('id', $id)->where('user_id', $userId)->first();
if (!$theme) {
return $this->errorResponse('User theme not found', 404);
}
$json = $this->request->getJSON(true);
$allowedFields = ['is_active', 'custom_settings'];
$updateData = array_intersect_key($json, array_flip($allowedFields));
if (isset($updateData['custom_settings'])) {
$updateData['custom_settings'] = json_encode($updateData['custom_settings']);
}
if (empty($updateData)) {
return $this->errorResponse('No valid fields to update');
}
$this->userThemeModel->update($id, $updateData);
$theme = $this->userThemeModel->find($id);
return $this->successResponse($theme, 'User theme updated successfully');
}
/**
* Delete a user theme
* DELETE /api/v1/user/themes/{id}
*/
public function delete($id = null)
{
$userId = $this->getUserId();
$theme = $this->userThemeModel->where('id', $id)->where('user_id', $userId)->first();
if (!$theme) {
return $this->errorResponse('User theme not found', 404);
}
$this->userThemeModel->delete($id);
return $this->successResponse(null, 'User theme deleted successfully');
}
private function generateUuid(): string
{
return sprintf(
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateApiAuthKeysTable extends Migration
{
public function up()
{
$this->forge->addField([
'id' => [
'type' => 'CHAR',
'constraint' => 36,
'null' => false,
],
'user_id' => [
'type' => 'CHAR',
'constraint' => 36,
'null' => false,
],
'key_hash' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => false,
'comment' => 'SHA-256 hash of the API key',
],
'key_prefix' => [
'type' => 'VARCHAR',
'constraint' => 20,
'null' => false,
'comment' => 'First 8 characters for identification',
],
'name' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
'comment' => 'User-friendly name for the key',
],
'scopes' => [
'type' => 'JSON',
'null' => true,
'comment' => 'Array of allowed scopes (e.g., ["read", "write"])',
],
'expires_at' => [
'type' => 'DATETIME',
'null' => true,
'comment' => 'Optional expiration date',
],
'last_used_at' => [
'type' => 'DATETIME',
'null' => true,
],
'last_used_ip' => [
'type' => 'VARCHAR',
'constraint' => 45,
'null' => true,
'comment' => 'IPv4 or IPv6 address',
],
'is_active' => [
'type' => 'BOOLEAN',
'default' => true,
],
'created_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addKey('id', true);
$this->forge->addKey('user_id');
$this->forge->addKey('key_hash');
$this->forge->addKey('is_active');
$this->forge->addForeignKey('user_id', 'users', 'id', 'CASCADE', 'CASCADE');
$this->forge->createTable('api_auth_keys');
}
public function down()
{
$this->forge->dropTable('api_auth_keys');
}
}

View File

@@ -309,5 +309,38 @@ class SampleDataSeeder extends Seeder
if (!empty($recurringTaskCategories)) { if (!empty($recurringTaskCategories)) {
$this->db->table('recurring_task_categories')->insertBatch($recurringTaskCategories); $this->db->table('recurring_task_categories')->insertBatch($recurringTaskCategories);
} }
// Create an API key for the demo user
$existingApiKey = $this->db->table('api_auth_keys')
->where('user_id', $userId)
->where('name', 'Demo API Key')
->get()
->getRowArray();
if (!$existingApiKey) {
$apiKey = 'todo_' . bin2hex(random_bytes(32));
$keyHash = hash('sha256', $apiKey);
$keyPrefix = substr($apiKey, 0, 8);
$this->db->table('api_auth_keys')->insert([
'id' => $generateUuid(),
'user_id' => $userId,
'key_hash' => $keyHash,
'key_prefix' => $keyPrefix,
'name' => 'Demo API Key',
'scopes' => json_encode(['read', 'write']),
'expires_at' => null,
'is_active' => true,
'created_at' => date('Y-m-d H:i:s'),
]);
echo "\n========================================\n";
echo "DEMO API KEY CREATED:\n";
echo "========================================\n";
echo "API Key: {$apiKey}\n";
echo "Prefix: {$keyPrefix}\n";
echo "Use this key in the X-API-Key header\n";
echo "========================================\n\n";
}
} }
} }

View File

@@ -0,0 +1,100 @@
<?php
namespace App\Filters;
use CodeIgniter\Filters\FilterInterface;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
class ApiAuthFilter implements FilterInterface
{
/**
* Do whatever processing this filter needs to do.
* By default it should not return anything during
* normal execution. However, when an abnormal state
* is found, it should return an instance of
* CodeIgniter\HTTP\Response. If it does, script
* execution will end and that Response will be
* sent back to the client, allowing for error pages,
* redirects, etc.
*
* @param RequestInterface $request
* @param array|null $arguments
*
* @return mixed
*/
public function before(RequestInterface $request, $arguments = null)
{
$apiKey = $request->getHeaderLine('X-API-Key');
if (empty($apiKey)) {
return $this->unauthorized('API key is required');
}
$apiAuthKeyModel = new \App\Models\ApiAuthKeyModel();
$result = $apiAuthKeyModel->validateKey($apiKey);
if (!$result) {
return $this->unauthorized('Invalid or expired API key');
}
// Store the authenticated user in the request
$request->user = $result['user'];
$request->authKey = $result['auth_key'];
// Check scopes if required
if (!empty($arguments)) {
$requiredScopes = $arguments;
$keyScopes = $result['auth_key']['scopes'] ? json_decode($result['auth_key']['scopes'], true) : [];
if (empty($keyScopes)) {
// No scopes defined, allow all
return;
}
foreach ($requiredScopes as $scope) {
if (!in_array($scope, $keyScopes)) {
return $this->forbidden('Insufficient permissions. Required scope: ' . $scope);
}
}
}
}
/**
* We don't need to do anything here.
*
* @param RequestInterface $request
* @param ResponseInterface $response
* @param array|null $arguments
*
* @return mixed
*/
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
{
// Do nothing
}
/**
* Return unauthorized response
*/
private function unauthorized(string $message): ResponseInterface
{
$response = \Config\Services::response();
return $response->setStatusCode(401)->setJSON([
'error' => 'Unauthorized',
'message' => $message,
]);
}
/**
* Return forbidden response
*/
private function forbidden(string $message): ResponseInterface
{
$response = \Config\Services::response();
return $response->setStatusCode(403)->setJSON([
'error' => 'Forbidden',
'message' => $message,
]);
}
}

View File

@@ -25,7 +25,7 @@ class ActivityLogModel extends Model
protected $useTimestamps = true; protected $useTimestamps = true;
protected $createdField = 'created_at'; protected $createdField = 'created_at';
protected $updatedField = null; protected $updatedField = '';
protected $validationRules = [ protected $validationRules = [
'action' => 'required|max_length[255]', 'action' => 'required|max_length[255]',
@@ -34,9 +34,6 @@ class ActivityLogModel extends Model
// Log an activity // Log an activity
public function logActivity($data) public function logActivity($data)
{ {
// Disable events to prevent any recursive logging
$this->skipEvents();
if (!isset($data['id'])) { if (!isset($data['id'])) {
$data['id'] = $this->generateUuid(); $data['id'] = $this->generateUuid();
} }
@@ -44,12 +41,9 @@ class ActivityLogModel extends Model
$data['created_at'] = date('Y-m-d H:i:s'); $data['created_at'] = date('Y-m-d H:i:s');
} }
$result = $this->insert($data); // Use builder directly to avoid triggering events
$builder = $this->db->table($this->table);
// Re-enable events return $builder->insert($data);
$this->skipEvents(false);
return $result;
} }
// Get logs by user // Get logs by user

View File

@@ -22,7 +22,7 @@ class AiMessageModel extends Model
protected $useTimestamps = true; protected $useTimestamps = true;
protected $createdField = 'created_at'; protected $createdField = 'created_at';
protected $updatedField = null; protected $updatedField = '';
protected $validationRules = [ protected $validationRules = [
'chat_id' => 'required', 'chat_id' => 'required',

View File

@@ -22,7 +22,7 @@ class AiProviderModel extends Model
protected $useTimestamps = true; protected $useTimestamps = true;
protected $createdField = 'created_at'; protected $createdField = 'created_at';
protected $updatedField = null; protected $updatedField = '';
protected $validationRules = [ protected $validationRules = [
'name' => 'required|max_length[100]|is_unique[ai_providers.name]', 'name' => 'required|max_length[100]|is_unique[ai_providers.name]',

View File

@@ -0,0 +1,164 @@
<?php
namespace App\Models;
use CodeIgniter\Model;
class ApiAuthKeyModel extends Model
{
protected $table = 'api_auth_keys';
protected $primaryKey = 'id';
protected $useAutoIncrement = false;
protected $returnType = 'array';
protected $useSoftDeletes = false;
protected $allowedFields = [
'id',
'user_id',
'key_hash',
'key_prefix',
'name',
'scopes',
'expires_at',
'last_used_at',
'last_used_ip',
'is_active',
'created_at',
];
protected $useTimestamps = false;
protected $createdField = 'created_at';
protected $updatedField = '';
protected $validationRules = [
'user_id' => 'required',
'key_hash' => 'required',
'key_prefix' => 'required|max_length[20]',
];
/**
* Generate a new API key
*/
public function generateKey(): string
{
return 'todo_' . bin2hex(random_bytes(32));
}
/**
* Create a new API key for a user
*/
public function createKey(string $userId, ?string $name = null, ?array $scopes = null, ?string $expiresAt = null): array
{
$key = $this->generateKey();
$keyHash = hash('sha256', $key);
$keyPrefix = substr($key, 0, 8);
$data = [
'id' => $this->generateUuid(),
'user_id' => $userId,
'key_hash' => $keyHash,
'key_prefix' => $keyPrefix,
'name' => $name,
'scopes' => $scopes ? json_encode($scopes) : null,
'expires_at' => $expiresAt,
'is_active' => true,
'created_at' => date('Y-m-d H:i:s'),
];
$this->insert($data);
return [
'id' => $data['id'],
'key' => $key,
'prefix' => $keyPrefix,
'name' => $name,
'scopes' => $scopes,
'expires_at' => $expiresAt,
];
}
/**
* Validate an API key and return the associated user
*/
public function validateKey(string $key): ?array
{
$keyHash = hash('sha256', $key);
$authKey = $this->where('key_hash', $keyHash)
->where('is_active', true)
->first();
if (!$authKey) {
return null;
}
// Check if key has expired
if ($authKey['expires_at'] && strtotime($authKey['expires_at']) < time()) {
return null;
}
// Update last used information
$this->update($authKey['id'], [
'last_used_at' => date('Y-m-d H:i:s'),
'last_used_ip' => $this->getClientIp(),
]);
// Get the user
$userModel = new UserModel();
$user = $userModel->find($authKey['user_id']);
if (!$user) {
return null;
}
return [
'user' => $user,
'auth_key' => $authKey,
];
}
/**
* Get all API keys for a user
*/
public function getByUser(string $userId): array
{
return $this->where('user_id', $userId)
->orderBy('created_at', 'DESC')
->findAll();
}
/**
* Revoke an API key
*/
public function revokeKey(string $keyId): bool
{
return $this->update($keyId, ['is_active' => false]);
}
/**
* Get client IP address
*/
private function getClientIp(): ?string
{
try {
$request = \Config\Services::request();
return $request->getIPAddress();
} catch (\Exception $e) {
return null;
}
}
/**
* Generate UUID
*/
private function generateUuid(): string
{
return sprintf(
'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);
}
}

View File

@@ -6,8 +6,6 @@ use CodeIgniter\Model;
class CategoryModel extends Model class CategoryModel extends Model
{ {
use LoggableTrait;
protected $table = 'categories'; protected $table = 'categories';
protected $primaryKey = 'id'; protected $primaryKey = 'id';
protected $useAutoIncrement = false; protected $useAutoIncrement = false;
@@ -24,15 +22,10 @@ class CategoryModel extends Model
protected $useTimestamps = true; protected $useTimestamps = true;
protected $createdField = 'created_at'; protected $createdField = 'created_at';
protected $updatedField = null; protected $updatedField = '';
protected $validationRules = [ protected $validationRules = [
'user_id' => 'required', 'user_id' => 'required',
'name' => 'required|max_length[255]', 'name' => 'required|max_length[255]',
]; ];
protected function getEntityType(): string
{
return 'category';
}
} }

View File

@@ -134,7 +134,7 @@ trait LoggableTrait
{ {
try { try {
$request = \Config\Services::request(); $request = \Config\Services::request();
return $request->getUserAgent()->toString(); return $request->getUserAgent()->getAgentString();
} catch (\Exception $e) { } catch (\Exception $e) {
return 'CLI/Script'; return 'CLI/Script';
} }

View File

@@ -6,8 +6,6 @@ use CodeIgniter\Model;
class ProjectModel extends Model class ProjectModel extends Model
{ {
use LoggableTrait;
protected $table = 'projects'; protected $table = 'projects';
protected $primaryKey = 'id'; protected $primaryKey = 'id';
protected $useAutoIncrement = false; protected $useAutoIncrement = false;
@@ -24,15 +22,10 @@ class ProjectModel extends Model
protected $useTimestamps = true; protected $useTimestamps = true;
protected $createdField = 'created_at'; protected $createdField = 'created_at';
protected $updatedField = null; protected $updatedField = '';
protected $validationRules = [ protected $validationRules = [
'user_id' => 'required', 'user_id' => 'required',
'name' => 'required|max_length[255]', 'name' => 'required|max_length[255]',
]; ];
protected function getEntityType(): string
{
return 'project';
}
} }

View File

@@ -6,8 +6,6 @@ use CodeIgniter\Model;
class RecurringTaskModel extends Model class RecurringTaskModel extends Model
{ {
use LoggableTrait;
protected $table = 'recurring_tasks'; protected $table = 'recurring_tasks';
protected $primaryKey = 'id'; protected $primaryKey = 'id';
protected $useAutoIncrement = false; protected $useAutoIncrement = false;
@@ -35,11 +33,6 @@ class RecurringTaskModel extends Model
'schedule' => 'required|in_list[daily,weekly,monthly,custom]', 'schedule' => 'required|in_list[daily,weekly,monthly,custom]',
]; ];
protected function getEntityType(): string
{
return 'recurring_task';
}
// Get recurring tasks with categories // Get recurring tasks with categories
public function getWithCategories($taskId = null) public function getWithCategories($taskId = null)
{ {

View File

@@ -6,8 +6,6 @@ use CodeIgniter\Model;
class TodoModel extends Model class TodoModel extends Model
{ {
use LoggableTrait;
protected $table = 'todos'; protected $table = 'todos';
protected $primaryKey = 'id'; protected $primaryKey = 'id';
protected $useAutoIncrement = false; protected $useAutoIncrement = false;
@@ -39,11 +37,6 @@ class TodoModel extends Model
'status' => 'permit_empty|in_list[open,in_progress,completed,archived]', 'status' => 'permit_empty|in_list[open,in_progress,completed,archived]',
]; ];
protected function getEntityType(): string
{
return 'todo';
}
// Get todos with categories // Get todos with categories
public function getWithCategories($todoId = null) public function getWithCategories($todoId = null)
{ {
@@ -59,15 +52,23 @@ class TodoModel extends Model
return $builder->get()->getResultArray(); return $builder->get()->getResultArray();
} }
// Get todos by user with categories // Get todos by user with categories (optionally filtered by todo id)
public function getByUserWithCategories($userId) public function getByUserWithCategories($userId, $todoId = null)
{ {
return $this->select('todos.*, GROUP_CONCAT(categories.name) as category_names') $builder = $this->select('
todos.*,
GROUP_CONCAT(DISTINCT categories.id SEPARATOR \',\') as category_ids,
GROUP_CONCAT(DISTINCT categories.name SEPARATOR \', \') as category_names
')
->join('todo_categories', 'todos.id = todo_categories.todo_id', 'left') ->join('todo_categories', 'todos.id = todo_categories.todo_id', 'left')
->join('categories', 'todo_categories.category_id = categories.id', 'left') ->join('categories', 'todo_categories.category_id = categories.id', 'left')
->where('todos.user_id', $userId) ->where('todos.user_id', $userId)
->groupBy('todos.id') ->groupBy('todos.id');
->get()
->getResultArray(); if ($todoId) {
$builder->where('todos.id', $todoId);
}
return $builder->get()->getResultArray();
} }
} }

View File

@@ -6,8 +6,6 @@ use CodeIgniter\Model;
class UserModel extends Model class UserModel extends Model
{ {
use LoggableTrait;
protected $table = 'users'; protected $table = 'users';
protected $primaryKey = 'id'; protected $primaryKey = 'id';
protected $useAutoIncrement = false; protected $useAutoIncrement = false;
@@ -40,9 +38,4 @@ class UserModel extends Model
'is_unique' => 'This email is already registered', 'is_unique' => 'This email is already registered',
], ],
]; ];
protected function getEntityType(): string
{
return 'user';
}
} }

122
public/example_login.html Normal file
View File

@@ -0,0 +1,122 @@
<!DOCTYPE html>
<html>
<head>
<title>Login & Register</title>
</head>
<body>
<h1>Login</h1>
<form id="loginForm">
<div>
<label for="loginEmail">Email:</label>
<input type="email" id="loginEmail" name="email" required>
</div>
<br>
<div>
<label for="loginPassword">Password:</label>
<input type="password" id="loginPassword" name="password" required>
</div>
<br>
<button type="submit">Login</button>
</form>
<hr>
<h1>Register</h1>
<form id="registerForm">
<div>
<label for="regEmail">Email:</label>
<input type="email" id="regEmail" name="email" required>
</div>
<br>
<div>
<label for="regPassword">Password:</label>
<input type="password" id="regPassword" name="password" required>
</div>
<br>
<div>
<label for="regName">Name:</label>
<input type="text" id="regName" name="name" required>
</div>
<br>
<button type="submit">Register</button>
</form>
<h2>Response</h2>
<pre id="response"></pre>
<script>
document.getElementById('loginForm').addEventListener('submit', async function(e) {
e.preventDefault();
const email = document.getElementById('loginEmail').value;
const password = document.getElementById('loginPassword').value;
try {
const response = await fetch('http://localhost:8080/api/v1/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: email,
password: password
})
});
const data = await response.json();
document.getElementById('response').textContent = JSON.stringify(data, null, 2);
if (data.success && data.data.api_key) {
localStorage.setItem('apiKey', data.data.api_key);
alert('API Key saved to localStorage: ' + data.data.api_key);
}
} catch (error) {
document.getElementById('response').textContent = 'Error: ' + error.message;
}
});
document.getElementById('registerForm').addEventListener('submit', async function(e) {
e.preventDefault();
const email = document.getElementById('regEmail').value;
const password = document.getElementById('regPassword').value;
const name = document.getElementById('regName').value;
try {
const response = await fetch('http://localhost:8080/api/v1/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: email,
password: password,
name: name
})
});
const text = await response.text();
let data;
try {
data = JSON.parse(text);
} catch (e) {
data = { error: 'Invalid JSON response', raw: text, status: response.status, statusText: response.statusText };
}
if (!response.ok) {
data = { ...data, httpError: `HTTP ${response.status}: ${response.statusText}` };
}
document.getElementById('response').textContent = JSON.stringify(data, null, 2);
if (data.success && data.data.api_key) {
localStorage.setItem('apiKey', data.data.api_key);
alert('API Key saved to localStorage: ' + data.data.api_key);
}
} catch (error) {
document.getElementById('response').textContent = 'Error: ' + error.message;
}
});
</script>
</body>
</html>

File diff suppressed because one or more lines are too long