- drop ClientPortal module, ClientTicket entity, ROLE_CLIENT and all couplings (Task, TaskDocument, User, Notification) back to an internal-only model
- migration drops client_ticket / user_allowed_projects / related FK columns and removes leftover external client accounts (would otherwise be promoted to ROLE_USER)
- remove client-portal frontend module, admin tickets tab, user portal section, portal nav item and portal/clientTicket i18n keys
- fix directory nav icon (invalid mdi:contact-multiple-outline -> mdi:card-account-details-outline)
- add 'make sync-permissions' target, wire it into install/db-reset and the prod deploy script
Findings from the post-migration code review. The arrival of ROLE_CLIENT
exposed internal endpoints still guarded only by IS_AUTHENTICATED_FULLY (or no
security), reachable by a client. Verified by re-running a multi-role smoke
test (client -> 403, internal roles -> 200).
Security (closed real client-isolation holes):
- TaskDocumentDownloadController: add ownership check (admin all / client only
own clientTicket docs / user only task-linked docs) — the custom download
bypassed the cloistered provider.
- Share browse/download/search/status controllers: IS_AUTHENTICATED_FULLY ->
ROLE_USER (SMB share is internal).
- User Get/GetCollection: add security ROLE_USER (was exposing the internal
directory to clients).
- BookStackLink GetCollection/Post/Delete: IS_AUTHENTICATED_FULLY -> ROLE_USER.
Contracts / robustness:
- TaskInterface gains getProject(): ?ProjectInterface; TimeTracking export
controller/service drop concrete cross-module entities for repo interfaces.
- Shared MCP Serializer signatures widened to the contracts (user/projectRef/
taskRef/tags/users); project()/userFull()/etc. kept concrete (use getters
outside the contracts).
- RecurrenceHandler: null-guard before findMaxNumberByProjectForUpdate().
180 tests green, cs-fixer clean, routes unchanged.
Security hardening on the document POST that phase 1 widened to ROLE_CLIENT:
a client user could reach the share-link path (arbitrary SMB file reference)
instead of an upload. Now the sharePath branch is admin-only — client users
must upload. attachTarget already scopes documents to the client's own ticket.
178 tests green.
LST-59 (3.1) backend. New native reporting module that aggregates across
TimeTracking/ProjectManagement/Absence with ZERO direct inter-module imports —
coupled only to the physical SQL schema via read-only DBAL (AuditLog provider
pattern).
- 4 read-only reports (ApiResource + DBAL provider + readonly DTO,
paginationEnabled false, security reporting.view): /api/reports/
{time-per-project, time-per-user, tasks-by-status, absences-by-type}.
All filters bound-param, dates validated YYYY-MM-DD (default = current month),
int filters validated by regex (cs-fixer-stable).
- No Doctrine entity, no migration. ReportFilterTrait centralises validation.
Absence status compared by literal 'approved' to avoid importing the enum.
- ReportingModule registered (id reporting, reporting.view/export perms);
sidebar /reporting item gated by module + permission (ROLE_ADMIN section).
169 tests green (163 + 6), 4 routes exposed, cs-fixer clean.
LST-58 (2.4), part 1/2 — Client move. Prospect + repertoire front are pending
the product spec and will be added on this branch afterward.
- Client entity moved to src/Module/Directory/Domain/Entity; repository split
into Domain/Repository/ClientRepositoryInterface + Doctrine impl (bound in
services.yaml). 5 client MCP tools moved to Infrastructure/Mcp/Tool, now
injecting the interface.
- resolve_target_entities ClientInterface repointed to Directory\Client;
Directory mapping added; DirectoryModule registered (id directory, 2 RBAC
perms). Client.projects relation now uses ProjectInterface -> Directory no
longer depends on ProjectManagement.
- ProjectManagement Create/UpdateProjectTool inject Directory's
ClientRepositoryInterface; Serializer and fixtures repointed.
- Garde-fous: #[Auditable] + Timestampable/Blamable on Client (additive
migration: created_at/updated_at + created_by/updated_by FK ON DELETE SET
NULL + COMMENT).
161 tests green, mapping valid, no API route regression, cs-fixer clean.
LST-66 (2.3) backend. Behaviour-preserving move of the absences domain into
src/Module/Absence/. API operations, securities, routes and the 10 MCP tool
names are unchanged.
- 3 entities + 3 enums moved to Domain/{Entity,Enum}; user relations stay on
UserInterface. 3 repositories split into Domain/Repository interfaces +
Doctrine impls (bound in services.yaml); find() kept off interfaces
(findById instead).
- Pure services (AbsenceDayCalculator, PublicHolidayProvider) -> Domain/Service;
AbsenceBalanceService -> Application/Service; State (5), controllers (5),
10 MCP tools and AccrueLeaveCommand -> Infrastructure/.
- New LeaveProfileInterface contract (Shared) exposes the HR getters used by
AbsenceBalanceService/AccrueLeaveCommand; User implements it -> Absence no
longer imports the concrete Core User. MCP tools/command inject
UserRepositoryInterface (findById) instead of the concrete repository.
- Timestampable/Blamable added to AbsenceBalance and AbsencePolicy (additive
migration: created_at/updated_at + created_by/updated_by FK ON DELETE SET
NULL + COMMENT). AbsenceRequest untouched (already has createdAt/reviewedAt).
- AbsenceModule registered (id absence, 4 RBAC perms, not re-wired); doctrine
mapping added; team-absences sidebar item gated by the module.
161 tests green, mapping valid, no API route regression, cs-fixer clean.
Tranche 3 of LST-65. Task and Project adopt TimestampableBlamableTrait.
- Additive migration on task and project: created_at/updated_at (nullable),
created_by/updated_by (nullable INT, FK to "user" ON DELETE SET NULL) +
indexes + COMMENT ON COLUMN. down() drops only the added objects.
- Trait fields stay out of the existing API groups (trait carries its own).
- Functional test (TaskTimestampableTest) confirms created_at on persist and
updated_at refresh on update.
161 tests green, no destructive migration.
Tranche 2 of LST-65. Mechanical, behaviour-preserving move of the core
business domain into src/Module/ProjectManagement/. API operations,
securities, uriTemplates and the 38 MCP tool names are all unchanged.
- 10 entities + 2 enums moved to Domain/{Entity,Enum}; intra-module
relations stay concrete, cross-module relations go through contracts
(Project.client -> ClientInterface, Task/TaskDocument users ->
UserInterface).
- 9 repositories split into Domain/Repository interfaces + Doctrine impls,
bound in services.yaml; consumers inject the interfaces. find() kept off
the interfaces (ServiceEntityRepository ?object compat) -> findById().
- State (7), MCP tools (38), controller, CalDavService/RecurrenceCalculator,
3 Doctrine listeners and SwitchWorkflowOutput moved under Infrastructure/.
- doctrine.yaml: ProjectManagement mapping + resolve_target_entities of the
3 module contracts repointed to the module (ClientInterface stays legacy).
- ProjectManagementModule registered (id project-management, 4 RBAC perms,
not re-wired); sidebar my-tasks/projects gated by the module.
- Legacy not-yet-modularised consumers (Mail/Gitea/BookStack, Serializer,
fixtures, tests) swapped to the module FQCN — transitional coupling to be
cleaned in 2.4/2.5/2.6.
159 tests green, mapping valid, no API route regression, cs-fixer clean.
Tranche 1 of LST-65 (ProjectManagement module migration). Decouples the
TimeTracking module from the core-business entities before they move, with
no entity relocation yet — keeps the diff minimal and the risk isolated.
- New read contracts in Shared/Domain/Contract (minimal surface, aligned on
the entities' real nullable signatures): ProjectInterface (id/code/name),
TaskInterface (id/number/title), TaskTagInterface (id/label/color),
ClientInterface (id/name).
- Project/Task/TaskTag/Client implement their contract (entities stay in
src/Entity for now). Project.client typed as ClientInterface.
- TimeEntry (TimeTracking) now references ProjectInterface/TaskInterface/
TaskTagInterface instead of the concrete entities; repository + DQL
untouched in behaviour.
- resolve_target_entities maps the 4 contracts to the legacy entities (will
be repointed to the module in tranche 2).
- Adds the migration plan doc.
159 tests green, mapping valid, cs-fixer clean.
First business module of Phase 2 (LST-64, rodage). Strangler-style,
additive move — no behavioural change to the public API or MCP tools.
- New module App\Module\TimeTracking (TimeTrackingModule, id "time-tracking",
declares time-tracking.entries.view/export permissions in the RBAC catalog;
operation security left on ROLE_USER, not re-wired here).
- Move TimeEntry entity, repository (now interface + Doctrine impl bound in
services.yaml), ActiveTimeEntryProvider, export service/controller and the
4 MCP TimeEntry tools into the module. #[ApiResource] (operations, security,
uriTemplates /time_entries/*), filters and serialization groups preserved.
- Doctrine mapping "TimeTracking" added; table time_entry unchanged.
- Sidebar item gated with module "time-tracking" (SidebarFilter disables the
route when the module is inactive).
- Timestampable/Blamable adopted (first adopter): additive migration adds
created_at/updated_at/created_by/updated_by (nullable, FK SET NULL) +
COMMENT ON COLUMN. Functional test confirms created_at on persist and
updated_at refresh on update — the suspected preUpdate recompute issue does
not occur (Doctrine ORM 3.6.2 recomputes change sets after preUpdate).
159 tests green, schema mapping valid, php-cs-fixer clean.