Skip to content

Development: Let admins manage every user from one place - #2487

Open
az108 wants to merge 35 commits into
mainfrom
feat/manage-users-admin-page
Open

Development: Let admins manage every user from one place#2487
az108 wants to merge 35 commits into
mainfrom
feat/manage-users-admin-page

Conversation

@az108

@az108 az108 commented May 8, 2026

Copy link
Copy Markdown
Collaborator

Checklist

General

  • I tested all changes and their related features with all corresponding user types.
  • Language: I followed the guidelines for inclusive, diversity-sensitive, and appreciative language.
  • I chose a title conforming to the naming conventions for pull requests.

Server

  • Important: I implemented the changes with a very good performance and prevented too many (unnecessary) and too complex database calls.
  • I strictly followed the principle of data economy for all database calls.
  • I strictly followed the server coding and design guidelines.
  • I added multiple server tests (JUnit) related to the features.
  • I documented the Java code using JavaDoc style.

Client

  • Important: I implemented the changes with a very good performance, prevented too many (unnecessary) REST calls and made sure the UI is responsive.
  • I strictly followed the principle of data economy for all client-server REST calls.
  • I strictly followed the client coding and design guidelines.
  • I added multiple integration tests (Vitest) related to the features.
  • I documented the TypeScript code using JSDoc style.
  • I added multiple screenshots/screencasts of my UI changes.

Motivation and Context

Closes #2486.

Administrators previously had no UI to oversee, create, edit, or delete users. This change adds a dedicated Manage Users admin page, plus a small role-picker addition to the existing research-group add-members flow.

This branch was originally built against the old Keycloak-backed user management and has since been adapted to the internal user management introduced in #2590. Keycloak is now used only for TUM members, and the admin page no longer writes to it at all: its single remaining Keycloak call is a read used to look up TUM members for import.

Description

Server

  • New admin-only /api/admin/users controller with list / view / create / import / update / delete endpoints (UserAdminResource + UserAdminService).
  • Create provisions an internally managed user (UserService.provisionExternalUser + setLocalPassword). Admin-created users are always internal; nothing is written to Keycloak.
  • Import pulls an existing TUM member into the local DB. The identity is re-resolved server-side via KeycloakUserService.findUserByUniversityId (read-only) rather than trusting an id supplied by the client, so a picked entry cannot be used to fabricate a user.
  • Delete anonymises the local record only. TUM identities remain owned by Keycloak, so a later login re-provisions a fresh row.
  • Passwords are refused for TUM members at the single point where passwordHash is written. This also closes the same gap on PUT /api/users/password, whose Javadoc claimed the restriction but which enforced nothing.
  • Admin create rejects an email that already belongs to an existing account. provisionExternalUser resolves by email and emails are not unique across TUM and applicant rows, so without this an admin could set a password on — and mark verified — somebody else's account, including a TUM member. create is transactional and applies optional fields before the password check, so a supplied universityId cannot be written after the guard and slip past it.
  • New UserRetentionService.deleteUserByAdmin — public single-user delete that reuses the existing per-category anonymisation pipeline (admins included).
  • New UserRepository.findAllUsersForAdmin JPQL projection with role + research-group filter, search across name / email / university id.
  • New DTOs: AdminUserOverviewDTO, AdminUserDetailDTO, CreateUserDTO, UpdateUserDTO, ImportUserDTO (carries universityId).
  • Bundled: extended AddMembersToResearchGroupDTO with an optional role field (default EMPLOYEE for back-compat); ResearchGroupService.addMembersToResearchGroup propagates it (only PROFESSOR or EMPLOYEE accepted).
  • Self-delete is rejected with 400 (OperationNotAllowedException).

Client

  • New routes /manage-users, /manage-users/create, /manage-users/:userId (admin only).
  • ManageUsersPageComponent mirrors the recent admin-list pattern (signals, computed filter labels, no method-calls in templates).
  • ManageUserFormComponent — single shared form with three modes: create / edit / import. Reactive Forms + signals.
  • Import mode uses the same searchable Keycloak user picker as the research-group flows (/api/users/available-for-research-group, no target group). Users without a universityId are filtered out and cannot be selected, since only TUM members are importable. Submitting posts { universityId }.
  • Bundled: ResearchGroupAddMembersComponent gets a role-picker dropdown (PROFESSOR / EMPLOYEE, default EMPLOYEE).
  • New manageUsersPage i18n keys (en + de) plus route titles. German uses neutral plural style ("Nutzende verwalten", "Mitarbeitende", "Professoren").

What changed during review

  • Role assignment moved out of the admin service. UserAdminService was reaching for ResearchGroupRepository itself to write role mappings. That logic is now UserService.setPrimaryRole, so the admin service goes through the service that owns users rather than around it, and the same rule is available to anything else that needs it.

  • The delete endpoint's Javadoc claimed it deleted from Keycloak. It never did after the move to internal user management — it anonymises the local record only. Corrected, since a stale contract on a destructive endpoint is worth more than a stale comment elsewhere.

  • Added resource tests for the update and delete flows, which had none, and folded the create/import test fixtures into the resource test rather than a shared holder.

  • Page titles picked up {siteName} when main's rename landed, so this page follows the configurable platform name like the rest of the app.

  • A role change no longer empties the user's other research groups. Setting a role deleted every mapping the user had and wrote one back, so changing someone's role in one group silently removed them from every other group — while the form says "set the role", not "remove from all other groups". A group-bound role now replaces only the role held in the group it names. A role that belongs to no group still clears the group-bound ones, because that is a demotion out of every group rather than a change within one.

  • The test context stopped replacing UserService with a plain mock. The endpoint an admin calls delegates to that service, so with a mock in place nothing ran and the behaviour above was unreachable from any test. It is a spy now: unstubbed methods run against the real database, and two tests assert what a role change leaves behind. One of them fails against the old behaviour, so it is a regression guard rather than a description of whatever the code happens to do.

  • Twenty stubs moved from when(...) to doReturn(...). Stubbing a spy with when(spy.method()) calls the real method first. That showed up immediately as a double-counted invocation, and would also have written a real password hash while setting up an unrelated test.

Steps for Testing

Prerequisites:

  1. Local Keycloak running at http://localhost:9080 (deployed Keycloak URL is set via the KEYCLOAK_URL GitHub variable). Still required — the import picker reads TUM members from Keycloak.
  2. Log in as an admin user.

Steps:

  1. Sidebar → "Manage Users". Confirm /manage-users loads with the table populated.
  2. Search by name / email / university id; confirm filtering.
  3. Open Role and Research Group filters; confirm filtering combines.
  4. "Create User" → fill firstName, lastName, email, password (min 8 + uppercase + lowercase + digit). Submit. Confirm: redirect to /manage-users/:id, the local DB row has a password_hash and the APPLICANT role, and — importantly — no user is created in Keycloak.
  5. "Create User" again using the email of an existing account. Confirm the request is rejected with 400 and no row is created.
  6. "Import from Keycloak" → type at least 3 characters to search, pick a TUM member from the result list, Submit. Confirm the user is imported with their university_id set. Confirm users without a university id do not appear as selectable options, and that Submit stays disabled until a user is picked.
  7. Import a university id that does not exist in Keycloak (via the API directly). Confirm 404.
  8. View an existing user → fields pre-fill. Edit phoneNumber + Save. Confirm DB updated. Confirm Keycloak NOT touched.
  9. Delete a non-self user → confirm dialog → Confirm. Confirm local anonymisation runs and FK references stay valid via the deleted-user placeholder. Confirm the user still exists in Keycloak (TUM identities are not deleted by us).
  10. Self-delete → confirm UI disables the button on your own row; the API returns 400 if the request is forced.
  11. As a TUM member, call PUT /api/users/password. Confirm it is refused and no password_hash is written.
  12. Manage-members in a research group → pick PROFESSOR. Add a user. Verify the new UserResearchGroupRole row has PROFESSOR.
  13. Change an existing user's role from the admin page. Confirm the new UserResearchGroupRole row is written.
  14. Do the same for a user who belongs to two research groups. The membership of the group you did not touch should survive. Then set that user to Applicant and confirm both group memberships are gone.
  15. Log out / log in as professor → navigate to /manage-users → 403 / redirect.

Review Progress

Code Review

  • Code Review 1

Manual Tests

  • Admin can list, search, filter, view, create, import, edit, delete users
  • Admin-created users are internal — nothing is written to Keycloak
  • Import picker only offers TUM members and imports by university id
  • Deleting a user does not remove them from Keycloak
  • TUM members cannot be given a local password
  • Self-delete is blocked
  • Manage-members role picker passes PROFESSOR / EMPLOYEE through correctly
  • Changing a role writes the new mapping
  • A role change keeps the user's other research-group memberships
  • Professor cannot reach /manage-users

Screenshots

To be added before merge.

Test Coverage

Client

Class/File Line Coverage Lines Expects Ratio
manage-user-form.component.ts 93.07% 507 95 18.7
manage-users-page.component.ts 97.36% 282 53 18.8
research-group-add-members.component.ts 95.95% 196 51 26.0

Server

Class/File Line Coverage Lines
UserRetentionService.java 83.90% 207
PageUtil.java 87.50% 32
AddMembersToResearchGroupDTO.java 100.00% 12
AdminUserDetailDTO.java 100.00% 29
AdminUserOverviewDTO.java 100.00% 19
CreateUserDTO.java 100.00% 25
ImportUserDTO.java 100.00% 5
UpdateUserDTO.java 100.00% 22
UserRepository.java 100.00% 230
ResearchGroupService.java 79.53% 536
UserAdminService.java 78.70% 275
UserService.java 33.33% 227
ResearchGroupResource.java 87.10% 182
UserAdminResource.java 82.35% 82
OAuth2LoginSuccessHandler.java 85.29% 74
WebAuthnLoginSuccessHandler.java 95.45% 51
AuthenticationResource.java 10.64% 131

Last updated: 2026-07-30 10:59:57 UTC

az108 and others added 12 commits May 8, 2026 13:42
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…WithPassword

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…in delete

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…embers

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d-members

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added server Pull requests that update Java code. (Added Automatically!) client Pull requests that update TypeScript code. (Added Automatically!) tests user management Code changes in user management module. labels May 8, 2026
@github-actions

github-actions Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor

📊 Client Test Coverage Too Low

🔍 View coverage locally:

npm run test:ci
open build/test-results/vitest/coverage/index.html

🌐 View coverage from GitHub:
Download the "coverage-report-client" artifact from this workflow run.

@codacy-production

codacy-production Bot commented May 8, 2026

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 5 high · 5 medium

Alerts:
⚠ 10 issues (≤ 0 issues of at least minor severity)

Results:
10 new issues

Category Results
Security 5 high
Complexity 5 medium

View in Codacy

🟢 Metrics 185 complexity

Metric Results
Complexity 185

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@github-actions

github-actions Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor

📊 Server Test Coverage Too Low

🔍 View coverage locally:

./gradlew test jacocoTestReport
open build/reports/jacoco/test/html/index.html

🌐 View coverage from GitHub:
Download the "coverage-report-server" artifact from this workflow run.

The detail view streams `user.researchGroupRoles` to derive the primary
role, but `findById` returns a User whose roles collection is lazy. By
the time the stream runs the Hibernate session is gone and the lookup
500s. Use the existing JOIN-FETCH finder used by UserRetentionService.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented May 9, 2026

Copy link
Copy Markdown
Contributor

📊 Client Test Coverage Too Low

🔍 View coverage locally:

npm run test:ci
open build/test-results/vitest/coverage/index.html

🌐 View coverage from GitHub:
Download the "coverage-report-client" artifact from this workflow run.

@github-actions

github-actions Bot commented May 9, 2026

Copy link
Copy Markdown
Contributor

🤖 No OpenAPI or client changes needed.

@github-actions

github-actions Bot commented May 9, 2026

Copy link
Copy Markdown
Contributor

📊 Server Test Coverage Too Low

🔍 View coverage locally:

./gradlew test jacocoTestReport
open build/reports/jacoco/test/html/index.html

🌐 View coverage from GitHub:
Download the "coverage-report-server" artifact from this workflow run.

@github-actions

github-actions Bot commented May 9, 2026

Copy link
Copy Markdown
Contributor

📊 Client Test Coverage Too Low

🔍 View coverage locally:

npm run test:ci
open build/test-results/vitest/coverage/index.html

🌐 View coverage from GitHub:
Download the "coverage-report-client" artifact from this workflow run.

@github-actions

github-actions Bot commented May 9, 2026

Copy link
Copy Markdown
Contributor

🤖 No OpenAPI or client changes needed.

@github-actions

github-actions Bot commented May 9, 2026

Copy link
Copy Markdown
Contributor

📊 Server Test Coverage Too Low

🔍 View coverage locally:

./gradlew test jacocoTestReport
open build/reports/jacoco/test/html/index.html

🌐 View coverage from GitHub:
Download the "coverage-report-server" artifact from this workflow run.

az108 and others added 2 commits May 10, 2026 20:32
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eate-field branches

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

There hasn't been any activity on this pull request recently. Therefore, this pull request has been automatically marked as stale and will be closed if no further activity occurs within seven days. Thank you for your contributions.

…in-page

# Conflicts:
#	openapi/openapi.yaml
#	src/main/java/de/tum/cit/aet/usermanagement/repository/UserRepository.java
#	src/main/java/de/tum/cit/aet/usermanagement/service/ResearchGroupService.java
#	src/main/webapp/app/generated/.openapi-generator/FILES
@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

🤖 No OpenAPI or client changes needed.

@github-actions

Copy link
Copy Markdown
Contributor

There hasn't been any activity on this pull request recently. Therefore, this pull request has been automatically marked as stale and will be closed if no further activity occurs within seven days. Thank you for your contributions.

@github-actions

Copy link
Copy Markdown
Contributor

There hasn't been any activity on this pull request recently. Therefore, this pull request has been automatically marked as stale and will be closed if no further activity occurs within seven days. Thank you for your contributions.

Adapt the Manage Users admin page to the internal user management introduced
in #2590, and resolve the conflicts in KeycloakUserService and UserService.

#2590 removed the external Keycloak realm: Keycloak is now used only for TUM
members, and the whole external-realm write surface was deleted. This branch
still drove Keycloak for admin user CRUD, so after the merge it no longer
compiled - create, import and delete each called a method that no longer
exists.

Conflict resolution
- KeycloakUserService: took main's version. This branch's only additions
  (findKeycloakUserById, createUserWithPassword, deleteUser) were all built on
  externalKeycloak/externalRealm, which main deleted.
- UserService: kept both sides of the constructor - this branch's
  researchGroupRepository and main's passwordEncoder.

Admin user management, rewired
- create now provisions an internally managed user (provisionExternalUser plus
  setLocalPassword) instead of creating a Keycloak user. Admin-created users
  are always internal.
- import now re-resolves the TUM member server-side via findUserByUniversityId
  instead of trusting a client-supplied Keycloak UUID, and the client picks the
  user through the existing Keycloak search picker rather than pasting a UUID.
- delete no longer removes the user from Keycloak; only the local record is
  anonymised, so a later login re-provisions a fresh row.

The admin feature's only remaining Keycloak call is a read.

Password handling
Local passwords are now refused for TUM members at the single point where
passwordHash is written, which also closes the same gap on PUT
/api/users/password - its Javadoc claimed the restriction but nothing enforced
it.

Admin create additionally rejects an email that already belongs to an account.
provisionExternalUser resolves by email and emails are not unique across TUM
and applicant rows, so without this an admin could set a password on, and mark
verified, somebody else's account - including a TUM member, granting them
password login. create is now transactional and applies optional fields before
the password check, so a supplied universityId cannot be written after the
guard and slip past it.

Tests
UserAdminResourceTest covered neither create nor import, so the suite stayed
green while none of this was exercised. Added coverage for the internal create
path (asserting Keycloak is untouched), duplicate-email and TUM-password
rejection, and import by university ID including the unknown-id and blank-id
cases. The setLocalPassword guard itself is not directly covered: UserService
is a mock in resource tests and this repo keeps server coverage at the resource
layer.

Verified on the merged tree: 804 server tests, 2063 client tests, client
typecheck, client production build, eslint (0 errors), a11y lint and prettier.

Co-Authored-By: Claude <noreply@anthropic.com>
@codacy-production

codacy-production Bot commented Jul 25, 2026

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 2 high · 4 medium · 4 minor

Alerts:
⚠ 10 issues (≤ 0 issues of at least minor severity)

Results:
10 new issues

Category Results
Security 2 high
CodeStyle 4 minor
Complexity 4 medium

View in Codacy

🟢 Metrics 276 complexity

Metric Results
Complexity 276

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 No OpenAPI or client changes needed.

- manage-user-form spec: built the second candidate explicitly instead of
  spreading the first. The client guidelines call out the spread operator with
  this exact pattern as the counter-example, and it was one of only two uses
  across the spec suite.
- UserAdminResource: the delete endpoint's javadoc still claimed the user is
  removed from Keycloak. That stopped being true when the Keycloak call was
  dropped from the flow, and it contradicted the service javadoc directly below
  it. It now says what the endpoint does, which is anonymise the local record
  and leave the Keycloak identity alone.
- UserAdminResourceTest: the create and import payloads went from untyped maps
  to the CreateUserDTO and ImportUserDTO records, matching the sibling
  UpdateUser tests and the rest of the resource tests.

One payload stays a map deliberately. ImportUserDTO is @JsonInclude(NON_EMPTY),
so new ImportUserDTO("") serialises to {} and the blank-value test would still
pass while no longer sending a blank value. There is now a comment saying so.

Verified: 804 server tests, 2063 client tests, typecheck, eslint with no errors
and prettier.

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🤖 No OpenAPI or client changes needed.

…in-page

# Conflicts:
#	src/main/webapp/app/generated/.openapi-generator/FILES
#	src/main/webapp/app/usermanagement/research-group/research-group-add-members/research-group-add-members.component.ts
#	src/main/webapp/i18n/de/global.json
#	src/main/webapp/i18n/en/global.json
@github-actions

Copy link
Copy Markdown
Contributor

🤖 No OpenAPI or client changes needed.

…heir role

Setting a role deleted every role mapping the user had and wrote one back,
so an admin changing someone's role in one group silently removed them from
every other group they belonged to. The form says "set the role", not
"remove from all other groups".

A group-bound role now only replaces the role held in the group it names.
A role that belongs to no group still clears the group-bound ones, since
that is a demotion out of every group rather than a change within one.

Covering this meant the test context could no longer replace UserService
with a plain mock, because the endpoint an admin actually calls delegates
to it and nothing ran. It is a spy now, so unstubbed methods run against
the real database and the two new tests assert what the role change leaves
behind.

That change makes when(...) on this service dangerous: stubbing that way
calls the real method first, which counted an extra invocation in one test
and would have written a real password hash while setting up another. The
twenty existing stubs are doReturn/doThrow now, which do not.

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🤖 No OpenAPI or client changes needed.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

There hasn't been any activity on this pull request recently. Therefore, this pull request has been automatically marked as stale and will be closed if no further activity occurs within seven days. Thank you for your contributions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

client Pull requests that update TypeScript code. (Added Automatically!) ready for review server Pull requests that update Java code. (Added Automatically!) tests user management Code changes in user management module.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Admin: Manage Users page with Keycloak sync + research-group role picker

2 participants