Appearance
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.
| URL | Controller method | File:line | HTTP | Description |
|---|---|---|---|---|
auth/tasks[/{offset}] | tasks($offset = 0) | Adv_auth.php:196-228 | GET (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-267 | GET (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-306 | GET (POST for filter) | Tasks created by me (forces creator_id = uid). Open to any admin; self-scoped. |
auth/addTask | addTask() | Adv_auth.php:308-351 | GET form / POST save | Fields: assigneeId, dueDate, title, description. |
auth/editTask/{id} | editTask($taskId) | Adv_auth.php:353-401 | GET form / POST save | Ownership check via canModifyTask() before serving or saving the form. |
auth/taskDelete/{id} | taskDelete($taskId) | Adv_auth.php:403-414 | POST | Inline form + taskCsrf token. authorizeTaskMutation() guard. |
auth/taskCompleted/{id} | taskCompleted($taskId) | Adv_auth.php:416-427 | POST | Inline form + taskCsrf token. authorizeTaskMutation() guard. |
auth/taskUncompleted/{id} | taskUncompleted($taskId) | Adv_auth.php:429-440 | POST | Inline form + taskCsrf token. authorizeTaskMutation() guard. |
auth/resetTasksIndex | resetTasksIndex() | Adv_auth.php:470-475 | GET | Clears the tskSearch session key and redirects. |
auth/getUserTasks | getUserTasks() | Adv_auth.php:477-483 | GET (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_listCreating 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 tskPageUrlEditing (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| Method | File:line | Model call | Hook fired |
|---|---|---|---|
taskDelete | Adv_auth.php:403-414 | deleteTask($taskId) | afterTaskDelete |
taskCompleted | Adv_auth.php:416-427 | completeTask($taskId, now) | afterTaskDelete (bug — see Known Issues #12) |
taskUncompleted | Adv_auth.php:429-440 | unCompleteTask($taskId) | afterTaskDelete (bug — see Known Issues #12) |
Model operations (ecommercen/auth/models/Adv_tasks_model.php)
| Method | Description |
|---|---|
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
| Component | Path | Purpose |
|---|---|---|
Adv_auth | ecommercen/auth/controllers/Adv_auth.php | Hosts all task methods (:196, :230, :269, :308, :353, :403, :416, :429, :470, :477). |
Auth (subclass) | application/modules/auth/controllers/Auth.php:1-7 | Empty override shell. |
Adv_tasks_model | ecommercen/auth/models/Adv_tasks_model.php | 168-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-7 | Empty override shell. |
| List view | application/views/admin/auth/tasks_list.php | Shared by all three list endpoints. |
| Add view | application/views/admin/auth/addTask.php | Datepicker + TinyMCE description + chosen-select assignee. |
| Edit view | application/views/admin/auth/updateTask.php | Same layout as add form. |
| Routes | application/config/routes.php:475-480 | Route definitions. |
| Admin menu | application/config/admin_menu.php:357-377 | Menu entries (see Security gap #2). |
| Controller base | application/core/Admin_c.php → ecommercen/core/Adv_admin_controller.php:27-72 | Enforces "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;| Column | Type | Null | Default | Notes |
|---|---|---|---|---|
id | INT(11) AUTO_INCREMENT | No | — | Primary key. |
creator_id | INT(11) | No | — | Implicit FK to users.id (no DB constraint). |
assignee_id | INT(11) | No | — | Implicit FK to users.id. NOT NULL at schema level, but the controller accepts an empty POST and falls back to the session uid. |
created_at | DATETIME | No | — | Set explicitly by the app on insert. |
completed_at | DATETIME | Yes | NULL | NULL = open, any datetime = done. |
due_date | DATETIME | Yes | NULL | Optional deadline. |
title | TEXT | No | — | Stored as TEXT (not VARCHAR). |
description | TEXT | Yes | NULL | Plain 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 applied | Meaning in UI dropdown (tasks_list.php:59-60) |
|---|---|---|
'true' | completed_at IS NULL | Uncompleted / open |
'false' | completed_at IS NOT NULL | Completed / done |
'all' or unset | no filter | All 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 withbg-warningifdue_date IS NOT NULL AND now() > due_date AND completed_at IS NULL. Completed tasks getbg-successand 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:
completed_at IS NOT NULLASC — open tasks float above closed ones.completed_at DESC— most recently completed first among closed.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 field | tskSearch key | Column | Model behavior |
|---|---|---|---|
searchTitle | title | title | LIKE %value% |
| (hidden, not exposed) | description | description | LIKE %value% |
creatorId select | creator_id | creator_id | Exact match |
assigneeId select | assignee_id | assignee_id | Exact match |
searchStatus dropdown | status | completed_at | Inverted (see lifecycle) |
createdDate datepicker | created_at | created_at | Full-day match |
endDate datepicker | completed_at | completed_at | Full-day match |
dueDate datepicker | due_date | due_date | Full-day match |
Context rules:
- On
myTasks, theassigneeIdselect is rendered disabled (tasks_list.php:27) and theassignee_idfilter is locked to the current user. - On
tasksTo, thecreatorIdselect is disabled (tasks_list.php:40) andcreator_idis 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 viasetTaskRedirectUrl,Adv_auth.php:560-566, session keytskPageUrl).
Views
List view
application/views/admin/auth/tasks_list.php— shared bytasks,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 ataskCsrfhidden 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 sametaskCsrftoken. No action in the list view reaches the destructive endpoints via GET.Add form
application/views/admin/auth/addTask.php—dueDate(datepicker),title,description(TinyMCEmceEditor),assigneeId(chosen-selectpopulated viaallowedUsers($users, $roles)fromecommercen/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.
[FIXED #21] Per-endpoint RBAC — all-tasks list now gated;
myTasks/tasksTointentionally open but self-scoped. Before commit0260023895, the only gate on any task endpoint was the constructor "must be logged-in admin" check.tasks()now opens withallowRole([AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN], ...) → error_401()(Adv_auth.php:200-202), matching the admin-menu gating.myTasksandtasksToremain open to any admin role — they are self-scoped to the session uid by forcingassignee_id/creator_idfilters, so they cannot expose another user's data. Related open tracker: #66 (menu gating — the menu entry hide forauth/tasksis 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).Menu-level gating is cosmetic security.
application/config/admin_menu.php:357-377showsauth/tasksonly to[AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN]. The menu and the endpoint now agree on who can access the all-tasks list. However,auth/myTasksandauth/tasksToare 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.[FIXED #22] Ownership check on edit, delete, complete, and uncomplete. Before commit
0260023895, none of these endpoints comparedcreator_id/assignee_idto the sessionuid.editTasknow callscanModifyTask($task)(Adv_auth.php:363-365) immediately after loading the task, returningerror_401()on failure.taskDelete,taskCompleted, andtaskUncompletedall reachcanModifyTaskthrough the sharedauthorizeTaskMutation()guard (Adv_auth.php:448-468). A supervisor role (ADVISABLE/ADMIN) may modify any task; other admins are restricted to tasks where their uid matchescreator_idorassignee_id(Adv_auth.php:581-590).[FIXED #23] Destructive actions now require POST + per-session CSRF token. Before commit
0260023895,taskDelete,taskCompleted, andtaskUncompletedwere 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 throughauthorizeTaskMutation(), which callsverifyTaskRequest()(Adv_auth.php:615-619): the request must be POST and carry ataskCsrffield matching the per-session token generated bytaskCsrfToken()(Adv_auth.php:599-608). The token is lazily generated asbin2hex(random_bytes(16)), stored in session keytaskCsrf, and passed to views as$this->render['taskCsrf']. All three list views render it as a hidden field in inline forms. Note:csrf_protectionremains globallyfalseinapplication/config/config.php:131; thetaskCsrfmechanism is a task-specific synchronizer token, not a framework-level CSRF guard.Raw TinyMCE HTML in
description. The controller savespost('description')untouched and the list view renders it viacontent_shorten($task->description, 400)(tasks_list.php:179) and into atitleHTML attribute at:226. Stored XSS surface — cross-reference #24 (deferred; HTMLPurifier dependency vs escape-on-output trade-off still open).NOT NULL violations possible under edge cases.
addTaskwritesassignee_id = (!empty(post('assigneeId'))) ? post('assigneeId') : session->userdata('uid')(Adv_auth.php:325-327).assignee_idis NOT NULL at the schema level, but if the sessionuidis 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 POSTedassigneeIdcorresponds to an existing user.No audit trail, no soft delete. There is no
updated_at, nocompleted_by_id, nodeleted_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.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, andafterTaskDeletehooks atAdv_auth.php:678-691are empty stubs explicitly designed for client repos to override — the upstream platform ships zero notification implementation.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.Bell: hard cap of 20 silently clamps. The JSON endpoint
getUserTaskscallsgetAllByAssignee(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.Bell: polluted by the last list filter.
getUserTasksroutes through the samesetSearchTerms()helper as the three list views, so thetskSearchsession key is shared (Adv_auth.php:485-558). If the admin just browsedmyTaskswith status filter set to "completed", the bell query inherits that filter and counts only completed tasks.Copy-paste hook bug:
taskCompletedandtaskUncompletedfireafterTaskDelete. BothtaskCompleted(Adv_auth.php:416-427) andtaskUncompleted(Adv_auth.php:429-440) callafterTaskDeleteinstead of dedicatedafterTaskCompleted/afterTaskUncompletedhooks. There are no such hooks — they were never created. This bug predates commit0260023895and remains open.countAllusesnum_rows()instead ofCOUNT(*).Adv_tasks_model::countAll()(Adv_tasks_model.php:98-104) loads the full result set into PHP and returnsnum_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_authinto 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 file | Line | Title/description source |
|---|---|---|
ecommercen/job/libraries/AdvInsertProductsFromFile.php | :49-56 | Import summary (counts of inserted/failed products). |
ecommercen/job/libraries/AdvUpdateProductsFromFileByBarcode.php | :49 | Same pattern. |
ecommercen/job/libraries/AdvUpdateProductsFromFileByProductCode.php | :47 | Same pattern. |
ecommercen/job/libraries/AdvUploadImagesFromZipFile.php | :33-40 | Hardcoded 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()andtasksTo()are open to any authenticated admin but self-scope to the session uid. Destructive mutations (taskDelete,taskCompleted,taskUncompleted) additionally require POST +taskCsrftoken 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
Authinapplication/modules/auth/controllers/Auth.php(main repo) or a client-repo equivalent. The empty subclass atapplication/modules/auth/controllers/Auth.php:1-7exists specifically for this. - Override model: extend
Tasks_modelatapplication/modules/auth/models/Tasks_model.php:1-7. - Post-mutation hooks: override
afterAddTask,afterEditTask,afterTaskDeletefromAdv_auth.php:678-691. Note the copy-paste bug —taskCompletedandtaskUncompletedboth fireafterTaskDelete, so a client override will run for completions as well as deletions unless the client also overrides the controller methods directly. canModifyTask/verifyTaskRequest: both areprotectedmethods onAdv_auth(Adv_auth.php:581-590and:615-619). A client subclass may override them to apply stricter or different ownership rules.
Business Rules
- Three views, one template. All three list endpoints share
tasks_list.php; only the forced where-clause and disabled filter dropdown differ. - Binary lifecycle. Open (
completed_at IS NULL) or done (completed_at IS NOT NULL). No priority, no in-progress state. - Session-persisted filters. Search/filter criteria live in the session key
tskSearch, shared across the three list views and the Vue bell endpoint. - Session-persisted redirect.
tskPageUrlremembers the last list URL so write endpoints return the admin to the correct paginated view. myTasksandtasksToboth require a valid session uid. Both methods (Adv_auth.php:232-236, :271-275) redirect with an error if the sessionuidis missing.- Bell badge only refreshes on full navigation. The Vue widget fetches once on
mounted()— there is no polling or push. - 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). - Destructive actions require POST + token.
taskDelete,taskCompleted, andtaskUncompletedreject GET requests and requests with a missing or mismatchedtaskCsrffield (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)
| Test | Scenario | Expected |
|---|---|---|
supervisor_can_modify_any_task | uid=99, role=ADMIN, task creator=1/assignee=2 | true |
creator_can_modify_own_task | uid=5, non-supervisor role, creator_id=5 | true |
assignee_can_modify_own_task | uid=7, non-supervisor role, assignee_id=7 | true |
unrelated_admin_cannot_modify_task | uid=8, non-supervisor role, creator=1/assignee=2 | false |
verifyTaskRequest() cases (#23)
| Test | Scenario | Expected |
|---|---|---|
verify_passes_for_post_with_matching_token | POST, session token = posted token | true |
verify_fails_for_get_request | GET, token present but method wrong | false |
verify_fails_for_post_with_wrong_token | POST, token mismatch | false |
verify_fails_for_post_with_no_token | POST, no taskCsrf in body | false |
Coverage gaps
- The
tasks()RBAC gate (Adv_auth.php:200-202) is not unit-tested — it exercisesallowRole()+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.
Related Flows
- 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.