Skip to content

Admin Task Management

Flow ID: AD-55 | Module(s): auth | Complexity: Medium Last Updated: 2026-06-12

Business Context

The admin task management system provides a simple internal to-do feature for logged-in admin users. It lives entirely inside the auth HMVC module and is exposed through three list views (all tasks, assigned to me, created by me), an add/edit form, and a Vue "bell" component in the admin header that surfaces a badge count of overdue tasks.

The feature is meant as a lightweight internal coordination tool — tasks are not visible to storefront customers, are not linked by foreign key to any other business entity (no orders, no customers, no products), and the upstream platform does not ship any notification, reminder, or recurrence mechanism for them. Bulk product import jobs repurpose the table as a cheap notification queue so that, once an import finishes, the admin bell lights up with a summary row.


API Reference

REST Endpoints

No REST API. There is no modern domain layer or REST controller for tasks — a grep of src/Domains/ and src/Rest/ returns only the unrelated DeferredTaskRunner (a post-response work queue). All task CRUD is served by the legacy admin interface.

Legacy Admin Routes

Routes are registered in application/config/routes.php:475-480:

php
$route['auth'] = 'auth';
$route['auth/tasks/(:num)'] = 'auth/tasks/$1';
$route['auth/(.+)'] = 'auth/$1';
$route['(\w{2})/auth'] = 'auth';
$route['(\w{2})/auth/tasks/(:num)'] = 'auth/tasks/$2';
$route['(\w{2})/auth/(.+)'] = 'auth/$2';

The dedicated numeric-offset route for tasks exists so that the (:num) match does not collide with the catch-all (.+) rewrite. All endpoints live on Adv_auth in ecommercen/auth/controllers/Adv_auth.php; the application-level application/modules/auth/controllers/Auth.php:1-7 is an empty subclass.

URLController methodFile:lineHTTPDescription
auth/tasks[/{offset}]tasks($offset = 0)Adv_auth.php:196-228GET (POST for filter)All-tasks list. Supervisor-only (ADVISABLE/ADMIN). NOT filtered to current user.
auth/myTasks[/{offset}]myTasks($offset = 0)Adv_auth.php:230-267GET (POST for filter)Tasks assigned to me (forces assignee_id = uid). Open to any admin; self-scoped.
auth/tasksTo[/{offset}]tasksTo($offset = 0)Adv_auth.php:269-306GET (POST for filter)Tasks created by me (forces creator_id = uid). Open to any admin; self-scoped.
auth/addTaskaddTask()Adv_auth.php:308-351GET form / POST saveFields: assigneeId, dueDate, title, description.
auth/editTask/{id}editTask($taskId)Adv_auth.php:353-401GET form / POST saveOwnership check via canModifyTask() before serving or saving the form.
auth/taskDelete/{id}taskDelete($taskId)Adv_auth.php:403-414POSTInline form + taskCsrf token. authorizeTaskMutation() guard.
auth/taskCompleted/{id}taskCompleted($taskId)Adv_auth.php:416-427POSTInline form + taskCsrf token. authorizeTaskMutation() guard.
auth/taskUncompleted/{id}taskUncompleted($taskId)Adv_auth.php:429-440POSTInline form + taskCsrf token. authorizeTaskMutation() guard.
auth/resetTasksIndexresetTasksIndex()Adv_auth.php:470-475GETClears the tskSearch session key and redirects.
auth/getUserTasksgetUserTasks()Adv_auth.php:477-483GET (AJAX, JSON)Bell-count endpoint; capped at 20 results.

The current stub used to describe /auth/tasks as "tasks created by the current user" — that is wrong. tasks is the all-tasks list, tasksTo is "tasks I created", and myTasks is "tasks assigned to me".


Code Flow

Listing (all three views)

All three list endpoints share the same view (application/views/admin/auth/tasks_list.php) and the same filter pipeline:

Adv_auth::tasks() | myTasks() | tasksTo()
  |
  +--> tasks():    allowRole([ADVISABLE, ADMIN]) → error_401()  (Adv_auth.php:200-202)
  |
  +--> setTaskRedirectUrl('auth/<view>', $offset)         (Adv_auth.php:560-566)
  +--> setSearchTerms()                                    (Adv_auth.php:485-558)
  |     |
  |     +--> reads/writes session key tskSearch
  |
  +--> myTasks:  force $where['assignee_id'] = uid
  |   tasksTo:   force $where['creator_id']  = uid
  |   tasks:     no ownership filter (supervisor-only gate at top)
  |
  +--> Adv_tasks_model::getAll* ($where, $limit, $offset)
  +--> Adv_tasks_model::countAll*                          (used for pagination)
  +--> taskCsrfToken() → $this->render['taskCsrf']        (Adv_auth.php:599-608)
  +--> Render admin/auth/tasks_list

Creating a task (addTask, Adv_auth.php:308-351)

addTask()
  |
  +--> form_validation rules from addTaskValidation() (Adv_auth.php:621-629)
  |     |
  |     +--> only `title` is `required`; all other fields `trim`-only
  |
  +--> if invalid  -> render admin/auth/addTask with errors
  +--> if valid    -> build task data:
  |     assignee_id  = post('assigneeId') ?: session uid
  |     creator_id   = session uid
  |     created_at   = now()
  |     due_date     = post('dueDate') ?: null
  |     title        = post('title')
  |     description  = post('description')
  |
  +--> Adv_tasks_model::addTask($taskData)
  +--> afterAddTask()                                      (Adv_auth.php:683-686 — empty hook)
  +--> redirect to tskPageUrl

Editing (editTask, Adv_auth.php:353-401)

Loads task by id, checks ownership via canModifyTask($task) (Adv_auth.php:363-365) — returns error_401() if the check fails. On POST runs editTaskValidation (Adv_auth.php:638-646), saves via Adv_tasks_model::updateTask($taskId, $taskData), then invokes the empty afterEditTask hook. Supervisor roles (ADVISABLE/ADMIN) may edit any task; other admins may only edit tasks where creator_id or assignee_id matches their session uid.

Complete / uncomplete / delete

All three now require a POST submission carrying the per-session taskCsrf token. They share the authorizeTaskMutation($taskId) guard (Adv_auth.php:448-468) before reaching the model:

authorizeTaskMutation($taskId)         (Adv_auth.php:448-468)
  |
  +--> verifyTaskRequest()             (Adv_auth.php:615-619)
  |     +--> method == 'post'  AND  hash_equals(session taskCsrf, post taskCsrf)
  |     +--> failure → error_401(), return null
  |
  +--> tasks_model->getBy('id', $taskId)
  |     +--> not found → set error, redirect, return null
  |
  +--> canModifyTask($task)            (Adv_auth.php:581-590)
  |     +--> supervisor (ADVISABLE/ADMIN) → true
  |     +--> uid == creator_id || uid == assignee_id → true
  |     +--> otherwise → error_401(), return null
  |
  +--> return $task
MethodFile:lineModel callHook fired
taskDeleteAdv_auth.php:403-414deleteTask($taskId)afterTaskDelete
taskCompletedAdv_auth.php:416-427completeTask($taskId, now)afterTaskDelete (bug — see Known Issues #12)
taskUncompletedAdv_auth.php:429-440unCompleteTask($taskId)afterTaskDelete (bug — see Known Issues #12)

Model operations (ecommercen/auth/models/Adv_tasks_model.php)

MethodDescription
getAll($where, $limit, $offset)All tasks with search/filter.
getAllByAssignee($assigneeId, $where, $limit, $offset)Tasks assigned to a specific user.
getAllByCreated($creatorId, $where, $limit, $offset)Tasks created by a specific user.
addTask($taskData)Insert.
updateTask($taskId, $taskData)Update.
deleteTask($taskId)Hard delete.
completeTask($taskId, $completedAt)Stamps completed_at.
unCompleteTask($taskId)Clears completed_at.
countAll($where)Loads full result set into PHP and returns num_rows() (see Known Issues #13).
fixOrder()Ordering (see Task Lifecycle).
fixWhere($where)Where-clause translation (see Search & Filter).

Domain Layer

None. No src/Domains/**/Task* entity, repository, or service. No src/Rest/**/Task* controller or resource. The only match for "Task" under src/ is src/DeferredTask/DeferredTaskRunner.php, which is unrelated (a post-response work queue). application/config/container/deferred_task.php references that runner, not the admin task list.


Architecture

ComponentPathPurpose
Adv_authecommercen/auth/controllers/Adv_auth.phpHosts all task methods (:196, :230, :269, :308, :353, :403, :416, :429, :470, :477).
Auth (subclass)application/modules/auth/controllers/Auth.php:1-7Empty override shell.
Adv_tasks_modelecommercen/auth/models/Adv_tasks_model.php168-line CRUD model, table tasks, extends Adv_base_model. Loaded in Adv_auth::__construct at Adv_auth.php:8.
Tasks_model (subclass)application/modules/auth/models/Tasks_model.php:1-7Empty override shell.
List viewapplication/views/admin/auth/tasks_list.phpShared by all three list endpoints.
Add viewapplication/views/admin/auth/addTask.phpDatepicker + TinyMCE description + chosen-select assignee.
Edit viewapplication/views/admin/auth/updateTask.phpSame layout as add form.
Routesapplication/config/routes.php:475-480Route definitions.
Admin menuapplication/config/admin_menu.php:357-377Menu entries (see Security gap #2).
Controller baseapplication/core/Admin_c.phpecommercen/core/Adv_admin_controller.php:27-72Enforces "must be logged-in admin" in constructor.

Data Model

Defined only in database/initial/initial.sql:2239-2251. There is no Phinx migration for this table — the misleadingly-named database/migrations/20250324121720_migration_task.php is actually a chmod helper for cache/delete.sh and has nothing to do with tasks.

sql
CREATE TABLE `tasks` (
    `id`           int(11)  NOT NULL AUTO_INCREMENT,
    `creator_id`   int(11)  NOT NULL,
    `assignee_id`  int(11)  NOT NULL,
    `created_at`   datetime NOT NULL,
    `completed_at` datetime DEFAULT NULL,
    `due_date`     datetime DEFAULT NULL,
    `title`        text     NOT NULL,
    `description`  text     DEFAULT NULL,
    PRIMARY KEY (`id`) USING BTREE,
    KEY `creator_id` (`creator_id`) USING BTREE,
    KEY `assignee_id` (`assignee_id`) USING BTREE
) ENGINE = InnoDB DEFAULT CHARSET = utf8;
ColumnTypeNullDefaultNotes
idINT(11) AUTO_INCREMENTNoPrimary key.
creator_idINT(11)NoImplicit FK to users.id (no DB constraint).
assignee_idINT(11)NoImplicit FK to users.id. NOT NULL at schema level, but the controller accepts an empty POST and falls back to the session uid.
created_atDATETIMENoSet explicitly by the app on insert.
completed_atDATETIMEYesNULLNULL = open, any datetime = done.
due_dateDATETIMEYesNULLOptional deadline.
titleTEXTNoStored as TEXT (not VARCHAR).
descriptionTEXTYesNULLPlain text / raw HTML from TinyMCE.

Indexes: PRIMARY on id, non-unique creator_id, non-unique assignee_id. No composite indexes on hot paths (completed_at, due_date).

Companion tables: none. There is no task_assignees, no task_comments, no task_attachments, no tasks_mui. Tasks are single-assignee, single-table, no translations.

Foreign keys: none at the database level. creator_id and assignee_id are bare indexed integer columns; deleting a user from users leaves orphan tasks behind.

Audit trail: none. There is no updated_at, no completed_by_id, no deleted_at, no record of who mutated the row. (The previous version of this document listed an updated_at column — that was incorrect.)


Task Lifecycle

States are binary. A task is either open (completed_at IS NULL) or done (completed_at IS NOT NULL). There is no "in progress", no priority field, no status enum, no assignee change history.

Status filter (inverted!)

Adv_tasks_model::fixWhere() (Adv_tasks_model.php:51-57) translates the session filter value into SQL:

tskSearch['status']SQL appliedMeaning in UI dropdown (tasks_list.php:59-60)
'true'completed_at IS NULLUncompleted / open
'false'completed_at IS NOT NULLCompleted / done
'all' or unsetno filterAll statuses

The inversion is intentional and matches the view dropdown, but it is a trap for anyone reading the model directly.

Overdue detection

  • List view (tasks_list.php:137-142): row is rendered with bg-warning if due_date IS NOT NULL AND now() > due_date AND completed_at IS NULL. Completed tasks get bg-success and win.
  • Vue bell: uses a much looser rule — see TasksBell Vue Widget.

Recurrence / reminders

None. No recurrence_rule column, no cron sweep of the table, no reminder emails, no webhook fired at the due date. Once completed, the row is frozen with a completed_at timestamp.

Ordering

Adv_tasks_model::fixOrder() (Adv_tasks_model.php:24-31) applies:

  1. completed_at IS NOT NULL ASC — open tasks float above closed ones.
  2. completed_at DESC — most recently completed first among closed.
  3. created_at DESC — newest first among open.

Pagination is 20 rows per page (Adv_base_controller::$limit = 20).


Search & Filter

Filter criteria are session-persisted in the key tskSearch via Adv_auth::setSearchTerms() (Adv_auth.php:485-558). The helper is reused across tasks, myTasks, tasksTo, and the Vue bell endpoint getUserTasks — see Bell Count Quirks.

The model translates tskSearch into SQL in Adv_tasks_model::fixWhere() (Adv_tasks_model.php:33-85):

UI fieldtskSearch keyColumnModel behavior
searchTitletitletitleLIKE %value%
(hidden, not exposed)descriptiondescriptionLIKE %value%
creatorId selectcreator_idcreator_idExact match
assigneeId selectassignee_idassignee_idExact match
searchStatus dropdownstatuscompleted_atInverted (see lifecycle)
createdDate datepickercreated_atcreated_atFull-day match
endDate datepickercompleted_atcompleted_atFull-day match
dueDate datepickerdue_datedue_dateFull-day match

Context rules:

  • On myTasks, the assigneeId select is rendered disabled (tasks_list.php:27) and the assignee_id filter is locked to the current user.
  • On tasksTo, the creatorId select is disabled (tasks_list.php:40) and creator_id is locked.
  • The Reset action hits /auth/resetTasksIndex (Adv_auth.php:470-475), which unsets the session key and redirects to the last list URL (tracked via setTaskRedirectUrl, Adv_auth.php:560-566, session key tskPageUrl).

Views

  • List view application/views/admin/auth/tasks_list.php — shared by tasks, myTasks, tasksTo. A single POST form at the top holds all filters. Tasks render in a Bootstrap table with a details modal per row.

    Per-row actions (complete, uncomplete, delete) are inline POST forms (tasks_list.php:246-272) with a taskCsrf hidden field (tasks_list.php:244), matching the modal controls (tasks_list.php:182-209). The edit button is a plain <a> GET anchor (tasks_list.php:262-266), which is correct — edit is a safe GET that redirects to the edit form with its own POST submission.

    The modal delete button (tasks_list.php:182-188) and complete/uncomplete buttons (tasks_list.php:191-209) are also inline POST forms with the same taskCsrf token. No action in the list view reaches the destructive endpoints via GET.

  • Add form application/views/admin/auth/addTask.phpdueDate (datepicker), title, description (TinyMCE mceEditor), assigneeId (chosen-select populated via allowedUsers($users, $roles) from ecommercen/helpers/auth_helper.php:208-215).

  • Edit form application/views/admin/auth/updateTask.php — identical layout.

The description is echoed back into the list via content_shorten($task->description, 400) at tasks_list.php:179 and placed into a title attribute at :226. See security gap #5.


TasksBell Vue Widget

The admin header ships a small Vue 2 + Vuex widget that polls the same backend for a badge count of "overdue" tasks.

Component — assets/admin/js/tasks/TasksBell.vue

vue
<template>
  <a :href="getMyTasksUrl()">
    <i :class="getBellClass()">
      <span>{{ getUserTasksWithDueDateExpired.length }}</span>
    </i>
  </a>
</template>

<script>
import { mapGetters } from 'vuex'

export default {
  name: 'TasksBell',
  props: { siteUrl: String },
  computed: {
    ...mapGetters(['getUserTasksWithDueDateExpired'])
  },
  async mounted () {
    await this.$store.dispatch('fetchAllUserTasks')
  },
  methods: {
    getBellClass () {
      if (this.getUserTasksWithDueDateExpired.length > 0) {
        return 'mdi mdi-bell-ring'
      }
      return 'mdi mdi-bell'
    },
    getMyTasksUrl () { return this.siteUrl + 'auth/myTasks' }
  }
}
</script>

Vuex store — assets/admin/js/tasks/tasks.js

js
const store = new Vuex.Store({
  state: { allUserTasks: {}, namespaced: { namespace: 'tasks' } },
  getters: {
    getAllUserTasks (state) { return state.allUserTasks },
    getUserTasksWithDueDateExpired (state) {
      return filter(state.allUserTasks, e => e.due_date && new Date(e.due_date).getTime() < Date.now())
    }
  },
  mutations: { setAllUserTasks (state, tasksData) { state.allUserTasks = tasksData } },
  actions: {
    async fetchAllUserTasks ({ commit }) {
      try {
        axios.get('/auth/getUserTasks', {}).then((res) => {
          const data = res.data
          commit('setAllUserTasks', data)
        }, (error) => {
          console.log(error)
        })
      } catch (error) {
        console.log(error)
      }
    }
  }
})

new Vue({ el: '#tasks', store, config, locale, components: { TasksBell } })

Mount point — application/views/admin/head.php:75-81

html
<li class="eshopAdminUrl adminTasksUrl admin-user-item" id="tasks" title="My Tasks">
    <div class="dropdown-head">
        <button class="dropbtn-head">
            <tasks-bell :site-url="'<?= site_url(); ?>'"></tasks-bell>
        </button>
    </div>
</li>

Bundle registration

  • webpack.mix.admin.js:85.js('assets/admin/js/tasks/tasks.js', 'public/ui/admin/dist/').vue()
  • application/views/admin/footer_js.php:2994 — bundle <script> include.

Backend endpoint it talks to

Adv_auth::getUserTasks() at Adv_auth.php:477-483 returns JSON. It calls Adv_tasks_model::getAllByAssignee($uid, $where, 20, 0) — hardcoded page size of 20, offset 0, and $where is whatever setSearchTerms() pulled from the tskSearch session key.


Bell Count Quirks

The Vue bell widget has three interacting behavioral quirks that cause the badge count to be unreliable. All three are enumerated as bugs 9-11 in Known Issues & Security Gaps.

The bell also only fetches once, in mounted(). No polling, no push, no websocket — the count only refreshes on a full page navigation.

There is no dropdown preview; clicking the bell simply hard-redirects to /auth/myTasks via siteUrl + 'auth/myTasks'.


Known Issues & Security Gaps

These gaps are serious enough to treat as a dedicated section rather than a TODO list.

  1. [FIXED #21] Per-endpoint RBAC — all-tasks list now gated; myTasks/tasksTo intentionally open but self-scoped. Before commit 0260023895, the only gate on any task endpoint was the constructor "must be logged-in admin" check. tasks() now opens with allowRole([AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN], ...) → error_401() (Adv_auth.php:200-202), matching the admin-menu gating. myTasks and tasksTo remain open to any admin role — they are self-scoped to the session uid by forcing assignee_id/creator_id filters, so they cannot expose another user's data. Related open tracker: #66 (menu gating — the menu entry hide for auth/tasks is still cosmetic security even though the endpoint is now gated; a direct URL still reaches it for non-ADMIN roles, which is the intended new behaviour, but the menu logic and the endpoint RBAC still do not perfectly mirror each other).

  2. Menu-level gating is cosmetic security. application/config/admin_menu.php:357-377 shows auth/tasks only to [AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN]. The menu and the endpoint now agree on who can access the all-tasks list. However, auth/myTasks and auth/tasksTo are still exposed to everyone in the menu (empty roles array) and the endpoint, which is correct by design (self-scoped). Menu gating is not a security boundary in any case — see #66.

  3. [FIXED #22] Ownership check on edit, delete, complete, and uncomplete. Before commit 0260023895, none of these endpoints compared creator_id/assignee_id to the session uid. editTask now calls canModifyTask($task) (Adv_auth.php:363-365) immediately after loading the task, returning error_401() on failure. taskDelete, taskCompleted, and taskUncompleted all reach canModifyTask through the shared authorizeTaskMutation() guard (Adv_auth.php:448-468). A supervisor role (ADVISABLE/ADMIN) may modify any task; other admins are restricted to tasks where their uid matches creator_id or assignee_id (Adv_auth.php:581-590).

  4. [FIXED #23] Destructive actions now require POST + per-session CSRF token. Before commit 0260023895, taskDelete, taskCompleted, and taskUncompleted were GET routes reachable via plain <a> anchor tags — a single <img src="/auth/taskDelete/42"> was enough to destroy a task. These three methods now pass through authorizeTaskMutation(), which calls verifyTaskRequest() (Adv_auth.php:615-619): the request must be POST and carry a taskCsrf field matching the per-session token generated by taskCsrfToken() (Adv_auth.php:599-608). The token is lazily generated as bin2hex(random_bytes(16)), stored in session key taskCsrf, and passed to views as $this->render['taskCsrf']. All three list views render it as a hidden field in inline forms. Note: csrf_protection remains globally false in application/config/config.php:131; the taskCsrf mechanism is a task-specific synchronizer token, not a framework-level CSRF guard.

  5. Raw TinyMCE HTML in description. The controller saves post('description') untouched and the list view renders it via content_shorten($task->description, 400) (tasks_list.php:179) and into a title HTML attribute at :226. Stored XSS surface — cross-reference #24 (deferred; HTMLPurifier dependency vs escape-on-output trade-off still open).

  6. NOT NULL violations possible under edge cases. addTask writes assignee_id = (!empty(post('assigneeId'))) ? post('assigneeId') : session->userdata('uid') (Adv_auth.php:325-327). assignee_id is NOT NULL at the schema level, but if the session uid is missing and the POST field is empty, the insert attempts to write an empty string into a NOT NULL INT column. There is no server-side check that the POSTed assigneeId corresponds to an existing user.

  7. No audit trail, no soft delete. There is no updated_at, no completed_by_id, no deleted_at, no log of who mutated the task. Once a task is deleted it is gone with no trace of which admin clicked the button.

  8. No notifications of any kind. Assignment changes send no email, fire no in-app notification, and do not update the target user's bell until the target's browser performs a full page navigation. The afterAddTask, afterEditTask, and afterTaskDelete hooks at Adv_auth.php:678-691 are empty stubs explicitly designed for client repos to override — the upstream platform ships zero notification implementation.

  9. Bell: completed tasks still count. The getter never checks completed_at, so a completed task still contributes to the badge count until the next full page navigation drops it out of the top 20.

  10. Bell: hard cap of 20 silently clamps. The JSON endpoint getUserTasks calls getAllByAssignee(uid, where, 20, 0) (Adv_auth.php:477-483) — the bell can never show more than 20 even if the user has hundreds of overdue tasks.

  11. Bell: polluted by the last list filter. getUserTasks routes through the same setSearchTerms() helper as the three list views, so the tskSearch session key is shared (Adv_auth.php:485-558). If the admin just browsed myTasks with status filter set to "completed", the bell query inherits that filter and counts only completed tasks.

  12. Copy-paste hook bug: taskCompleted and taskUncompleted fire afterTaskDelete. Both taskCompleted (Adv_auth.php:416-427) and taskUncompleted (Adv_auth.php:429-440) call afterTaskDelete instead of dedicated afterTaskCompleted/afterTaskUncompleted hooks. There are no such hooks — they were never created. This bug predates commit 0260023895 and remains open.

  13. countAll uses num_rows() instead of COUNT(*). Adv_tasks_model::countAll() (Adv_tasks_model.php:98-104) loads the full result set into PHP and returns num_rows(). This is a performance anti-pattern that scales poorly with table size.

Form validation

addTaskValidation() (Adv_auth.php:621-629) and editTaskValidation() (Adv_auth.php:638-646) mark only title as required; everything else is trim-only. There is no min/max length check, no assignee-existence check, and no due-date format validation.

Open trackers

  • #24 — stored XSS via description (deferred, see gap #5)
  • #66 — menu gating vs endpoint RBAC alignment
  • #311 — split task management out of Adv_auth into its own controller

Bulk Import Jobs as Notification Queue

Outside the admin UI, four bulk product import jobs write rows directly into the tasks table and use it as a poor-man's notification queue. In every case creator_id = assignee_id = customerId and due_date = date('Y-m-d H:i:s') (i.e. now), so the bell lights up the instant the job finishes.

Job fileLineTitle/description source
ecommercen/job/libraries/AdvInsertProductsFromFile.php:49-56Import summary (counts of inserted/failed products).
ecommercen/job/libraries/AdvUpdateProductsFromFileByBarcode.php:49Same pattern.
ecommercen/job/libraries/AdvUpdateProductsFromFileByProductCode.php:47Same pattern.
ecommercen/job/libraries/AdvUploadImagesFromZipFile.php:33-40Hardcoded Greek title/description at :30-31 — not translated.

Because the generated due_date is always "now", these rows feed directly into the bell's overdue count the moment they land.

Dead load: ecommercen/audit/controllers/Adv_audit.php:10 loads tasks_model in its constructor but never calls it — leftover copy-paste, not an actual integration point.


Cross-References to Business Entities

Tasks are not linked to any other business entity. There is no FK (or implicit column) pointing at orders, customers, products, categories, or suppliers. Tasks cannot be used as "follow-up reminders for order X" or "review this product" — the feature is strictly a freeform to-do list for admin users. If a client needs task-to-entity linking it must be added via a migration in the client repo.


Configuration

  • Routes: application/config/routes.php:475-480.
  • Admin menu: application/config/admin_menu.php:357-377 — three entries ("All Tasks", "My Tasks", "Tasks I Created"). See security gap #2 for why the per-entry role arrays are now mostly aligned but still not a security boundary.
  • Required roles: tasks() requires ADVISABLE or ADMIN role (Adv_auth.php:200-202). myTasks() and tasksTo() are open to any authenticated admin but self-scope to the session uid. Destructive mutations (taskDelete, taskCompleted, taskUncompleted) additionally require POST + taskCsrf token and pass an ownership check.
  • Session keys used by task endpoints: tskSearch (filter criteria), tskPageUrl (redirect target), taskCsrf (per-session mutation token, Adv_auth.php:599-608).

Client Extension Points

  • Override controller: extend Auth in application/modules/auth/controllers/Auth.php (main repo) or a client-repo equivalent. The empty subclass at application/modules/auth/controllers/Auth.php:1-7 exists specifically for this.
  • Override model: extend Tasks_model at application/modules/auth/models/Tasks_model.php:1-7.
  • Post-mutation hooks: override afterAddTask, afterEditTask, afterTaskDelete from Adv_auth.php:678-691. Note the copy-paste bug — taskCompleted and taskUncompleted both fire afterTaskDelete, so a client override will run for completions as well as deletions unless the client also overrides the controller methods directly.
  • canModifyTask / verifyTaskRequest: both are protected methods on Adv_auth (Adv_auth.php:581-590 and :615-619). A client subclass may override them to apply stricter or different ownership rules.

Business Rules

  1. Three views, one template. All three list endpoints share tasks_list.php; only the forced where-clause and disabled filter dropdown differ.
  2. Binary lifecycle. Open (completed_at IS NULL) or done (completed_at IS NOT NULL). No priority, no in-progress state.
  3. Session-persisted filters. Search/filter criteria live in the session key tskSearch, shared across the three list views and the Vue bell endpoint.
  4. Session-persisted redirect. tskPageUrl remembers the last list URL so write endpoints return the admin to the correct paginated view.
  5. myTasks and tasksTo both require a valid session uid. Both methods (Adv_auth.php:232-236, :271-275) redirect with an error if the session uid is missing.
  6. Bell badge only refreshes on full navigation. The Vue widget fetches once on mounted() — there is no polling or push.
  7. Supervisor override. A user with role ADVISABLE or ADMIN may edit or delete any task, regardless of creator_id/assignee_id. Any other admin role is restricted to tasks they created or are assigned to (Adv_auth.php:581-590).
  8. Destructive actions require POST + token. taskDelete, taskCompleted, and taskUncompleted reject GET requests and requests with a missing or mismatched taskCsrf field (Adv_auth.php:615-619).

Tests

tests/Legacy/Auth/AdvAuthTaskSecurityTest.php covers the two new security predicates added in commit 0260023895 (#21/#22/#23). Tests use reflection on a constructor-less Adv_auth instance with injected stub session and input objects.

canModifyTask() cases (#22)

TestScenarioExpected
supervisor_can_modify_any_taskuid=99, role=ADMIN, task creator=1/assignee=2true
creator_can_modify_own_taskuid=5, non-supervisor role, creator_id=5true
assignee_can_modify_own_taskuid=7, non-supervisor role, assignee_id=7true
unrelated_admin_cannot_modify_taskuid=8, non-supervisor role, creator=1/assignee=2false

verifyTaskRequest() cases (#23)

TestScenarioExpected
verify_passes_for_post_with_matching_tokenPOST, session token = posted tokentrue
verify_fails_for_get_requestGET, token present but method wrongfalse
verify_fails_for_post_with_wrong_tokenPOST, token mismatchfalse
verify_fails_for_post_with_no_tokenPOST, no taskCsrf in bodyfalse

Coverage gaps

  • The tasks() RBAC gate (Adv_auth.php:200-202) is not unit-tested — it exercises allowRole() + error_401() interaction that requires a more integrated harness.
  • authorizeTaskMutation() orchestration (verifyTaskRequest → load task → canModifyTask) is not unit-tested as a whole; only its constituent predicates are covered.

  • AD-01 Admin Auth — admin authentication and the uid/role model that every task endpoint reads from.

No other flow is related — tasks have no connection to orders, customers, products, or any other business domain.