Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
6a79947
General: add admin user-management DTOs
az108 May 8, 2026
6707b14
General: extend KeycloakUserService with delete, findById, and create…
az108 May 8, 2026
f8ad03f
General: add UserRepository.findAllUsersForAdmin query
az108 May 8, 2026
76463af
General: add UserRetentionService.deleteUserByAdmin for on-demand adm…
az108 May 8, 2026
4bffa11
General: add UserAdminService orchestrating Keycloak + DB user CRUD
az108 May 8, 2026
58809c9
General: add UserAdminResource for admin user CRUD endpoints
az108 May 8, 2026
8199d62
General: left-join research group in admin user listing so admins appear
az108 May 8, 2026
a0f08f6
General: let admins pick a role (PROFESSOR or EMPLOYEE) when adding m…
az108 May 8, 2026
7827f42
General: regenerate OpenAPI client for admin user-management endpoints
az108 May 8, 2026
76cfec5
General: add Manage Users admin page wiring, list, and form
az108 May 8, 2026
65f9876
General: add role picker (PROFESSOR or EMPLOYEE) to research-group ad…
az108 May 8, 2026
77a04a2
General: add Vitest specs for ManageUsers list and form
az108 May 8, 2026
3f5421a
Bugfix: fetch user with research-group roles for admin detail view
az108 May 9, 2026
ecdce92
translation keys
az108 May 9, 2026
85f10fb
General: apply Prettier formatting to manage-users files
az108 May 10, 2026
0ed448d
Test: cover UserAdminService listing, detail, update, and optional cr…
az108 May 10, 2026
74468ef
Test: tidy UserAdminService test style and drop redundant verifications
az108 May 10, 2026
e91b843
Merge remote-tracking branch 'origin/main' into feat/manage-users-adm…
az108 May 16, 2026
8a4ecf4
\`Bugfix\`: Migrate admin user-create paths to the per-realm Keycloak…
az108 May 16, 2026
51d8326
Merge branch 'main' into feat/manage-users-admin-page
az108 May 19, 2026
7006cae
Merge branch 'main' into feat/manage-users-admin-page
az108 May 21, 2026
5107d1f
`General`: Address review feedback on Manage Users admin page
az108 May 21, 2026
e601ad7
\`General\`: Align Manage Users form header with project page-header …
az108 May 21, 2026
9ce3e8c
`Test`: Use account service util mock and tighten toast assertions
az108 May 21, 2026
ecbced2
`Development`: Let admins set a user's role and add language labels
az108 May 21, 2026
dd1b545
chore: update OpenAPI spec and generated client
github-actions[bot] May 21, 2026
1deab0f
`General`: Move admin role assignment into UserService
az108 May 21, 2026
208090a
\`Test\`: Cover UserAdminResource update + delete flows
az108 May 21, 2026
58d2e15
Merge branch 'main' into feat/manage-users-admin-page
az108 May 21, 2026
1494b47
Test: Fix prettier formatting in UserAdminResourceTest
az108 May 21, 2026
96cf66d
Merge remote-tracking branch 'origin/main' into feat/manage-users-adm…
az108 Jul 5, 2026
dc308c6
Merge branch 'main' into feat/manage-users-admin-page
az108 Jul 25, 2026
173a0c0
`General`: Address the review findings on the admin user management
az108 Jul 26, 2026
e92c984
Merge remote-tracking branch 'origin/main' into feat/manage-users-adm…
az108 Jul 28, 2026
01db621
`Bugfix`: Keep a user's other research groups when an admin changes t…
az108 Jul 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 224 additions & 0 deletions openapi/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,126 @@ paths:
content:
application/json:
schema: {$ref: '#/components/schemas/AdminExportTaskDTO'}
/api/admin/users:
get:
tags: [user-admin-resource]
operationId: getAllUsers
parameters:
- name: pageSize
in: query
required: false
schema: {type: integer, format: int32, minimum: 1}
- name: pageNumber
in: query
required: false
schema: {type: integer, format: int32, minimum: 0}
- name: sortBy
in: query
required: false
schema: {type: string}
- name: direction
in: query
required: false
schema:
type: string
enum: [ASC, DESC]
- name: roles
in: query
required: false
schema:
type: array
items:
type: string
enum: [APPLICANT, PROFESSOR, ADMIN, EMPLOYEE]
- name: researchGroupIds
in: query
required: false
schema:
type: array
items: {type: string, format: uuid}
- name: searchQuery
in: query
required: false
schema: {type: string}
responses:
'200':
description: OK
content:
application/json:
schema: {$ref: '#/components/schemas/PageAdminUserOverviewDTO'}
post:
tags: [user-admin-resource]
operationId: createUser
requestBody:
content:
application/json:
schema: {$ref: '#/components/schemas/CreateUserDTO'}
required: true
responses:
'200':
description: OK
content:
application/json:
schema: {$ref: '#/components/schemas/AdminUserDetailDTO'}
/api/admin/users/import:
post:
tags: [user-admin-resource]
operationId: importUser
requestBody:
content:
application/json:
schema: {$ref: '#/components/schemas/ImportUserDTO'}
required: true
responses:
'200':
description: OK
content:
application/json:
schema: {$ref: '#/components/schemas/AdminUserDetailDTO'}
/api/admin/users/{userId}:
get:
tags: [user-admin-resource]
operationId: getUser
parameters:
- name: userId
in: path
required: true
schema: {type: string, format: uuid}
responses:
'200':
description: OK
content:
application/json:
schema: {$ref: '#/components/schemas/AdminUserDetailDTO'}
put:
tags: [user-admin-resource]
operationId: updateUser
parameters:
- name: userId
in: path
required: true
schema: {type: string, format: uuid}
requestBody:
content:
application/json:
schema: {$ref: '#/components/schemas/UpdateUserDTO'}
required: true
responses:
'200':
description: OK
content:
application/json:
schema: {$ref: '#/components/schemas/AdminUserDetailDTO'}
delete:
tags: [user-admin-resource]
operationId: deleteUser
parameters:
- name: userId
in: path
required: true
schema: {type: string, format: uuid}
responses:
'200': {description: OK}
/api/ai/analyze-job-description:
post:
tags: [ai-resource]
Expand Down Expand Up @@ -3000,6 +3120,9 @@ components:
items: {$ref: '#/components/schemas/KeycloakUserDTO'}
minItems: 1
researchGroupId: {type: string, format: uuid}
role:
type: string
enum: [APPLICANT, PROFESSOR, ADMIN, EMPLOYEE]
required: [keycloakUsers]
AdminApplicationOverviewDTO:
type: object
Expand Down Expand Up @@ -3064,6 +3187,47 @@ components:
APPLICATIONS_ONLY]
userResearchGroupRoles: {$ref: '#/components/schemas/Counts'}
users: {$ref: '#/components/schemas/Counts'}
AdminUserDetailDTO:
type: object
properties:
aiFeaturesEnabled: {type: boolean}
avatar: {type: string}
birthday: {type: string, format: date}
createdAt: {type: string, format: date-time}
email: {type: string}
firstName: {type: string}
gender: {type: string}
lastActivityAt: {type: string, format: date-time}
lastName: {type: string}
linkedinUrl: {type: string}
nationality: {type: string}
phoneNumber: {type: string}
primaryRole:
type: string
enum: [APPLICANT, PROFESSOR, ADMIN, EMPLOYEE]
researchGroupId: {type: string, format: uuid}
researchGroupName: {type: string}
selectedLanguage: {type: string}
universityId: {type: string}
userId: {type: string, format: uuid}
website: {type: string}
required: [userId]
AdminUserOverviewDTO:
type: object
properties:
avatar: {type: string}
email: {type: string}
firstName: {type: string}
lastActivityAt: {type: string, format: date-time}
lastName: {type: string}
primaryRole:
type: string
enum: [APPLICANT, PROFESSOR, ADMIN, EMPLOYEE]
researchGroupId: {type: string, format: uuid}
researchGroupName: {type: string}
universityId: {type: string}
userId: {type: string, format: uuid}
required: [userId]
AiFeatureStatusDTO:
type: object
properties:
Expand Down Expand Up @@ -3355,6 +3519,26 @@ components:
items: {$ref: '#/components/schemas/SlotInput'}
minItems: 1
required: [slots]
CreateUserDTO:
type: object
properties:
birthday: {type: string, format: date}
email: {type: string, format: email, minLength: 1}
firstName: {type: string, minLength: 1}
gender: {type: string}
lastName: {type: string, minLength: 1}
linkedinUrl: {type: string}
nationality: {type: string}
password: {type: string, maxLength: 2147483647, minLength: 8}
phoneNumber: {type: string}
primaryRole:
type: string
enum: [APPLICANT, PROFESSOR, ADMIN, EMPLOYEE]
researchGroupId: {type: string, format: uuid}
selectedLanguage: {type: string}
universityId: {type: string}
website: {type: string}
required: [email, firstName, lastName, password]
CreatedJobDTO:
type: object
properties:
Expand Down Expand Up @@ -3555,6 +3739,11 @@ components:
sizeBytes: {type: integer, format: int64}
uploadedById: {type: string, format: uuid}
url: {type: string}
ImportUserDTO:
type: object
properties:
universityId: {type: string, minLength: 1}
required: [universityId]
InternalCommentDTO:
type: object
properties:
Expand Down Expand Up @@ -3936,6 +4125,22 @@ components:
sort: {$ref: '#/components/schemas/SortObject'}
totalElements: {type: integer, format: int64}
totalPages: {type: integer, format: int32}
PageAdminUserOverviewDTO:
type: object
properties:
content:
type: array
items: {$ref: '#/components/schemas/AdminUserOverviewDTO'}
empty: {type: boolean}
first: {type: boolean}
last: {type: boolean}
number: {type: integer, format: int32}
numberOfElements: {type: integer, format: int32}
pageable: {$ref: '#/components/schemas/PageableObject'}
size: {type: integer, format: int32}
sort: {$ref: '#/components/schemas/SortObject'}
totalElements: {type: integer, format: int64}
totalPages: {type: integer, format: int32}
PageApplicationOverviewDTO:
type: object
properties:
Expand Down Expand Up @@ -4357,6 +4562,25 @@ components:
properties:
location: {type: string, minLength: 1}
required: [location]
UpdateUserDTO:
type: object
properties:
aiFeaturesEnabled: {type: boolean}
avatar: {type: string}
birthday: {type: string, format: date}
firstName: {type: string}
gender: {type: string}
lastName: {type: string}
linkedinUrl: {type: string}
nationality: {type: string}
phoneNumber: {type: string}
primaryRole:
type: string
enum: [APPLICANT, PROFESSOR, ADMIN, EMPLOYEE]
researchGroupId: {type: string, format: uuid}
selectedLanguage: {type: string}
universityId: {type: string}
website: {type: string}
UpdateUserNameDTO:
type: object
properties:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,34 @@ public void warnUserOfDataDeletion(LocalDateTime warningDate) {
}
}

/**
* Deletes a single user on demand from the admin "Manage Users" page. Reuses the
* existing per-category anonymisation helpers and finally removes the user row.
* Unlike {@link #processUserIdsList(List, LocalDateTime, boolean)}, this method
* does not skip admins — the caller is responsible for preventing self-delete.
*
* @param userId the user UUID to delete
*/
@Transactional
public void deleteUserByAdmin(UUID userId) {
Optional<User> userOpt = userRepository.findWithResearchGroupRolesByUserId(userId);
if (userOpt.isEmpty()) {
log.info("Admin delete: userId={} not found, nothing to do", userId);
return;
}
User user = userOpt.get();
RetentionCategory category = classify(user);

if (category == RetentionCategory.PROFESSOR_OR_EMPLOYEE || category == RetentionCategory.SKIP_ADMIN) {
// 1) Treat admins identically to professor/employee for anonymisation purposes.
handleProfessorOrEmployee(user, false);
} else if (category == RetentionCategory.APPLICANT) {
handleApplicant(user, false);
}
// 2) UNKNOWN users have no FKs to anonymise; jump straight to general cleanup.
handleGeneralData(user, false);
}

// Helper methods for handling different categories

private RetentionCategory classify(User user) {
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/de/tum/cit/aet/core/util/PageUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ public enum ColumnMapping {
*/
SCHOOLS_ADMIN(Set.of("name", "abbreviation")),

/**
* Sortable columns used in admin "Manage Users" listings.
*/
USERS_ADMIN(Set.of("firstName", "lastName", "email", "universityId", "lastActivityAt")),

/**
* Sortable columns used in applicant's application listings.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
package de.tum.cit.aet.usermanagement.dto;

import com.fasterxml.jackson.annotation.JsonInclude;
import de.tum.cit.aet.usermanagement.constants.UserRole;
import jakarta.validation.constraints.NotEmpty;
import java.util.List;
import java.util.UUID;

@JsonInclude(JsonInclude.Include.NON_NULL)
public record AddMembersToResearchGroupDTO(@NotEmpty List<KeycloakUserDTO> keycloakUsers, UUID researchGroupId) {}
public record AddMembersToResearchGroupDTO(@NotEmpty List<KeycloakUserDTO> keycloakUsers, UUID researchGroupId, UserRole role) {
/**
* Returns the role to assign to the added members, defaulting to {@link UserRole#EMPLOYEE}
* when no role was specified by the caller (back-compat).
*
* @return the resolved {@link UserRole}, never {@code null}
*/
public UserRole roleOrDefault() {
return role == null ? UserRole.EMPLOYEE : role;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package de.tum.cit.aet.usermanagement.dto;

import com.fasterxml.jackson.annotation.JsonInclude;
import de.tum.cit.aet.usermanagement.constants.UserRole;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.UUID;

/** Full admin-scoped user detail DTO returned by the view / edit page. */
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record AdminUserDetailDTO(
@NotNull UUID userId,
String firstName,
String lastName,
String email,
String avatar,
String universityId,
UserRole primaryRole,
UUID researchGroupId,
String researchGroupName,
String phoneNumber,
String gender,
String nationality,
LocalDate birthday,
String website,
String linkedinUrl,
String selectedLanguage,
Boolean aiFeaturesEnabled,
LocalDateTime createdAt,
LocalDateTime lastActivityAt
) {}
Loading
Loading