mirror of
https://github.com/JGH0/Todo-App-Backend.git
synced 2026-06-03 13:28:47 +02:00
added API and login
This commit is contained in:
@@ -34,7 +34,7 @@ class Cors extends BaseConfig
|
||||
* - ['http://localhost:8080']
|
||||
* - ['https://www.example.com']
|
||||
*/
|
||||
'allowedOrigins' => [],
|
||||
'allowedOrigins' => ['*'],
|
||||
|
||||
/**
|
||||
* Origin regex patterns for the `Access-Control-Allow-Origin` header.
|
||||
@@ -68,7 +68,7 @@ class Cors extends BaseConfig
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers
|
||||
*/
|
||||
'allowedHeaders' => [],
|
||||
'allowedHeaders' => ['Content-Type', 'Authorization', 'X-API-Key'],
|
||||
|
||||
/**
|
||||
* Set headers to expose.
|
||||
@@ -93,7 +93,7 @@ class Cors extends BaseConfig
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods
|
||||
*/
|
||||
'allowedMethods' => [],
|
||||
'allowedMethods' => ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
|
||||
/**
|
||||
* Set how many seconds the results of a preflight request can be cached.
|
||||
|
||||
@@ -34,6 +34,7 @@ class Filters extends BaseFilters
|
||||
'forcehttps' => ForceHTTPS::class,
|
||||
'pagecache' => PageCache::class,
|
||||
'performance' => PerformanceMetrics::class,
|
||||
'apiauth' => \App\Filters\ApiAuthFilter::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,3 +6,74 @@ use CodeIgniter\Router\RouteCollection;
|
||||
* @var RouteCollection $routes
|
||||
*/
|
||||
$routes->get('/', 'Home::index');
|
||||
|
||||
// ============================================================================
|
||||
// API Routes - Version 1.0
|
||||
// ============================================================================
|
||||
|
||||
// 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');
|
||||
});
|
||||
|
||||
88
app/Controllers/Api/BaseController.php
Normal file
88
app/Controllers/Api/BaseController.php
Normal 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;
|
||||
}
|
||||
}
|
||||
45
app/Controllers/Api/V1/ActivityLogController.php
Normal file
45
app/Controllers/Api/V1/ActivityLogController.php
Normal 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 = $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');
|
||||
}
|
||||
}
|
||||
238
app/Controllers/Api/V1/AuthController.php
Normal file
238
app/Controllers/Api/V1/AuthController.php
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
133
app/Controllers/Api/V1/CategoryController.php
Normal file
133
app/Controllers/Api/V1/CategoryController.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?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;
|
||||
}
|
||||
|
||||
$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);
|
||||
$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)
|
||||
);
|
||||
}
|
||||
}
|
||||
42
app/Controllers/Api/V1/MarketplaceController.php
Normal file
42
app/Controllers/Api/V1/MarketplaceController.php
Normal 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');
|
||||
}
|
||||
}
|
||||
133
app/Controllers/Api/V1/ProjectController.php
Normal file
133
app/Controllers/Api/V1/ProjectController.php
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
202
app/Controllers/Api/V1/RecurringTaskController.php
Normal file
202
app/Controllers/Api/V1/RecurringTaskController.php
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
202
app/Controllers/Api/V1/TodoController.php
Normal file
202
app/Controllers/Api/V1/TodoController.php
Normal file
@@ -0,0 +1,202 @@
|
||||
<?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);
|
||||
$todo = $this->todoModel->getByUserWithCategories($userId, $data['id']);
|
||||
|
||||
return $this->successResponse($todo, 'Todo created successfully', 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific todo
|
||||
* GET /api/v1/todos/{id}
|
||||
*/
|
||||
public function show($id = null)
|
||||
{
|
||||
$userId = $this->getUserId();
|
||||
$todo = $this->todoModel->getByUserWithCategories($userId, $id);
|
||||
|
||||
if (!$todo) {
|
||||
return $this->errorResponse('Todo not found', 404);
|
||||
}
|
||||
|
||||
return $this->successResponse($todo, '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);
|
||||
$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)) {
|
||||
return $this->errorResponse('No valid fields to update');
|
||||
}
|
||||
|
||||
$this->todoModel->update($id, $updateData);
|
||||
$todo = $this->todoModel->getByUserWithCategories($userId, $id);
|
||||
|
||||
return $this->successResponse($todo, '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);
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
126
app/Controllers/Api/V1/UserController.php
Normal file
126
app/Controllers/Api/V1/UserController.php
Normal 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');
|
||||
}
|
||||
}
|
||||
120
app/Controllers/Api/V1/UserThemeController.php
Normal file
120
app/Controllers/Api/V1/UserThemeController.php
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -309,5 +309,38 @@ class SampleDataSeeder extends Seeder
|
||||
if (!empty($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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
100
app/Filters/ApiAuthFilter.php
Normal file
100
app/Filters/ApiAuthFilter.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
164
app/Models/ApiAuthKeyModel.php
Normal file
164
app/Models/ApiAuthKeyModel.php
Normal 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 = null;
|
||||
|
||||
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)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user