User management — redesign technical guide
Architecture and boundaries
| Layer | Responsibilities / source |
|---|---|
| Legacy employee list | src/Company/ProspectBundle/Controller/DefaultController.php::listAction, Resources/views/Default/list.html.twig, company employee queries and configurable DataTable. Live route /company/accompagnement/list. |
| Legacy manual creation | src/Company/CandidateBundle/Controller/DefaultController.php::newAction, Resources/views/Default/new.html.twig, src/Main/ConfigBundle/Form/Type/ProspectNewType.php. Live route /company/candidate/new. The Prospect controller's separate newAction is not this observed creation surface. |
| Embedded nextgen list | Frontend src/modules/Users/User/user.routes.ts, User.vue, components/UserList.vue. Legacy src/Twig/Company/CompanyNavigationExtension.php::getSmrUsersPeopleFeature selects admin/personal feature path from Service/V2/SmrDisplayService. Observed manager path /company/space/admin/users/people, embedded /space/admin/users/people. |
| Nextgen API | src/Http/Api/Routing/V1/user.yaml, user_import.yaml; Domain/V1/User/DTO/UserDTO.php; User controller/transformer/handler/repository/listener. V1 routes mounted under /api/v1 by config/routes.yaml. |
| AIhub | Separate company-scoped extraction and standardization of user/job/skill data, not an established source of account roles or synchronous provisioning. |
New user list -> user.composables -> user.api -> /api/v1/user -> permission + company/visibility query
Create wizard -> POST user -> UserHandler creates identity + Customer
-> separate POST user/{id}/job/{jobId} -> assignment event -> consumer
Legacy creation -> Candidate controller + form -> user/related records -> creation subscriber
User create/update -> UserListener -> group-rule messages -> recalculation consumer
AIhub user import -> source extraction -> standardization (separate process)
The new list can return a user to a legacy detail route using the employee hash. This is explicit client navigation, not proof of automatic migration. Familiar shared entity/table names do not establish shared deployed storage, sync direction, SSO precedence or rollback.
Data model and identities
The nextgen Infrastructure/Persistence/Entity/User/ model distinguishes User, Customer (employee profile) and School (manager-related record). The legacy domain has equivalent concepts under Main/ConfigBundle. Do not treat a User ID, Customer hash, manager User ID and School ID as interchangeable.
UserDTO maps status to enabled, lastname to the persisted name, and employee profile hash through Customer. Manager ID in the API is a User ID; Infrastructure/Persistence/Transformer/V1/User/UserTransformer::reverseTransformManager resolves it through SchoolRepository::findByUser. It uses isset, so a null manager payload is not proof of an implemented detach operation. Job ID/titles are read-only user DTO properties; use the assignment endpoint.
Infrastructure/Persistence/Handler/User/UserHandler::finalizeEntityBeforeCreate generates username/timestamps, hashes credentials via UserPasswordService, and creates Customer::fromUser. Updates regenerate the display username. Entity/User/User::canonicalizeProperties canonicalizes email and username before persistence/update. User DTO uniqueness checks emailCanonical; it is not simply a tenant-visible list search.
Employee activation, job requirements/skills, role membership, visibility, anonymization and employee classification are separate state. Anonymized-user filtering is a separate concern; UserManager::loadIncludingAnonymized temporarily manages that filter for internal processing. This does not authorize general UI access to anonymized records.
Nextgen API contracts
All paths below use /api/v1, except the explicit V2 permission endpoint. Read/list and mutation permissions are separate.
| Method / path | Permission / behavior |
|---|---|
GET /user, /user/{id} | read-user; company/visibility-scoped UserRepository::findUsers. Numeric detail ID. |
GET /user/self | Authenticated ROLE_USER, current user repository method. Not general user-list access. |
POST /user | create-user, DTO create validation; creates identity and employee profile. |
PUT/PATCH /user/{id} | update-user, scoped resource and corresponding write validation. Partial updates must follow actual DTO/group handling. |
DELETE /user/{id} | delete-user; UserHandler::handleDelete disables, persists and flushes the account. Route metadata says soft_deletable: false, but custom handler behavior prevails. Not a hard erasure contract. |
POST /user/{id}/job/{jobId} | update-user, no body; resolves job, rejects already assigned job, assigns and publishes JobAssignedToUser. |
POST /user/import | create-user, OAuth scopes import, import_user; bulk upsert action importUser, repository findUserByEmployeeIdOrEmail. Different from manual POST. |
POST /user/self/update-password | Defined with update-user, dedicated password payload/controller/service. Never infer that generic PATCH password updates are supported. No credential action was performed. |
GET /api/v2/user/self/permission | Authenticated ROLE_USER; current-user permission resolution. Separate from V1 user CRUD. |
List request, filters and serialization
nextgen-front/src/modules/Users/User/api/user.api.ts::getUserList sends page/per_page (defaults 1/50), username contains, status, manager IDs joined by |, and requested sorting. Example read-only contract, with illustrative manager IDs, not executed against live data:
GET /api/v1/user?page=1&per_page=50&filters[username][contain]=Alex&filters[status]=1&filters[manager_id][equal]=123|456
Infrastructure/Resource/Filter/FilterManager::explodeMultiValue splits |; Adapter/QueryBuilderAdapter::addConditions ORs values within a filter and ANDs filter groups. Route metadata additionally supports firstname, lastname, email, manager_name, manager_only, hierarchy_level, only_default, job_title, updated_at, id and employee_id with declared operators. These extra API filters are not automatically visible UI controls.
Backend default sort declarations include firstname, job_title, manager_name, status and updated_at. The checked client maps the Name sort to username although username is not among these declared sorting defaults; actual Name-sort acceptance was not verified. It maps “Last Sync” from updated_at, so that column is not a successful-import timestamp.
user.composables::fetchUsers maps API results and preserves selected IDs across page fetches. Filter changes do not consistently reset page. The list empty-state resetFilters clears three criteria but leaves other state; a separate composable reset has different behavior. API mapping validates with schemas, then includes fallback mapping on errors. A rendered list alone does not prove a fully valid upstream schema.
Validation and write boundaries
Domain/V1/User/DTO/UserDTO.php enforces creation email, firstname and lastname; email length 1–180 and canonical uniqueness; names length 1–100; boolean status default true; supported locale; and company-scoped manager identity. Supplied creation passwords have dedicated length/complexity/compromised-password validation and are write-only; the service can generate credentials when absent. No password values, invitations or authentication outcomes were tested.
The wizard requires job/manager/status choices more strictly than the DTO. The create helper utils/transformUserData.ts omits false/empty values, which can omit status=false and invoke the API's true default. The submit object also omits the visible company-entry date. UserForm.vue defines optional additional fields and an age validator, but the last-step submit condition can emit success without requiring its validation result. These are source findings, not live successful mutation tests.
Creation and job attachment are separate requests in UserList::handleFormSubmit. Create attachment failure can leave the user present; edit attachment failure is caught while the edit flow continues. There is no verified client transaction or compensating delete. The user DTO ignores job assignment through its read-only job properties; tests/Behat/Features/V1/user/create.feature expects a null job after creation with job_id. Verify assignment separately.
Authorization and population isolation
Infrastructure/Persistence/Repository/User/UserRepository::{createBaseUserQueryBuilder,findUsers} limits company, excludes the service account by default and, for general lists, excludes the master-manager case unless requested. It does not apply an enabled-only filter by default; status is separate.
Infrastructure/Resource/Repository/EntityRepository::addVisibilityFilters uses resolved allowed visibility groups and supports hierarchical relationship OR paths. If the resolved group array is empty, it returns the existing query without adding a restriction. Do not equate “no groups” with “no users” or promise a universal hierarchy-only default. The upstream permission resolver and route access must be assessed together. Identifier-based DTO company validation supplements, rather than replaces, action authorization.
Legacy list access checks customer_r for the relevant subordinate-manager case; Candidate creation checks customer_c when a profile is attached. Profile absence/privileged branches mean the nearest conditional is not a complete platform-wide policy. Navigation right smr_users_people_access, allow-admin-space/allow-personal-space and nextgen action rights have different roles. SmrDisplayService currently consults SMR scope grants, so older Confluence descriptions of legacy-menu versus new functional rights are not exhaustive navigation algorithms.
Lifecycle and asynchronous work
Legacy CandidateBundle::newAction writes user and related records, establishes current/selected manager context, then dispatches UserEvent::CREATION. src/EventSubscriber/UserSubscriber::{onUserCreated,addAutoTestToUser,setHasToChangePassword} starts additional processing and dispatches UserProfileCreatedEvent. Do not assume manual creation is a side-effect-free identity insert or immediate completion of all downstream work.
Legacy ProspectBundle::activateDesactivateAction toggles enabled state and can rewrite the email with a deactivation marker; activation restores the original unless another account already uses it, producing a warning. The inspected method alone does not establish exhaustive authorization/tenant enforcement; its lookup should not be presented as proof of cross-company protection. No activation request was executed.
Nextgen UserHandler::handleDelete disables without deleting, confirmed by the delete feature's expected subsequent GET 200/status=false. This is distinct from anonymization. Persistence/Listener/User/UserListener publishes group-rule work after create/import-create and update/import-update. It records the old manager before update and republishes affected current/old manager work when changed. Command/Consumer/Rule/Group/UserGroupRuleUpdateConsumerCommand.php owns the corresponding rule-update processing; complete retry/latency semantics were not validated.
Http/Api/Controller/V1/User/UserController::assignJob dispatches an update event and publishes JobAssignedToUser. Command/Consumer/User/JobAssignedToUserConsumerCommand calls Infrastructure/Service/User/UserSkillService::addSkillsToUser to attach missing skills. It does not establish that job-required mastery overwrites assessments or that removal reverses all employee data.
Bulk status behavior
Frontend components/UserStatusBulkActions.vue loops selected IDs and calls useUserOperations::updateUserStatus, which returns false on an API failure. The loop does not require that boolean to be true before continuing to its success message; no atomic backend bulk operation is used there. Row changes are optimistic with a rollback attempt. Audit actual saved results and request failures; a toast is not a transaction receipt.
Provisioning and AIhub boundaries
Legacy src/Service/Imports/UserImportsService.php exposes user/customer/manager creation, job assignment, identity changes and relationship import helpers. The documentary Import Center sequence is distinct from nextgen /user/import metadata. Nextgen findUserByEmployeeIdOrEmail selects within company, using supplied employee ID or canonical email and prioritizing a match on both. Conflicts, empty-field semantics and row-level recovery need the actual import DTO/process contract; no universal overwrite promise is made here.
AIhub src/entity/user/imports/data_sources/import_users_sm.py::import_users_from_sm extracts company-filtered user identity, active flag, job/organizational data and related attributes; active-only extraction defaults true. Optional filters can use employee number, email or User ID depending on arguments. src/entity/user/imports/import_users_process.py::standardize_user_batch standardizes selected users. This is evidence of extraction/standardization, not a synchronous Save hook, role authority, immediate deactivation propagation or automatic access revocation in every downstream system.
Runtime, observability and extension points
Runtime dependencies include the API/security context, Doctrine persistence, legacy form/DataTable stack, embedded frontend messaging and the rule/assignment consumer runtime. External HR/SSO flows and AIhub are separate integrations whose scheduling and precedence were not verified.
| Symptom | Investigation point |
|---|---|
| List count differs by generation | Compare company, visibility, master/service-account exclusions, status and restored filters before diagnosing sync. |
| No result after filter | Current page, username versus email query, status and scoped population. |
| Creation error but user exists | Separate POST and job-assignment calls; inspect existing identity before retry. |
| Bulk success with unchanged account | Per-ID requests, helper false returns, saved status; no atomic completion guarantee. |
| Reactivation conflict | Legacy original-email collision; not equivalent to nextgen status PATCH. |
| Group/skill changes delayed | Published user/group/job work and consumer completion; no timing guarantee. |
| API failure | Route permission (403), DTO invalid-parameter response (400), scoped/missing resource (404) according to checked test contracts. |
Frontend catch paths log errors, but they are not an activity-audit trail. Log identifiers/status necessary for diagnosis rather than full email, address, birth date, profile or assessment payloads. Extend route → permission → DTO → transformer/handler → scoped repository → event/consumer and UI mapper together; do not add a field to the form alone and assume persistence/filtering exists.
Evidence, tests and limitations
Verified in code: named paths/symbols across the four repositories, inspected 15 October 2026. No application source changed, workers started or API mutation executed. Observed in application: UI routes and first-step validation only, documented in the functional/configuration guides.
Test sources inspected, not run: nextgen tests/Behat/Features/V1/user/{create,read,edit,delete,import,attach_job,access,anonymization}.feature were located; creation/deletion scenarios were read directly. They cover expected invalid/duplicate identities, status defaults, ignored job assignment during creation and deactivation-on-delete. The remaining located suites are pointers for future validation, not claimed passing checks.
Documented in Confluence: User Management System, v4, 12 June 2025 (intent, not proof of shipped bulk/export/custom-filter features); Import Center for Users, v2, 29 April 2025 (legacy documentary workflow, with title/code/deactivation inconsistencies); SMR playbook, v3, 24 September 2026 (coexistence and deployment intent).
Supplied KBCT 2136702980 was unavailable. Supplied PS methodology/onboarding entry pages were reviewed earlier in this session and do not establish user API contracts. Not verified: deployed database sharing, SSO conflict precedence, final create/edit behavior, full import semantics, consumer retry/delivery timing, audit completeness, session invalidation and account lifecycle propagation to external systems.
Dernière mise à jour : 3 novembre 2026