implement full backend requirements: pagination, filtering, sorting, meta responses, JWT auth, model validation, request logging, API key management

- BaseController: paginatedResponse() helper with meta (page/perPage/total/lastPage/hasMore), getSortParams(), getFilterParams(), encodeJwt()/decodeJwt(), logActivity() helper, validateWithModel()
- TodoController: paginated/sortable/filterable index, model-based validation, boolean conversion on write, activity logging
- CategoryController: same pagination/sort/filter patterns + duplicate-name check (409)
- ProjectController: paginated index + activity logging
- RecurringTaskController: paginated/sortable/filterable index + junction-table category linking
- AuthController: JWT register/login/refresh endpoints (firebase/php-jwt v7)
- Routes: JWT routes added as public endpoints
- Models: all have proper validationRules with exact error messages (field-level, user-facing)
- ApiAuthFilter: scoped API key auth + UserThemeController generateUuid visibility fix
- composer.json: add firebase/php-jwt ^7.0
This commit is contained in:
Jürg Hallenbarter
2026-05-13 14:54:16 +02:00
parent e125ac34d7
commit 02f77a15a7
15 changed files with 943 additions and 412 deletions

View File

@@ -6,26 +6,38 @@ use CodeIgniter\Model;
class CategoryModel extends Model
{
protected $table = 'categories';
protected $primaryKey = 'id';
protected $table = 'categories';
protected $primaryKey = 'id';
protected $useAutoIncrement = false;
protected $returnType = 'array';
protected $useSoftDeletes = false;
protected $allowedFields = [
'id',
'user_id',
'name',
'color',
'favorite',
'created_at',
protected $returnType = 'array';
protected $useSoftDeletes = false;
protected $allowedFields = [
'id', 'user_id', 'name', 'color', 'favorite', 'created_at',
];
protected $useTimestamps = true;
protected $createdField = 'created_at';
protected $updatedField = '';
protected $createdField = 'created_at';
protected $updatedField = '';
protected $validationRules = [
'user_id' => 'required',
'name' => 'required|max_length[255]',
'user_id' => [
'rules' => 'required',
'errors' => ['required' => 'User ID is required.'],
],
'name' => [
'rules' => 'required|max_length[255]',
'errors' => [
'required' => 'The category name is required.',
'max_length' => 'The category name must not exceed 255 characters.',
],
],
'color' => [
'rules' => 'required|max_length[7]|regex_match[/^#[0-9a-fA-F]{6}$/]',
'errors' => [
'required' => 'A color value is required.',
'max_length' => 'Color must be a hex code (e.g. #3B82F6).',
'regex_match' => 'Color must be a valid hex code (e.g. #3B82F6).',
],
],
];
}

View File

@@ -6,26 +6,30 @@ use CodeIgniter\Model;
class ProjectModel extends Model
{
protected $table = 'projects';
protected $primaryKey = 'id';
protected $table = 'projects';
protected $primaryKey = 'id';
protected $useAutoIncrement = false;
protected $returnType = 'array';
protected $useSoftDeletes = false;
protected $allowedFields = [
'id',
'user_id',
'name',
'description',
'color',
'created_at',
protected $returnType = 'array';
protected $useSoftDeletes = false;
protected $allowedFields = [
'id', 'user_id', 'name', 'description', 'color', 'created_at', 'updated_at',
];
protected $useTimestamps = true;
protected $createdField = 'created_at';
protected $updatedField = '';
protected $createdField = 'created_at';
protected $updatedField = '';
protected $validationRules = [
'user_id' => 'required',
'name' => 'required|max_length[255]',
'user_id' => [
'rules' => 'required',
'errors' => ['required' => 'User ID is required.'],
],
'name' => [
'rules' => 'required|max_length[255]',
'errors' => [
'required' => 'The project name is required.',
'max_length' => 'The project name must not exceed 255 characters.',
],
],
];
}

View File

@@ -6,40 +6,53 @@ use CodeIgniter\Model;
class RecurringTaskModel extends Model
{
protected $table = 'recurring_tasks';
protected $primaryKey = 'id';
protected $table = 'recurring_tasks';
protected $primaryKey = 'id';
protected $useAutoIncrement = false;
protected $returnType = 'array';
protected $useSoftDeletes = false;
protected $allowedFields = [
'id',
'user_id',
'title',
'description',
'schedule',
'custom_days',
'favorite',
'created_at',
'updated_at',
protected $returnType = 'array';
protected $useSoftDeletes = false;
protected $allowedFields = [
'id', 'user_id', 'title', 'description', 'schedule',
'custom_days', 'favorite', 'created_at', 'updated_at',
];
protected $useTimestamps = true;
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
protected $validationRules = [
'user_id' => 'required',
'title' => 'required|max_length[255]',
'schedule' => 'required|in_list[daily,weekly,monthly,custom]',
'user_id' => [
'rules' => 'required',
'errors' => ['required' => 'User ID is required.'],
],
'title' => [
'rules' => 'required|max_length[255]',
'errors' => [
'required' => 'The recurring task title is required.',
'max_length' => 'The title must not exceed 255 characters.',
],
],
'schedule' => [
'rules' => 'permit_empty|in_list[daily,weekly,monthly,custom]',
'errors' => [
'in_list' => 'Schedule must be one of: daily, weekly, monthly, custom.',
],
],
];
// Get recurring tasks with categories
public function getWithCategories($taskId = null)
// ── Queries ────────────────────────────────────────────────────────────
public function getByUserWithCategories($userId, $taskId = null)
{
$builder = $this->select('recurring_tasks.*, GROUP_CONCAT(categories.name) as category_names')
->join('recurring_task_categories', 'recurring_tasks.id = recurring_task_categories.recurring_task_id', 'left')
->join('categories', 'recurring_task_categories.category_id = categories.id', 'left')
->groupBy('recurring_tasks.id');
$builder = $this->select('
recurring_tasks.*,
GROUP_CONCAT(DISTINCT categories.id SEPARATOR \',\') as category_ids,
GROUP_CONCAT(DISTINCT categories.name SEPARATOR \', \') as category_names
')
->join('recurring_task_categories', 'recurring_tasks.id = recurring_task_categories.recurring_task_id', 'left')
->join('categories', 'recurring_task_categories.category_id = categories.id', 'left')
->where('recurring_tasks.user_id', $userId)
->groupBy('recurring_tasks.id');
if ($taskId) {
$builder->where('recurring_tasks.id', $taskId);
@@ -47,16 +60,4 @@ class RecurringTaskModel extends Model
return $builder->get()->getResultArray();
}
// Get recurring tasks by user with categories
public function getByUserWithCategories($userId)
{
return $this->select('recurring_tasks.*, GROUP_CONCAT(categories.name) as category_names')
->join('recurring_task_categories', 'recurring_tasks.id = recurring_task_categories.recurring_task_id', 'left')
->join('categories', 'recurring_task_categories.category_id = categories.id', 'left')
->where('recurring_tasks.user_id', $userId)
->groupBy('recurring_tasks.id')
->get()
->getResultArray();
}
}

View File

@@ -6,53 +6,51 @@ use CodeIgniter\Model;
class TodoModel extends Model
{
protected $table = 'todos';
protected $primaryKey = 'id';
protected $table = 'todos';
protected $primaryKey = 'id';
protected $useAutoIncrement = false;
protected $returnType = 'array';
protected $useSoftDeletes = false;
protected $allowedFields = [
'id',
'user_id',
'title',
'description',
'status',
'due_date',
'due_time',
'sync_enabled',
'reminder_enabled',
'recurring_enabled',
'project_id',
'created_at',
'updated_at',
protected $returnType = 'array';
protected $useSoftDeletes = false;
protected $allowedFields = [
'id', 'user_id', 'title', 'description', 'status',
'due_date', 'due_time', 'sync_enabled', 'reminder_enabled',
'recurring_enabled', 'project_id', 'created_at', 'updated_at',
];
protected $useTimestamps = true;
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
protected $validationRules = [
'user_id' => 'required',
'title' => 'required|max_length[255]',
'status' => 'permit_empty|in_list[open,in_progress,completed,archived]',
'user_id' => [
'rules' => 'required',
'errors' => ['required' => 'User ID is required.'],
],
'title' => [
'rules' => 'required|max_length[255]',
'errors' => [
'required' => 'The todo title is required.',
'max_length' => 'The title must not exceed 255 characters.',
],
],
'status' => [
'rules' => 'permit_empty|in_list[open,in_progress,completed,archived]',
'errors' => [
'in_list' => 'Status must be one of: open, in_progress, completed, archived.',
],
],
'due_date' => [
'rules' => 'permit_empty|valid_date[Y-m-d]',
'errors' => ['valid_date' => 'Due date must be in YYYY-MM-DD format.'],
],
'due_time' => [
'rules' => 'permit_empty|valid_date[H:i:s]',
'errors' => ['valid_date' => 'Due time must be in HH:MM format.'],
],
];
// Get todos with categories
public function getWithCategories($todoId = null)
{
$builder = $this->select('todos.*, GROUP_CONCAT(categories.name) as category_names')
->join('todo_categories', 'todos.id = todo_categories.todo_id', 'left')
->join('categories', 'todo_categories.category_id = categories.id', 'left')
->groupBy('todos.id');
// ── Queries ────────────────────────────────────────────────────────────
if ($todoId) {
$builder->where('todos.id', $todoId);
}
return $builder->get()->getResultArray();
}
// Get todos by user with categories (optionally filtered by todo id)
public function getByUserWithCategories($userId, $todoId = null)
{
$builder = $this->select('

View File

@@ -6,36 +6,35 @@ use CodeIgniter\Model;
class UserModel extends Model
{
protected $table = 'users';
protected $primaryKey = 'id';
protected $table = 'users';
protected $primaryKey = 'id';
protected $useAutoIncrement = false;
protected $returnType = 'array';
protected $useSoftDeletes = false;
protected $allowedFields = [
'id',
'email',
'password_hash',
'name',
'avatar_url',
'settings',
'created_at',
'updated_at',
protected $returnType = 'array';
protected $useSoftDeletes = false;
protected $allowedFields = [
'id', 'email', 'password_hash', 'name', 'avatar_url',
'settings', 'created_at', 'updated_at',
];
protected $useTimestamps = true;
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
protected $validationRules = [
'email' => 'required|valid_email|is_unique[users.email]',
'password_hash' => 'required',
];
protected $validationMessages = [
'email' => [
'required' => 'Email is required',
'valid_email' => 'Please enter a valid email address',
'is_unique' => 'This email is already registered',
'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.',
],
],
'name' => [
'rules' => 'required|max_length[255]',
'errors' => [
'required' => 'Name is required.',
'max_length' => 'Name must not exceed 255 characters.',
],
],
];
}