Appearance
Admin User Management
Flow ID: AD-58 | Module(s): auth | Complexity: Medium Last Updated: 2026-04-21
Business Context
Admin user management covers the full lifecycle of back-office user accounts: creating users, assigning roles, editing credentials, soft-deleting and restoring accounts, and allowing users to change their own passwords. The platform ships a dual-path implementation — a legacy CodeIgniter controller that powers the admin panel UI, and a modern domain + REST layer introduced in the feature/admin-users-domain branch (merged 2026-04-08).
Roles are database-driven as of release 4.99.10. Nine roles are stored in a roles DB table and referenced by the user_role pivot table through a MANY_TO_MANY relation. Role constants are still defined in application/config/constants.php for backward compatibility. Non-advisable administrators cannot see or assign the Advisable (1) or Developer (2) roles.
The flow has a deliberate RBAC split: all user-management actions (list, create, edit, delete, undelete, admin-initiated password reset) require ADVISABLE or ADMIN role, while self-service password change is available to any authenticated backend user.
API Reference
REST Endpoints (Modern Layer)
application/config/rest_routes.php:1662-1693
| Method | Path | Controller Method | Auth | Min Role |
|---|---|---|---|---|
| GET | /rest/admin/users | User::index() | backend JWT | ADMIN |
| GET | /rest/admin/users/item | User::item() | backend JWT | ADMIN |
| GET | /rest/admin/users/{id} | User::show() | backend JWT | ADMIN |
| POST | /rest/admin/users | User::store() | backend JWT | ADMIN |
| POST | /rest/admin/users/{id} | User::update() | backend JWT | ADMIN |
| POST | /rest/admin/users/{id}/undelete | User::undelete() | backend JWT | ADMIN |
| POST | /rest/admin/users/{id}/password | User::resetPassword() | backend JWT | ADMIN |
| POST | /rest/admin/users/change-password | User::changePassword() | backend JWT | any |
| DELETE | /rest/admin/users/{id} | User::destroy() | backend JWT | ADMIN |
| GET | /rest/admin/me | Me::show() | backend JWT | any |
| GET | /rest/admin/roles | Role::index() | backend JWT | ADMIN |
| GET | /rest/admin/roles/item | Role::item() | backend JWT | ADMIN |
| GET | /rest/admin/roles/{id} | Role::show() | backend JWT | ADMIN |
All endpoints have locale-prefixed variants (e.g., /el/rest/admin/users). The change-password static path is declared before the (:num) wildcard to prevent routing ambiguity (application/config/rest_routes.php:1666,1670). ADVISABLE users bypass role checks entirely via AuthorizationMiddleware superuser detection (src/Rest/Middleware/AuthorizationMiddleware.php:69).
REST policies are declared in application/config/rest_policies.php:659-669:
AdminUser::classdefault:backendauth +[AUTH_ROLE_ADMIN]AdminUser::classchangePasswordaction:backendauth +[](any backend user)AdminRole::classdefault:backendauth +[AUTH_ROLE_ADMIN]AdminMe::classdefault:backendauth +[](any backend user)
Legacy Admin Routes
application/config/routes.php:477-482
| Route | Controller | Notes |
|---|---|---|
auth | Adv_auth | Explicit route to index() |
auth/tasks/(:num) | Adv_auth | Explicit route for task detail (see AD-55) |
auth/(.+) | Adv_auth | Wildcard — all sub-actions (add, edit, delete, undelete, changePassword, changeMyPassword, etc.) are handled by CI default routing through this pattern |
(\w{2})/auth | Adv_auth | Locale-prefixed index |
(\w{2})/auth/(.+) | Adv_auth | Locale-prefixed wildcard for all sub-actions |
Per-action RBAC is enforced inside the controller methods, not by the route table. add(), edit(), delete(), undelete(), and changePassword() call allowRole([AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN]); changeMyPassword() carries no RBAC check.
Admin menu entries:
auth— group SETTINGS, roles[AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN](application/config/admin_menu.php:143-149)auth/changeMyPassword— group PROFILE, roles[AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN, AUTH_ROLE_ORDERS](application/config/admin_menu.php:378-384)
Code Flow
User Creation (Modern Path)
POST /rest/admin/users
└─ User::store() [src/Rest/Admin/Controllers/User.php]
└─ doStore()
└─ WriteService::create(array $data, bool $isAdvisable = false): ?Entity
|-- Validator::validateForCreate($writeData)
| |-- username unique check (DB)
| |-- password min 8 chars
| └─ role IDs exist in Role\Service (was: RoleProvider — deprecated in 4.99.10)
|-- Validator::validateRoleAuthorization($roleIds, $isAdvisable)
| └─ non-advisable cannot assign role 1 or 2
|-- AuthHashService::hash($password) → bcrypt
|-- WriteRepository::insert($data)
|-- WriteRepository::syncRoles($userId, $roleIds)
| |-- load current role IDs from user_role
| |-- compute toAdd / toRemove delta
| |-- DELETE removed rows, INSERT added rows
| └─ (diff-based, same logic as legacy updateUserRoles)
└─ Repository::get($id, ['roles']) → returns {id, username, softDelete, roles[{id, name, home}]}User Creation (Legacy Path)
POST auth/add
└─ Adv_auth::add() [ecommercen/auth/controllers/Adv_auth.php:52-81]
|-- allowRole([AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN])
|-- addValidation(): password required|trim|min_length[6], username is_unique, roles required
|-- Adv_users_model::addUser($userData, $roleIds) [ecommercen/auth/models/Adv_users_model.php:15]
| └─ internally calls $this->user_role_model->insertUserRoles() [line 25]
| └─ delegates to Adv_user_role_model::updateUserRoles() (diff-based sync)
└─ afterAdd($userId) [extension hook — empty by default]Self-Service Password Change (Modern Path)
POST /rest/admin/users/change-password
└─ User::changePassword() [src/Rest/Admin/Controllers/User.php]
└─ WriteService::changeOwnPassword($userId, $currentPwd, $newPwd)
|-- AuthHashService::verify($currentPwd, storedHash) → bcrypt verify
|-- Validator: new password min 8 chars
└─ WriteRepository::updatePassword($userId, hash($newPwd))Admin Profile Self-Service
src/Rest/Admin/Controllers/Me.php
GET /rest/admin/me
└─ Me::show() [src/Rest/Admin/Controllers/Me.php]
|-- guard: resourceContext missing or !isBackend() → 401
|-- userId = resourceContext->getUserId()
|-- guard: !$userId → 401
|-- if is_numeric($userId):
| |-- service->get($userId, parsedRelations) [Admin\User\Service]
| └─ 404 if null (deleted user), else view($entity) → AdminUserResource {id, username, softDelete, roles}
└─ if !is_numeric (config user, e.g. "advisable"):
|-- roleIds = resourceContext->getRoles() (from JWT)
|-- foreach roleId: roleService->get($roleId) [Admin\Role\Service]
└─ sendOutput {id: null, username, softDelete: false, roles[{id, name|null}], isConfigUser: true}The $id parameter in show(int|string $id = 0) is always ignored — identity is taken from resourceContext->getUserId(). The with query param (e.g. ?with=roles) is forwarded to the service call for DB users. For config users no DB lookup is performed; role names are resolved one-by-one via Admin\Role\Service::get($roleId) from the role IDs embedded in the JWT. A role ID that no longer exists in DB yields name: null in the response. POST /me is intentionally absent — username and password changes have dedicated endpoints (POST /rest/admin/users/{id} and POST /rest/admin/users/change-password).
Soft Delete with Self-Delete Guard (Modern Path)
DELETE /rest/admin/users/{id}
└─ User::destroy() [src/Rest/Admin/Controllers/User.php:246-260]
|-- if $id === $this->resourceContext->getUserId(): return 403
└─ WriteService::delete($id)
└─ WriteRepository::softDelete($id) [sets soft_delete = 1]The legacy Adv_auth::delete($id) (ecommercen/auth/controllers/Adv_auth.php:123-135) performs the same soft-delete but without the self-delete guard.
Role List (Modern Path)
GET /rest/admin/roles
└─ Role::index() [src/Rest/Admin/Controllers/Role.php:53-60]
|-- if non-advisable: filter out role IDs 1 (ADVISABLE) and 2 (DEVELOPER)
└─ Role\Service::all(ListRequest) [src/Domains/Admin/Role/]
|-- Role\Repository::match() queries `roles` table
└─ return [{id, name}, ...]Note: Prior to 4.99.10, RoleProvider::getAll(includeHidden:) served this path. RoleProvider is now deprecated (see Domain Layer).
Data Model
users table
database/initial/initial.sql:2336-2345
| Column | Type | Notes |
|---|---|---|
id | INT PK AI | User ID |
username | VARCHAR(50) UNIQUE | Login name |
password | VARCHAR(255) | Bcrypt hash |
soft_delete | TINYINT(1) DEFAULT 0 | 0 = active, 1 = deleted |
Indexes: PRIMARY KEY on id, UNIQUE on username, KEY on soft_delete, composite KEY on (username, soft_delete). Foreign keys: none.
user_role pivot table
database/initial/initial.sql:2347-2353
| Column | Type | Notes |
|---|---|---|
user_id | INT | References users.id (no FK constraint) |
role_id | INT | References roles.id (no FK constraint) |
Indexes: UNIQUE composite on (user_id, role_id), KEY on user_id, KEY on role_id. Foreign key: role_id → roles.id (added in migration 20260409131959); user_id has no FK constraint — orphan rows possible on user deletion (see Known Issues).
As of 4.99.10 the User RepositoryConfigurator uses a MANY_TO_MANY relation through user_role to the roles table. Prior to 4.99.10 it used a ONE_TO_MANY relation to the UserRole pivot entity only.
roles table (added in 4.99.10)
database/migrations/20260409131959_create_roles_table.php:42-50
| Column | Type | Notes |
|---|---|---|
id | INT PK | Role ID (matches existing AUTH_ROLE_* constants) |
name | VARCHAR(50) UNIQUE | Human-readable role name |
home | VARCHAR(100) NULL | Default redirect path after login for this role |
The nine pre-existing roles were seeded into this table as part of the #166 migration. Role constants in application/config/constants.php:99-107 remain as aliases for backward compatibility.
Roles — database-driven (as of 4.99.10)
application/config/constants.php:99-107
| Constant | Value | Name |
|---|---|---|
AUTH_ROLE_ADVISABLE | 1 | Advisable (hidden from non-advisable users) |
AUTH_ROLE_DEVELOPER | 2 | Developer (hidden from non-advisable users) |
AUTH_ROLE_ADMIN | 3 | Admin |
AUTH_ROLE_CMS | 4 | CMS |
AUTH_ROLE_PRODUCTS | 5 | Products |
AUTH_ROLE_ORDERS | 6 | Orders |
AUTH_ROLE_REPORTING | 7 | Reporting |
AUTH_ROLE_MARKETING | 8 | Marketing |
AUTH_ROLE_MEDIA | 9 | Media |
Role names and home URLs are now read from the roles DB table. The auth_roles config key has been removed from application/config/auth.php. RoleProvider (DI-injected config array) is deprecated — superseded by Role\Service and Role\Repository.
Related tables (documented elsewhere)
tasks(database/initial/initial.sql:2239-2251) — see AD-55 Task Managementuser_audit_logs(database/migrations/20250509080101_audit_log.php:12-28) — see AD-32 Audit Logging
Domain Layer
Modern Domain (src/Domains/Admin/)
| File | Lines | Responsibility |
|---|---|---|
src/Domains/Admin/User/Repository/Entity.php | 16 | Entity: id, username, password, soft_delete |
src/Domains/Admin/User/Repository/Repository.php | 16 | BaseRepository for users table |
src/Domains/Admin/User/Repository/RepositoryConfigurator.php | 23 | MANY_TO_MANY relation through user_role pivot to roles table (changed in 4.99.10; was ONE_TO_MANY to UserRole entity) |
src/Domains/Admin/User/Repository/WriteRepository.php | 61 | insert(), update(), syncRoles(), deleteRoles(), updatePassword(), softDelete(), undelete() |
src/Domains/Admin/User/Service.php | 80 | Read service; enrichRoles() removed in 4.99.10 — the MANY_TO_MANY relation loader now hydrates full Role entities directly |
src/Domains/Admin/User/WriteService.php | 104 | create(), update(), delete(), softDelete(), undelete(), resetPassword(), changeOwnPassword() |
src/Domains/Admin/User/Validator.php | 124 | Username uniqueness, password ≥ 8 chars, role existence, role authorization |
src/Domains/Admin/User/WriteData.php | 56 | DTO: username, password, roleIds. toArray() excludes password and roleIds |
src/Domains/Admin/User/ListRequest.php | 30 | Filters: id (exact), username (partial), softDelete (exact). Sorts: id, username |
src/Domains/Admin/UserRole/Repository/Entity.php | 14 | Pivot entity: user_id, role_id |
src/Domains/Admin/UserRole/Repository/Repository.php | 17 | BaseRepository for user_role. Uses NullRelationConfigurator |
src/Domains/Admin/Role/RoleProvider.php | 57 | Deprecated in 4.99.10. Was config-driven: getAll(), getById(), exists(), resolveRoleIds(). Superseded by Role\Service + Role\Repository reading from the roles DB table |
src/Domains/Admin/Role/Repository/ | — | New in 4.99.10: Entity, Repository, RepositoryConfigurator for the roles table |
src/Domains/Admin/Role/Service.php | — | New in 4.99.10: replaces RoleProvider; reads from DB, filters hidden roles |
src/Domains/Admin/container.php | 28 | DI registration; RoleProvider config injection removed in 4.99.10 — use_sections bug also resolved (see Known Issues) |
Support layer:
| File | Lines | Responsibility |
|---|---|---|
src/Domains/Support/Auth/AuthHashService.php | 29 | Wraps legacy Advauth for hash() and verify(); isolates CI dependency from modern domain |
REST Layer (src/Rest/Admin/)
| File | Lines | Responsibility |
|---|---|---|
src/Rest/Admin/Controllers/User.php | 392 | Full CRUD + undelete(), resetPassword(), changePassword(). Uses HandlesRestfulActions + HandlesWriteActions. Overrides doStore()/doUpdate() to pass isAdvisable flag. Self-delete guard in destroy() |
src/Rest/Admin/Controllers/Role.php | 112 | Read-only role list. Filters hidden roles for non-advisable callers. index() + item() + show() |
src/Rest/Admin/Controllers/Me.php | ~95 | GET-only self-service profile. Guards on isBackend(). Handles DB users (service lookup) and config users (synthesized from JWT + role name lookup via Admin\Role\Service). POST /me intentionally absent. |
src/Rest/Admin/Resources/User/Resource.php | 48 | Transforms to {id, username, softDelete, roles[{id, name}]}. Schema name: AdminUserResource |
src/Rest/Admin/Resources/User/Collection.php | 16 | Collection wrapper |
src/Rest/Admin/container.php | — | DI registration for REST controllers, including Me controller wired with Admin\User\Service, Admin\Role\Service, AdminUserResource, AdminUserCollection, and Admin\User\ListRequest (src/Rest/Admin/container.php) |
DI module registration:
- Domain:
application/config/container/modules.php:29 - REST:
application/config/container/modules.php:50
Legacy Layer (ecommercen/auth/)
| File | Lines | Responsibility |
|---|---|---|
ecommercen/auth/controllers/Adv_auth.php | 598 | User management (lines 25-194) + task management (lines 195+, see AD-55). 8 empty extension hooks (lines 559-598). Extends Admin_c |
ecommercen/auth/models/Adv_users_model.php | 179 | addUser(), editUser(), updatePassword(), deleteUser() (soft), undeleteUser(), getByUsername(), getAll(), getUsersAdmin(). guardUserEdit() strips soft_delete and password from update payloads |
ecommercen/auth/models/Adv_user_role_model.php | 135 | getUserRoles(), getUserRolesIds(), getUsersRolesIds() (bulk), updateUserRoles() (diff-based sync) |
ecommercen/auth/models/Adv_roles_model.php | 90 | Role model — now reads from the roles DB table (fixed in 4.99.10; was config-driven with no DB table). dropdown(), getRoles(), getRole($roleId), getRolesByIds(), getRolesByName(). Note: getRolesByName() null-crash bug resolved (#175) |
application/libraries/Advauth.php | 207 | login() (session-based), statelessAdminLogin() (REST JWT), advAuthHash() (bcrypt), advAuthVerify() |
ecommercen/helpers/auth_helper.php | — | allowRole($roles, $roleIds) — legacy RBAC check; mergeUsersRoles() — attaches role arrays to user objects |
Configuration
Auth config (application/config/auth.php, 66 lines)
| Key | Value | Purpose |
|---|---|---|
simple_auth | true | Enable config-based static users |
db_auth | true | Enable database user authentication |
auth_hash | 'password-hash-mcrypt' | Hashing algorithm (bcrypt in practice) |
config_auth | array | Static users loaded from env vars |
auth_roles | (removed in 4.99.10) | Was: nine roles with name and home keys. Role data now lives in the roles DB table |
Environment variables
The four static-auth env vars (APP_ADMIN_USERNAME, APP_ADMIN_PWD, APP_ADMIN_ADVISABLE_USERNAME, APP_ADMIN_ADVISABLE_PWD) are used by the login flow and are documented in full in (see AD-01 Admin Auth & SSO for static config-auth env vars and how they feed into Advauth::login()).
Scribe How (user tour)
ecommercen/language/english/adv_scribe_lang.php:19,120
- Title key:
scribe.how.settings.users.title= "Χρήστες | Προσθήκη users & tasks" (Greek label;ecommercen/language/english/adv_scribe_lang.php:19) - Iframe link key:
scribe.how.settings.users.iframe.link
Client Extension Points
Legacy hooks in Adv_auth
ecommercen/auth/controllers/Adv_auth.php:559-598 — eight empty protected methods intended for client-repo overrides:
| Method | Trigger |
|---|---|
afterAdd($userId) | After user created |
afterEdit($userId) | After user edited |
afterDelete($userId) | After user soft-deleted |
afterUndelete($userId) | After user restored |
afterChangePassword($userId) | After admin password reset |
afterAddTask($taskId) | After task created (see AD-55) |
afterEditTask($taskId) | After task edited (see AD-55) |
afterTaskDelete($taskId) | After task deleted (see AD-55) |
Client repos override these methods by extending Adv_auth in application/controllers/.
Modern REST layer
src/Rest/Admin/Controllers/User.php — doStore() and doUpdate() pass the isAdvisable flag derived from the JWT token, enabling client repos to adjust behavior per request context without modifying validation logic.
Business Rules
ADVISABLE or ADMIN required for all user management actions. Every legacy CRUD action calls
allowRole([AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN]). REST policies enforce the same viarest_policies.php:659-669. (ecommercen/auth/controllers/Adv_auth.php:28,55,86,126,140,153)Any authenticated backend user may change their own password.
changeMyPassword()has no RBAC guard in the legacy path (ecommercen/auth/controllers/Adv_auth.php:176); the RESTchange-passwordendpoint policy specifies[](any backend user) (application/config/rest_policies.php:663).
2a. Any authenticated backend user may read their own profile via GET /rest/admin/me. Config users (defined in config_auth in application/config/auth.php) are fully supported — their profile is synthesized from the JWT payload rather than a DB lookup; id is null and isConfigUser: true in the response. The in-controller isBackend() guard provides a second enforcement layer beyond the policy (src/Rest/Admin/Controllers/Me.php).
Non-advisable admins cannot see or assign roles 1 and 2. Legacy: role dropdown filters them in
defaultRender()(ecommercen/auth/controllers/Adv_auth.php:12-23). Modern:Validatorblocks assignment (src/Domains/Admin/User/Validator.php:70-83);Role::index()controller (previouslyRoleProvider::getAll(includeHidden: false)) filters them from the list (src/Rest/Admin/Controllers/Role.php:53-60).Soft delete, not hard delete.
deleteUser()setssoft_delete = 1(ecommercen/auth/models/Adv_users_model.php);WriteRepository::softDelete()does the same (src/Domains/Admin/User/Repository/WriteRepository.php). Records are recoverable viaundelete.Users cannot delete themselves (REST only).
User::destroy()compares the target$idagainst the current user's JWT identity and returns 403 on match (src/Rest/Admin/Controllers/User.php:246-260). The legacy controller does not enforce this guard.Role sync is diff-based. Both legacy
updateUserRoles()and modernWriteRepository::syncRoles()compute the delta between current and desired role IDs, then insert additions and delete removals atomically. (ecommercen/auth/models/Adv_user_role_model.php:108-134,src/Domains/Admin/User/Repository/WriteRepository.php:12-37)Password hashing algorithm is bcrypt.
auth_hash = 'password-hash-mcrypt'maps to bcrypt inAdvauth::advAuthHash()(application/libraries/Advauth.php:168-187). Delegated toAuthHashServicein the modern path (src/Domains/Support/Auth/AuthHashService.php:20-23).Password minimum length differs by path. Legacy: 6 characters (
ecommercen/auth/controllers/Adv_auth.php:556). Modern: 8 characters (src/Domains/Admin/User/Validator.php:31,87). See Known Issues.Self-service password change requires current password verification.
WriteService::changeOwnPassword()verifies the current password viaAuthHashService::verify()before hashing and persisting the new one (src/Domains/Admin/User/WriteService.php:88-103).Roles are resolved via MANY_TO_MANY relation — full
Roleentities are hydrated. As of 4.99.10 the User RepositoryConfigurator uses a MANY_TO_MANY relation throughuser_roleto therolestable. The relation loader hydrates complete{id, name, home}Role entities.Service::enrichRoles()has been removed; it was a workaround for the former absence of a DB roles table (resolved in #166).Role data is database-driven. Role names and home paths are now read from the
rolesDB table viaRole\Repository. Changes to roles take effect immediately without a container cache clear. Theauth_rolesconfig key andRoleProviderinjection have been removed fromsrc/Domains/Admin/container.php.guardUserEdit()protects sensitive fields on update. The model stripssoft_deleteandpasswordfrom raw update payloads so callers cannot accidentally overwrite them via a bulk edit (ecommercen/auth/models/Adv_users_model.php:82-93).
Known Issues & Security Gaps
user_role.user_idhas no FK tousers.id— orphan rows are possible on user deletion.role_idgained a FK constraint (role_id → roles.id) in migration20260409131959, so role-side orphans are now prevented. However, deleting a user directly leaves orphan pivot rows becauseuser_idremains unconstrained. (database/initial/initial.sql:2347-2353,database/migrations/20260409131959_create_roles_table.php)Password minimum-length mismatch: 6 (legacy) vs 8 (modern). A password that satisfies the legacy form but fails the REST validator (or vice versa) creates user confusion and potential inconsistency between creation paths. (
ecommercen/auth/controllers/Adv_auth.php:556,src/Domains/Admin/User/Validator.php:31,87)Config-auth password comparison uses plain-text
==. Static users defined inconfig_authare verified with a direct string equality check, bypassing theadvAuthVerify()bcrypt path. (application/libraries/Advauth.php:79,134)changeMyPasswordmenu entry has overly restrictive role list. The admin menu shows the "Change My Password" link only for ADVISABLE, ADMIN, and ORDERS users (application/config/admin_menu.php:382), but the endpoint itself accepts any backend user. CMS, Products, Reporting, Marketing, and Media users can access the endpoint but cannot discover it via the menu.Legacy
delete()has no self-delete guard. An ADMIN or ADVISABLE user can soft-delete their own account via the legacy UI path (ecommercen/auth/controllers/Adv_auth.php:123-135). The modern REST path blocks this (src/Rest/Admin/Controllers/User.php:246-260).(Resolved in 4.99.10 —Adv_roles_model::getRolesByName()always returns an empty array.Adv_roles_modelnow reads from therolesDB table; null-crash and dead-return bugs fixed as part of #175.)No explicit CSRF protection on legacy user management forms. CodeIgniter 3 CSRF protection must be enabled globally; there is no controller-level evidence of per-form CSRF tokens for the auth user management pages. (
ecommercen/auth/controllers/Adv_auth.php:52-194)Roles are not persisted to the database — tracked as GitHub issue #166.(Resolved in 4.99.10 — roles migrated to therolesDB table;Service::enrichRoles()removed; MANY_TO_MANY relation hydrates full Role entities.)(Resolved in 4.99.10 —container.phpuse_sections = truemisconfiguration — tracked as GitHub issue #172.use_sectionsflag removed fromsrc/Domains/Admin/container.php; DI registration now follows standard module pattern.)(Resolved in 4.99.10 — the MANY_TO_MANY relation loader now hydrates fullWriteService::create()andupdate()return stale, role-name-less entities.Roleentities withidandname;Service::enrichRoles()bypass is moot. Tracked as #170.)No database integration tests for the Admin domain. All 27 tests are unit-level (mocked repositories). There are no integration tests exercising the actual
users,user_role, orrolestables. (tests/Unit/Domains/Admin/)
Tests
| File | Lines | Tests | What is covered |
|---|---|---|---|
tests/Unit/Domains/Admin/User/ValidatorTest.php | 132 | 12 | Username validation, password length, role existence, role authorization, duplicate username detection |
tests/Unit/Domains/Admin/User/WriteServiceTest.php | 166 | 8 | Create + role sync, update, soft delete, undelete, admin password reset, self-service password change |
tests/Unit/Domains/Admin/Role/RoleProviderTest.php | 80 | 7 | getAll() with and without hidden roles, getById(), exists(), resolveRoleIds() — tests the now-deprecated RoleProvider; coverage for the new DB-backed Role\Repository/Role\Service is pending |
Total: 27 unit tests across 378 lines.
Coverage gaps:
- No integration/database tests against real
users,user_role, orrolestables - No REST controller tests (
src/Rest/Admin/Controllers/User.php,Role.php,Me.php) - No test for
WriteRepository::syncRoles()in isolation - No tests for new
Role\Repository/Role\Service(DB-backed, added in 4.99.10) - No tests for any legacy path (
Adv_auth,Adv_users_model,Adv_user_role_model,Adv_roles_model)
Related Flows
- AD-01 Admin Auth & SSO — covers the login flow, session-based auth,
statelessAdminLogin()for REST JWT, and SSO. Auth library (Advauth) is shared with this flow. - AD-32 Audit Logging — covers the
user_audit_logstable written by audit-instrumented admin actions. - AD-55 Task Management — tasks and user assignments live in the same
Adv_authcontroller (lines 195+); extension hooksafterAddTask,afterEditTask,afterTaskDeleteare sibling to the user hooks in this flow.