You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Build the admin user-management surface decided in ADR 006: one
controller serving a list view that's scoped by role — ROLE_ADMIN
sees every user, ROLE_DOMAIN_MANAGER sees only users whose email
domain matches their own. The approval queue (#64) is one filter on
top of this view; this issue covers the broader listing and the
controller / service plumbing that #64 plugs into.
Tasks
Route /admin/users (Twig list) — IsGranted('ROLE_DOMAIN_MANAGER')
on the controller, then the voter / repository scoping handles
the per-domain visibility.
App\Controller\Admin\UserController::list() — thin per
project conventions:
Twig template: list with name, email, status badge, and action
links (Approve / Block) that point at the existing feat: approval queue for domain managers #64 actions.
Re-use existing Twig components (templates/components/Layout/, Form/, etc.) — do not invent new card / badge primitives if
one is already in templates/components/.
Tests:
Functional: admin can see users across multiple email domains.
Functional: domain manager sees only same-domain users.
Functional: a regular user (no ROLE_DOMAIN_MANAGER) gets 403.
Unit: UserRepository::findVisibleTo scopes correctly for both
actor types and respects the optional status filter.
Translations for any new strings under translations/messages.da.yaml.
CHANGELOG.md under ## [Unreleased] / Added.
Details - AI specificities
Why: ADR 006 specifies one controller for user management with
role-scoped queries — admin sees everyone, domain manager sees own
domain. The ADR's "Domain manager — a role, not a flag" section is
the spec.
(Preferred) This issue ships /admin/users with an optional ?status=pending query and the action buttons. feat: approval queue for domain managers #64's /admin/users/pending becomes a redirect / pre-filtered view of
the same controller. Land this first.
Authorisation: the voter from the sibling roles issue
(ROLE_DOMAIN_MANAGER + same-domain check) is what powers the
per-row "may I act on this user" decisions. The list query uses
the same domain-scoping logic but at the repository level so the
page doesn't have to render rows the voter would just deny.
Out of scope:
Bulk actions, sortable / searchable lists, pagination beyond the
framework default. Track separately if the list grows.
Editing a user's role(s) from this screen. ROLE_DOMAIN_MANAGER
promotion is a separate UX decision.
src/Controller/Admin/UserController.php (new or extended)
src/Service/UserManager.php (new or extended)
src/Repository/UserRepository.php
config/routes.yaml (if attribute routing isn't already in use
for admin)
templates/admin/user/list.html.twig (new)
translations/messages.da.yaml
tests/Functional/Admin/UserListTest.php (new)
tests/Unit/Repository/UserRepositoryTest.php (extended) — note:
a domain-scoping query is repository-level integration territory,
so the actual test belongs under tests/Integration/Repository/.
CHANGELOG.md
Acceptance verification:task coding-standards-check and task test-coverage (100% gate) both green; manual smoke test
by logging in as the domainmanager@aarhus.dk fixture user
(whenever the fixture lands) and verifying the visible list.
Resume
Description
Build the admin user-management surface decided in ADR 006: one
controller serving a list view that's scoped by role —
ROLE_ADMINsees every user,
ROLE_DOMAIN_MANAGERsees only users whose emaildomain matches their own. The approval queue (#64) is one filter on
top of this view; this issue covers the broader listing and the
controller / service plumbing that #64 plugs into.
Tasks
/admin/users(Twig list) —IsGranted('ROLE_DOMAIN_MANAGER')on the controller, then the voter / repository scoping handles
the per-domain visibility.
App\Controller\Admin\UserController::list()— thin perproject conventions:
UserManager(or similar) service.userManager->listVisibleTo($currentUser, $statusFilter).templates/admin/user/list.html.twig.App\Service\UserManager(the persistent home for user-mutationlogic — extend if it exists, else create), fully documented per
project conventions:
listVisibleTo(User $actor, ?UserStatus $statusFilter = null): array.UserRepositorygains a scoped finder:findVisibleTo(User $actor, ?UserStatus $statusFilter = null): array.WHERE email LIKE '%@' || :domain(parameterised properly, no string interpolation).
statusfilter for the approval-queue view in feat: approval queue for domain managers #64.links (Approve / Block) that point at the existing feat: approval queue for domain managers #64 actions.
Re-use existing Twig components (
templates/components/Layout/,Form/, etc.) — do not invent new card / badge primitives ifone is already in
templates/components/.ROLE_DOMAIN_MANAGER) gets 403.UserRepository::findVisibleToscopes correctly for bothactor types and respects the optional status filter.
translations/messages.da.yaml.CHANGELOG.mdunder## [Unreleased]/Added.Details - AI specificities
role-scoped queries — admin sees everyone, domain manager sees own
domain. The ADR's "Domain manager — a role, not a flag" section is
the spec.
(
/admin/users/pending). Two reasonable shapes:/admin/userswith an optional?status=pendingquery and the action buttons. feat: approval queue for domain managers #64's/admin/users/pendingbecomes a redirect / pre-filtered view ofthe same controller. Land this first.
/admin/usersafter feat: approval queue for domain managers #64 lands and refactor feat: approval queue for domain managers #64'scontroller to delegate to this one's service.
Pick whichever sequencing is easier when the work is picked up —
both end up at the same shape.
(
ROLE_DOMAIN_MANAGER+ same-domain check) is what powers theper-row "may I act on this user" decisions. The list query uses
the same domain-scoping logic but at the repository level so the
page doesn't have to render rows the voter would just deny.
framework default. Track separately if the list grows.
promotion is a separate UX decision.
/ Block) live in feat: approval queue for domain managers #64's
UserApprovalservice (or whatever itbecomes). This issue's
UserManager::listVisibleTo()is read-onlyscoping; mutations stay where they are.
src/Controller/Admin/UserController.php(new or extended)src/Service/UserManager.php(new or extended)src/Repository/UserRepository.phpconfig/routes.yaml(if attribute routing isn't already in usefor admin)
templates/admin/user/list.html.twig(new)translations/messages.da.yamltests/Functional/Admin/UserListTest.php(new)tests/Unit/Repository/UserRepositoryTest.php(extended) — note:a domain-scoping query is repository-level integration territory,
so the actual test belongs under
tests/Integration/Repository/.CHANGELOG.mdtask coding-standards-checkandtask test-coverage(100% gate) both green; manual smoke testby logging in as the
domainmanager@aarhus.dkfixture user(whenever the fixture lands) and verifying the visible list.
Related
approved).
ROLE_DOMAIN_MANAGER+ voter (this issue gates on thatvoter).
UserStatusenum (this view shows the status badge).