Skip to content

Commit

Permalink
Added server-side pagination API
Browse files Browse the repository at this point in the history
  • Loading branch information
oharsta committed Oct 21, 2024
1 parent 4c2daf7 commit 510c9bd
Show file tree
Hide file tree
Showing 14 changed files with 378 additions and 53 deletions.
6 changes: 3 additions & 3 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@surfnet/sds": "^0.0.113",
"dompurify": "^3.1.6",
"@surfnet/sds": "^0.0.114",
"dompurify": "^3.1.7",
"i18n-js": "^4.4.3",
"isomorphic-dompurify": "^2.15.0",
"isomorphic-dompurify": "^2.16.0",
"js-cookie": "^3.0.5",
"lodash.debounce": "^4.0.8",
"luxon": "^3.5.0",
Expand Down
2 changes: 1 addition & 1 deletion client/src/tabs/UserRoles.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ export const UserRoles = ({role, guests, userRoles}) => {

const displayEndDate = userRole => {
const allowed = allowedToRenewUserRole(user, userRole, false);
if (allowed && userRole.authority !== AUTHORITIES.GUEST) {
if (allowed && !guests) {
return (
<MinimalDateField
minDate={futureDate(1)}
Expand Down
57 changes: 26 additions & 31 deletions client/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1873,10 +1873,10 @@
dependencies:
"@sinonjs/commons" "^1.7.0"

"@surfnet/sds@^0.0.113":
version "0.0.113"
resolved "https://registry.yarnpkg.com/@surfnet/sds/-/sds-0.0.113.tgz#09f8f110134cc7f4a15647e9d980fdaa634e08b1"
integrity sha512-rrnC4rxO5COv8R7PyfQTkIGLxZfOMYqo6dA05/UP3TV+INUkPYMwuLnJqBOG0d97PapfIGTUnXbEUWz0oE+NHg==
"@surfnet/sds@^0.0.114":
version "0.0.114"
resolved "https://registry.yarnpkg.com/@surfnet/sds/-/sds-0.0.114.tgz#d0a8234c8a1a02ca2d559b4073333bb3b068c641"
integrity sha512-q57yOD+pOOnkgHPmzjDK1px7Xvi8qamhZWUf9fsGiuKVvvPnosSMwe8dAhKDfWQFPfRvjpmGvvNPKNmedIB2PA==

"@surma/rollup-plugin-off-main-thread@^2.2.3":
version "2.2.3"
Expand Down Expand Up @@ -3781,12 +3781,12 @@ cssstyle@^2.3.0:
dependencies:
cssom "~0.3.6"

cssstyle@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-4.0.1.tgz#ef29c598a1e90125c870525490ea4f354db0660a"
integrity sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==
cssstyle@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-4.1.0.tgz#161faee382af1bafadb6d3867a92a19bcb4aea70"
integrity sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==
dependencies:
rrweb-cssom "^0.6.0"
rrweb-cssom "^0.7.1"

csstype@^3.0.2:
version "3.1.3"
Expand Down Expand Up @@ -4079,10 +4079,10 @@ domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1:
dependencies:
domelementtype "^2.2.0"

dompurify@^3.1.6:
version "3.1.6"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.1.6.tgz#43c714a94c6a7b8801850f82e756685300a027e2"
integrity sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==
dompurify@^3.1.7:
version "3.1.7"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.1.7.tgz#711a8c96479fb6ced93453732c160c3c72418a6a"
integrity sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==

domutils@^1.7.0:
version "1.7.0"
Expand Down Expand Up @@ -5788,14 +5788,14 @@ isexe@^2.0.0:
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==

isomorphic-dompurify@^2.15.0:
version "2.15.0"
resolved "https://registry.yarnpkg.com/isomorphic-dompurify/-/isomorphic-dompurify-2.15.0.tgz#d3d35fe8cab700c4cf3c065da3dc86d508161502"
integrity sha512-RDHlyeVmwEDAPZuX1VaaBzSn9RrsfvswxH7faEQK9cTHC1dXeNuK6ElUeSr7locFyeLguut8ASfhQWxHB4Ttug==
isomorphic-dompurify@^2.16.0:
version "2.16.0"
resolved "https://registry.yarnpkg.com/isomorphic-dompurify/-/isomorphic-dompurify-2.16.0.tgz#c46ec33ae6bde43648bd6163925625949113a696"
integrity sha512-cXhX2owp8rPxafCr0ywqy2CGI/4ceLNgWkWBEvUz64KTbtg3oRL2ZRqq/zW0pzt4YtDjkHLbwcp/lozpKzAQjg==
dependencies:
"@types/dompurify" "^3.0.5"
dompurify "^3.1.6"
jsdom "^25.0.0"
dompurify "^3.1.7"
jsdom "^25.0.1"

istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0:
version "3.2.2"
Expand Down Expand Up @@ -6414,12 +6414,12 @@ jsdom@^16.6.0:
ws "^7.4.6"
xml-name-validator "^3.0.0"

jsdom@^25.0.0:
version "25.0.0"
resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-25.0.0.tgz#d1612b4ddab85af56821b2f731e15faae135f4e1"
integrity sha512-OhoFVT59T7aEq75TVw9xxEfkXgacpqAhQaYgP9y/fDqWQCMB/b1H66RfmPm/MaeaAIU9nDwMOVTlPN51+ao6CQ==
jsdom@^25.0.1:
version "25.0.1"
resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-25.0.1.tgz#536ec685c288fc8a5773a65f82d8b44badcc73ef"
integrity sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==
dependencies:
cssstyle "^4.0.1"
cssstyle "^4.1.0"
data-urls "^5.0.0"
decimal.js "^10.4.3"
form-data "^4.0.0"
Expand All @@ -6432,7 +6432,7 @@ jsdom@^25.0.0:
rrweb-cssom "^0.7.1"
saxes "^6.0.0"
symbol-tree "^3.2.4"
tough-cookie "^4.1.4"
tough-cookie "^5.0.0"
w3c-xmlserializer "^5.0.0"
webidl-conversions "^7.0.0"
whatwg-encoding "^3.1.1"
Expand Down Expand Up @@ -8497,11 +8497,6 @@ rollup@^2.43.1:
optionalDependencies:
fsevents "~2.3.2"

rrweb-cssom@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz#ed298055b97cbddcdeb278f904857629dec5e0e1"
integrity sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==

rrweb-cssom@^0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz#c73451a484b86dd7cfb1e0b2898df4b703183e4b"
Expand Down Expand Up @@ -9376,7 +9371,7 @@ [email protected]:
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==

tough-cookie@^4.0.0, tough-cookie@^4.1.4:
tough-cookie@^4.0.0, tough-cookie@^4.1.4, tough-cookie@^5.0.0:
version "4.1.4"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.4.tgz#945f1461b45b5a8c76821c33ea49c3ac192c1b36"
integrity sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==
Expand Down
18 changes: 10 additions & 8 deletions server/src/main/java/access/api/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -143,12 +143,12 @@ public ResponseEntity<List<User>> search(@RequestParam(value = "query") String q
}

@GetMapping("search-paginated")
public ResponseEntity<Page<User>> searchPaginated(@RequestParam(value = "query") String query,
@RequestParam(value = "pageNumber", required = false, defaultValue = "0") int pageNumber,
@RequestParam(value = "pageSize", required = false, defaultValue = "10") int pageSize,
@RequestParam(value = "sort", required = false, defaultValue = "id") String sort,
@RequestParam(value = "sortDirection", required = false, defaultValue = "ASC") String sortDirection,
@Parameter(hidden = true) User user) {
public ResponseEntity<Page<?>> searchPaginated(@RequestParam(value = "query", required = false, defaultValue = "") String query,
@RequestParam(value = "pageNumber", required = false, defaultValue = "0") int pageNumber,
@RequestParam(value = "pageSize", required = false, defaultValue = "10") int pageSize,
@RequestParam(value = "sort", required = false, defaultValue = "id") String sort,
@RequestParam(value = "sortDirection", required = false, defaultValue = "ASC") String sortDirection,
@Parameter(hidden = true) User user) {
LOG.debug(String.format("/search-paginated for user %s", user.getEduPersonPrincipalName()));
UserPermissions.assertSuperUser(user);
if (query.equals("owl")) {
Expand All @@ -157,8 +157,10 @@ public ResponseEntity<Page<User>> searchPaginated(@RequestParam(value = "query")
return ResponseEntity.ok(new PageImpl<>(content, pageRequest, content.size()));
}
Pageable pageable = PageRequest.of(pageNumber, pageSize, Sort.by(Sort.Direction.fromString(sortDirection), sort));
Page<User> users = userRepository.searchByPage(query.replaceAll("@", " ") + "*", pageable);
return ResponseEntity.ok(users);
Page<Map<String, Object>> page = StringUtils.hasText(query) ?
userRepository.searchByPage(pageable) :
userRepository.searchByPageWithKeyword(query.replaceAll("@", " ") + "*", pageable) ;
return ResponseEntity.ok(page);
}

@GetMapping("search-by-application")
Expand Down
32 changes: 31 additions & 1 deletion server/src/main/java/access/api/UserRoleController.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
Expand Down Expand Up @@ -91,7 +95,34 @@ public ResponseEntity<List<Map<String, Object>>> consequencesDeleteRole(@PathVar
return ResponseEntity.ok(res);
}

@GetMapping("/search/{roleId}/{guests}")
public ResponseEntity<Page<?>> searchPaginated(@PathVariable("roleId") Long roleId,
@PathVariable("guests") boolean guests,
@RequestParam(value = "query", required = false, defaultValue = "") String query,
@RequestParam(value = "pageNumber", required = false, defaultValue = "0") int pageNumber,
@RequestParam(value = "pageSize", required = false, defaultValue = "10") int pageSize,
@RequestParam(value = "sort", required = false, defaultValue = "id") String sort,
@RequestParam(value = "sortDirection", required = false, defaultValue = "ASC") String sortDirection,
@Parameter(hidden = true) User user) {
LOG.debug(String.format("/search for user %s", user.getEduPersonPrincipalName()));

Role role = roleRepository.findById(roleId).orElseThrow(() -> new NotFoundException("Role not found"));

UserPermissions.assertRoleAccess(user, role, Authority.INVITER);

Pageable pageable = PageRequest.of(pageNumber, pageSize, Sort.by(Sort.Direction.fromString(sortDirection), sort));
Page<Map<String, Object>> page;
if (StringUtils.hasText(query)) {
page = guests ?
userRoleRepository.searchGuestsByPageWithKeyword(roleId, query, pageable) :
userRoleRepository.searchNonGuestsByPageWithKeyword(roleId, query, pageable);
} else {
page = guests ?
userRoleRepository.searchGuestsByPage(roleId, pageable) :
userRoleRepository.searchNonGuestsByPage(roleId, pageable);
}
return ResponseEntity.ok(page);
}

@PostMapping("user_role_provisioning")
@Operation(summary = "Add Role to a User", description = "Provision the User if the User is unknown and add the Role(s)")
Expand Down Expand Up @@ -176,5 +207,4 @@ public ResponseEntity<Void> deleteUserRole(@PathVariable("id") Long id,
return Results.deleteResult();
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ public ResponseEntity<Void> deleteRole(@PathVariable("id") Long id,
@PostMapping("/invitations")
@PreAuthorize("hasRole('SP_DASHBOARD')")
@Operation(summary = "Invite member for existing Role",
description = "Invite a member for an existing role. An invitation email will be send",
description = "Invite a member for an existing role. An invitation email will be sent. Do not forget to set <guestRoleIncluded> to <true>",
requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
useParameterTypeSchema = true,
content = {@Content(examples = {@ExampleObject(value = """
Expand Down
29 changes: 29 additions & 0 deletions server/src/main/java/access/repository/RoleRepository.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package access.repository;

import access.model.Role;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Map;
import java.util.Optional;

@Repository
Expand All @@ -16,6 +19,32 @@ public interface RoleRepository extends JpaRepository<Role, Long> {
nativeQuery = true)
List<Role> search(String keyWord, int limit);


@Query(value = """
SELECT r.id, r.name, r.description, a.manage_id, a.manage_type,
(SELECT COUNT(*) FROM user_roles ur WHERE ur.role_id=r.id) as userRoleCount
FROM roles r INNER JOIN application_usages au on au.role_id = r.id
INNER JOIN applications a on au.application_id = a.id
""",
countQuery = """
SELECT COUNT(r.id) FROM roles r
""",
nativeQuery = true)
Page<Map<String, Object>> searchByPage(Pageable pageable);

@Query(value = """
SELECT r.id, r.name, r.description, a.manage_id, a.manage_type,
(SELECT COUNT(*) FROM user_roles ur WHERE ur.role_id=r.id) as userRoleCount
FROM roles r INNER JOIN application_usages au on au.role_id = r.id
INNER JOIN applications a on au.application_id = a.id
WHERE MATCH (name, description) against (?1 IN BOOLEAN MODE)
""",
countQuery = """
SELECT COUNT(r.id) FROM roles r WHERE MATCH (name, description) against (?1 IN BOOLEAN MODE)
""",
nativeQuery = true)
Page<Map<String, Object>> searchByPageWithKeyword(String keyword, Pageable pageable);

List<Role> findByApplicationUsagesApplicationManageId(String manageId);

List<Role> findByOrganizationGUID(String organizationGUID);
Expand Down
16 changes: 14 additions & 2 deletions server/src/main/java/access/repository/UserRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,22 @@ public interface UserRepository extends JpaRepository<User, Long> {
nativeQuery = true)
List<Map<String, Object>> searchByApplication(List<String> manageIdentifiers, String keyWord, int limit);

@Query(value = "SELECT * FROM users WHERE MATCH (given_name, family_name, email) against (?1 IN BOOLEAN MODE)",
@Query(value = """
select u.name, u.email, u.schac_home_organization, u.sub, u.super_user, u.institution_admin,
(SELECT GROUP_CONCAT(DISTINCT ur.authority) FROM user_roles ur WHERE ur.user_id = u.id) AS authority from users u
""",
countQuery = "SELECT count(*) FROM users",
nativeQuery = true)
Page<Map<String, Object>> searchByPage(Pageable pageable );

@Query(value = """
select u.name, u.email, u.schac_home_organization, u.sub, u.super_user, u.institution_admin,
(SELECT GROUP_CONCAT(DISTINCT ur.authority) FROM user_roles ur WHERE ur.user_id = u.id) AS authority
from users u WHERE MATCH (given_name, family_name, email) against (?1 IN BOOLEAN MODE)
""",
countQuery = "SELECT count(*) FROM users WHERE MATCH (given_name, family_name, email) against (?1 IN BOOLEAN MODE)",
nativeQuery = true)
Page<User> searchByPage(String keyWord, Pageable pageable );
Page<Map<String, Object>> searchByPageWithKeyword(String keyWord, Pageable pageable );

@Query(value = "SELECT u.id, u.email, u.name, u.schac_home_organization, u.created_at, u.last_activity, " +
"ur.authority, r.name AS role_name, r.id AS role_id, ur.end_date " +
Expand Down
54 changes: 54 additions & 0 deletions server/src/main/java/access/repository/UserRoleRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import access.model.Role;
import access.model.UserRole;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
Expand All @@ -11,6 +13,7 @@

import java.time.Instant;
import java.util.List;
import java.util.Map;

@Repository
public interface UserRoleRepository extends JpaRepository<UserRole, Long> {
Expand All @@ -32,4 +35,55 @@ public interface UserRoleRepository extends JpaRepository<UserRole, Long> {
@Query(value = "UPDATE user_roles SET expiry_notifications= ?1 WHERE id = ?2", nativeQuery = true)
@Transactional(isolation = Isolation.SERIALIZABLE)
void updateExpiryNotifications(Integer expiryNotifications, Long id);


@Query(value = """
SELECT ur.authority, ur.end_date, ur.created_at, u.name, u.email, u.schac_home_organization
FROM user_roles ur INNER JOIN users u on u.id = ur.user_id
WHERE ur.role_id = ?1 AND (ur.authority = 'GUEST' OR ur.guest_role_included )
""",
countQuery = """
SELECT COUNT(ur.id) FROM user_roles ur INNER JOIN users u on u.id = ur.user_id
WHERE ur.role_id = ?1 AND (ur.authority = 'GUEST' OR ur.guest_role_included )
""",
nativeQuery = true)
Page<Map<String, Object>> searchGuestsByPage(Long roleId, Pageable pageable);

@Query(value = """
SELECT ur.authority, ur.end_date, ur.created_at, u.name, u.email, u.schac_home_organization
FROM user_roles ur INNER JOIN users u on u.id = ur.user_id
WHERE ur.role_id = ?1 AND (ur.authority = 'GUEST' OR ur.guest_role_included )
AND MATCH (u.given_name, u.family_name, u.email) AGAINST (?2 IN BOOLEAN MODE)
""",
countQuery = """
SELECT COUNT(ur.id) FROM user_roles ur INNER JOIN users u on u.id = ur.user_id
WHERE ur.role_id = ?1 AND (ur.authority = 'GUEST' OR ur.guest_role_included )
AND MATCH (u.given_name, u.family_name, u.email) AGAINST (?2 IN BOOLEAN MODE)
""",
nativeQuery = true)
Page<Map<String, Object>> searchGuestsByPageWithKeyword(Long roleId, String keyWord, Pageable pageable);

@Query(value = """
SELECT ur.authority, ur.end_date, ur.created_at, u.name, u.email, u.schac_home_organization
FROM user_roles ur INNER JOIN users u on u.id = ur.user_id WHERE ur.role_id = ?1 AND ur.authority <> 'GUEST'
""",
countQuery = """
SELECT COUNT(ur.id) FROM user_roles ur INNER JOIN users u on u.id = ur.user_id
WHERE ur.role_id = ?1 AND ur.authority <> 'GUEST'
""",
nativeQuery = true)
Page<Map<String, Object>> searchNonGuestsByPage(Long roleId, Pageable pageable);

@Query(value = """
SELECT ur.authority, ur.end_date, ur.created_at, u.name, u.email, u.schac_home_organization
FROM user_roles ur INNER JOIN users u on u.id = ur.user_id WHERE ur.role_id = ?1
AND ur.authority <> 'GUEST' AND MATCH (u.given_name, u.family_name, u.email) AGAINST (?2 IN BOOLEAN MODE)
""",
countQuery = """
SELECT COUNT(ur.id) FROM user_roles ur INNER JOIN users u on u.id = ur.user_id
WHERE ur.role_id = ?1 AND ur.authority <> 'GUEST'
AND MATCH (u.given_name, u.family_name, u.email) AGAINST (?2 IN BOOLEAN MODE)
""",
nativeQuery = true)
Page<Map<String, Object>> searchNonGuestsByPageWithKeyword(Long roleId, String keyWord, Pageable pageable);
}
Loading

0 comments on commit 510c9bd

Please sign in to comment.