diff --git a/.env.example b/.env.example index 045895946626fc01dcbd3573d30f9836496f2db6..a5773760e786c31e97a23493d756c0b1f454ac42 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,5 @@ API="http://fda-gateway-service:9095" -MAIL_USERNAME="eMATRIKELNUMMER" // TU student e-mail server -MAIL_PASSWORD="PASSWORD" // TU student e-mail server -ADMIN_PASSWORD="admin" \ No newline at end of file +MAIL_HOST="stmp.example.com" +MAIL_PORT="587" +MAIL_USERNAME="user" +MAIL_PASSWORD="pass" \ No newline at end of file diff --git a/fda-authentication-service/rest-service/src/main/java/at/tuwien/endpoints/AuthenticationEndpoint.java b/fda-authentication-service/rest-service/src/main/java/at/tuwien/endpoints/AuthenticationEndpoint.java index 3d21d5e1d1be172cbad01112b61a2b7a4d261659..48fb5183c665e648ce9e132a46675c2514c37b18 100644 --- a/fda-authentication-service/rest-service/src/main/java/at/tuwien/endpoints/AuthenticationEndpoint.java +++ b/fda-authentication-service/rest-service/src/main/java/at/tuwien/endpoints/AuthenticationEndpoint.java @@ -3,6 +3,7 @@ package at.tuwien.endpoints; import at.tuwien.api.auth.JwtResponseDto; import at.tuwien.api.auth.LoginRequestDto; import at.tuwien.api.user.UserDto; +import at.tuwien.exception.OrcidMalformedException; import at.tuwien.exception.UserEmailNotVerifiedException; import at.tuwien.exception.UserNotFoundException; import at.tuwien.mapper.UserMapper; @@ -50,7 +51,7 @@ public class AuthenticationEndpoint { @PutMapping @Transactional(readOnly = true) @Operation(summary = "Validate token", security = @SecurityRequirement(name = "bearerAuth")) - public ResponseEntity<UserDto> authenticateUser(Principal principal) throws UserNotFoundException { + public ResponseEntity<UserDto> authenticateUser(Principal principal) throws UserNotFoundException, OrcidMalformedException { final UserDto user = userMapper.userToUserDto(userService.findByUsername(principal.getName())); return ResponseEntity.accepted() .body(user); diff --git a/fda-authentication-service/rest-service/src/main/java/at/tuwien/endpoints/UserEndpoint.java b/fda-authentication-service/rest-service/src/main/java/at/tuwien/endpoints/UserEndpoint.java index c7665d6fd6a8f7502da64e30b27e7a9a251cc5cf..aa3e3a1cf0c1bb564942a7ddbe6d877ca72f0dae 100644 --- a/fda-authentication-service/rest-service/src/main/java/at/tuwien/endpoints/UserEndpoint.java +++ b/fda-authentication-service/rest-service/src/main/java/at/tuwien/endpoints/UserEndpoint.java @@ -30,6 +30,7 @@ import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; import javax.validation.constraints.NotNull; import java.security.Principal; +import java.util.LinkedList; import java.util.List; import java.util.stream.Collectors; @@ -62,11 +63,13 @@ public class UserEndpoint { @Transactional(readOnly = true) @PreAuthorize("hasRole('ROLE_DATA_STEWARD') or hasRole('ROLE_DEVELOPER')") @Operation(summary = "List users", security = @SecurityRequirement(name = "bearerAuth")) - public ResponseEntity<List<UserDto>> list() { + public ResponseEntity<List<UserDto>> list() throws OrcidMalformedException { final List<User> users = userService.findAll(); - return ResponseEntity.ok(users.stream() - .map(userMapper::userToUserDto) - .collect(Collectors.toList())); + final List<UserDto> out = new LinkedList<>(); + for (User user : users) { + out.add(userMapper.userToUserDto(user)); + } + return ResponseEntity.ok(out); } @PostMapping @@ -74,7 +77,7 @@ public class UserEndpoint { @Operation(summary = "Create user") public ResponseEntity<UserDto> register(@NotNull @Valid @RequestBody SignupRequestDto data) throws UserEmailExistsException, - UserNameExistsException, RoleNotFoundException, UserEmailFailedException, BrokerUserCreationException { + UserNameExistsException, RoleNotFoundException, UserEmailFailedException, BrokerUserCreationException, OrcidMalformedException { final User user = userService.create(data); queueService.createUser(data); final Token token = tokenService.create(user); @@ -90,7 +93,7 @@ public class UserEndpoint { @Transactional @Operation(summary = "Forgot user information") public ResponseEntity<UserDto> forgot(@NotNull @Valid @RequestBody UserForgotDto data) - throws UserNotFoundException, UserEmailFailedException { + throws UserNotFoundException, UserEmailFailedException, OrcidMalformedException { final User user = userService.forgot(data); final Token token = tokenService.create(user); final Context context = new Context(); @@ -118,23 +121,11 @@ public class UserEndpoint { httpServletResponse.setStatus(302); } - @PutMapping("/token") - @Transactional - @PreAuthorize("hasRole('ROLE_RESEARCHER')") - @Operation(summary = "Update user token", security = @SecurityRequirement(name = "bearerAuth")) - public ResponseEntity<UserDto> updateTokens(@NotNull @Valid @RequestBody UserTokenModifyDto data, - @NotNull Principal principal) - throws UserNotFoundException { - final User entity = userService.updateToken(data, principal); - return ResponseEntity.status(HttpStatus.ACCEPTED) - .body(userMapper.userToUserDto(entity)); - } - @GetMapping("/{id}") @Transactional(readOnly = true) @PreAuthorize("hasRole('ROLE_DEVELOPER') or hasPermission(#id, 'READ_USER')") @Operation(summary = "Find some user", security = @SecurityRequirement(name = "bearerAuth")) - public ResponseEntity<UserDto> find(@NotNull @PathVariable("id") Long id) throws UserNotFoundException { + public ResponseEntity<UserDto> find(@NotNull @PathVariable("id") Long id) throws UserNotFoundException, OrcidMalformedException { final User entity = userService.find(id); return ResponseEntity.status(HttpStatus.OK) .body(userMapper.userToUserDto(entity)); @@ -146,7 +137,7 @@ public class UserEndpoint { @Operation(summary = "Update user", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<UserDto> update(@NotNull @PathVariable("id") Long id, @NotNull @Valid @RequestBody UserUpdateDto data) - throws UserNotFoundException { + throws UserNotFoundException, OrcidMalformedException { final User entity = userService.update(id, data); return ResponseEntity.status(HttpStatus.ACCEPTED) .body(userMapper.userToUserDto(entity)); @@ -158,7 +149,7 @@ public class UserEndpoint { @Operation(summary = "Update user roles", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<UserDto> updateRoles(@NotNull @PathVariable("id") Long id, @NotNull @Valid @RequestBody UserRolesDto data) - throws UserNotFoundException, RoleNotFoundException, RoleUniqueException { + throws UserNotFoundException, RoleNotFoundException, RoleUniqueException, OrcidMalformedException { final User entity = userService.updateRoles(id, data); return ResponseEntity.status(HttpStatus.ACCEPTED) .body(userMapper.userToUserDto(entity)); @@ -170,7 +161,7 @@ public class UserEndpoint { @Operation(summary = "Update user password", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<UserDto> updatePassword(@NotNull @PathVariable("id") Long id, @NotNull @Valid @RequestBody UserPasswordDto data) - throws UserNotFoundException, BrokerUserCreationException { + throws UserNotFoundException, BrokerUserCreationException, OrcidMalformedException { final User entity = userService.updatePassword(id, data); queueService.modifyUserPassword(entity, data); return ResponseEntity.status(HttpStatus.ACCEPTED) @@ -183,7 +174,7 @@ public class UserEndpoint { @Operation(summary = "Update user email", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<UserDto> updateEmail(@NotNull @PathVariable("id") Long id, @NotNull @Valid @RequestBody UserEmailDto data) - throws UserNotFoundException { + throws UserNotFoundException, OrcidMalformedException { final User entity = userService.updateEmail(id, data); return ResponseEntity.status(HttpStatus.ACCEPTED) .body(userMapper.userToUserDto(entity)); diff --git a/fda-authentication-service/rest-service/src/main/resources/application-docker.yml b/fda-authentication-service/rest-service/src/main/resources/application-docker.yml index a9589c3777f27824ddb5e11722d57fc25bc2a22c..78f3bd5c161dd3c9f6eab2dce483d1d2ea3ec2f0 100644 --- a/fda-authentication-service/rest-service/src/main/resources/application-docker.yml +++ b/fda-authentication-service/rest-service/src/main/resources/application-docker.yml @@ -52,5 +52,5 @@ fda: replyto: "${MAIL_REPLY_TO}" jwt: issuer: "${JWT_ISSUER}" - secret: "${JWT_SECRET" + secret: "${JWT_SECRET}" expiration.ms: "${JWT_EXPIRATION}" # 24 hrs \ No newline at end of file diff --git a/fda-authentication-service/rest-service/src/main/resources/application.yml b/fda-authentication-service/rest-service/src/main/resources/application.yml index a9f6e883099bb10d2aacfa9f5ea9d56a05e5d7ac..bdb5b07e6d3b79ab38dfee0d0a177ab606178311 100644 --- a/fda-authentication-service/rest-service/src/main/resources/application.yml +++ b/fda-authentication-service/rest-service/src/main/resources/application.yml @@ -22,8 +22,8 @@ spring: loadbalancer.ribbon.enabled: false mail: default-encoding: UTF-8 - host: "${SMTP_HOST}" - port: "${SMTP_PORT}" + host: mail.student.tuwien.ac.at + port: 993 username: "${SMTP_USERNAME}" password: "${SMTP_PASSWORD}" properties.mail.smtp: diff --git a/fda-authentication-service/services/src/main/java/at/tuwien/exception/OrcidMalformedException.java b/fda-authentication-service/services/src/main/java/at/tuwien/exception/OrcidMalformedException.java new file mode 100644 index 0000000000000000000000000000000000000000..29c85b49142eef8515532214effd45e5a60e231a --- /dev/null +++ b/fda-authentication-service/services/src/main/java/at/tuwien/exception/OrcidMalformedException.java @@ -0,0 +1,20 @@ +package at.tuwien.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.BAD_REQUEST) +public class OrcidMalformedException extends Exception { + + public OrcidMalformedException(String msg) { + super(msg); + } + + public OrcidMalformedException(String msg, Throwable thr) { + super(msg, thr); + } + + public OrcidMalformedException(Throwable thr) { + super(thr); + } +} diff --git a/fda-authentication-service/services/src/main/java/at/tuwien/mapper/UserMapper.java b/fda-authentication-service/services/src/main/java/at/tuwien/mapper/UserMapper.java index 3819e30bce2bfdbd21854ec86f10e4c613ad9376..05b1aa764b67395f4759adab83c46cb356159427 100644 --- a/fda-authentication-service/services/src/main/java/at/tuwien/mapper/UserMapper.java +++ b/fda-authentication-service/services/src/main/java/at/tuwien/mapper/UserMapper.java @@ -7,6 +7,7 @@ import at.tuwien.api.auth.SignupRequestDto; import at.tuwien.api.user.*; import at.tuwien.entities.user.RoleType; import at.tuwien.entities.user.User; +import at.tuwien.exception.OrcidMalformedException; import org.mapstruct.Mapper; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.GrantedAuthority; @@ -47,7 +48,7 @@ public interface UserMapper { } @Transactional(readOnly = true) - default UserDto userToUserDto(User data) { + default UserDto userToUserDto(User data) throws OrcidMalformedException { return UserDto.builder() .id(data.getId()) .username(data.getUsername()) @@ -58,7 +59,8 @@ public interface UserMapper { .titlesBefore(data.getTitlesBefore()) .titlesAfter(data.getTitlesAfter()) .emailVerified(data.getEmailVerified()) - .hasInvenioToken(data.getInvenioToken() != null) + .affiliation(data.getAffiliation()) + .orcid(userToUncompressedOrcid(data)) .authorities(data.getRoles() .stream() .map(this::roleTypeToGrantedAuthorityDto) @@ -82,6 +84,32 @@ public interface UserMapper { .build(); } + default String userUpdateDtoToCompressedOrcid(UserUpdateDto data) { + if (data.getOrcid() == null) { + return null; + } + return data.getOrcid().replace("-", ""); + } + + default String userToUncompressedOrcid(User data) throws OrcidMalformedException { + if (data.getOrcid() == null) { + return null; + } + if (data.getOrcid().length() != 16) { + log.error("Provided ORCID is not compressed"); + log.debug("provided orcid {} is not compressed, length is {}", data.getOrcid(), data.getOrcid().length()); + throw new OrcidMalformedException("Provided ORCID is not compressed"); + } + return new StringBuilder(data.getOrcid().substring(0, 4)) + .append("-") + .append(data.getOrcid(), 4, 8) + .append("-") + .append(data.getOrcid(), 8, 12) + .append("-") + .append(data.getOrcid(), 12, 16) + .toString(); + } + default GrantVirtualHostPermissionsDto signupRequestDtoToGrantComponentDto() { return GrantVirtualHostPermissionsDto.builder() .virtualHost("/") diff --git a/fda-authentication-service/services/src/main/java/at/tuwien/service/UserService.java b/fda-authentication-service/services/src/main/java/at/tuwien/service/UserService.java index bd9ecbe2e5ae2faaca3e21e179e48783caf76f3d..d7877d78a2494ffcb6e9274b3dd705299437c9a2 100644 --- a/fda-authentication-service/services/src/main/java/at/tuwien/service/UserService.java +++ b/fda-authentication-service/services/src/main/java/at/tuwien/service/UserService.java @@ -74,7 +74,7 @@ public interface UserService { * @return The updated user. * @throws UserNotFoundException The user was not found. */ - User update(Long id, UserUpdateDto data) throws UserNotFoundException; + User update(Long id, UserUpdateDto data) throws UserNotFoundException, OrcidMalformedException; /** * Updates a user with given id and updated roles. @@ -98,16 +98,6 @@ public interface UserService { */ User updatePassword(Long id, UserPasswordDto data) throws UserNotFoundException; - /** - * Updates a user with the given id and updated Invenio tokens. - * - * @param data The updated Invenio token. - * @param principal The authentication principal. - * @return The updated user. - * @throws UserNotFoundException The user was not found. - */ - User updateToken(UserTokenModifyDto data, Principal principal) throws UserNotFoundException; - /** * Updates a user with the given id and updated email. * diff --git a/fda-authentication-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java b/fda-authentication-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java index 627a75dceb2bf23d12298ba67fd296d4b1e6b1c3..1a258e19c8873538d16de789d052bb5aeb353b50 100644 --- a/fda-authentication-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java +++ b/fda-authentication-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java @@ -127,15 +127,23 @@ public class UserServiceImpl implements UserService { @Override @Transactional - public User update(Long id, UserUpdateDto data) throws UserNotFoundException { + public User update(Long id, UserUpdateDto data) throws UserNotFoundException, OrcidMalformedException { /* check */ final User user = find(id); + /* check */ + if (data.getOrcid() != null && !validateOrcid(data.getOrcid())) { + log.error("Checksum of the provided ORCID does not match"); + log.debug("checksum of the provided orcid {} does not match", data.getOrcid()); + throw new OrcidMalformedException(data.getOrcid()); + } /* save */ user.setTitlesBefore(data.getTitlesBefore()); user.setTitlesAfter(data.getTitlesAfter()); user.setFirstname(data.getFirstname()); user.setLastname(data.getLastname()); user.setUsername(user.getUsername()); + user.setAffiliation(data.getAffiliation()); + user.setOrcid(userMapper.userUpdateDtoToCompressedOrcid(data)); log.debug("mapped data {} to new user {}", data, user); final User entity = userRepository.save(user); log.info("Updated user with id {}", entity.getId()); @@ -143,6 +151,37 @@ public class UserServiceImpl implements UserService { return entity; } + /** + * Validates a given ORCID checksum (ISO 7064 11,2) + * Source: https://support.orcid.org/hc/en-us/articles/360006897674-Structure-of-the-ORCID-Identifier + * + * @param orcid The ORCID. + * @return True if the ORCID provided is valid, false otherwise. + */ + protected static Boolean validateOrcid(String orcid) { + if (orcid == null) { + return true; + } + if (orcid.length() != 19) { + log.error("Provided ORCID has an invalid length"); + log.debug("provided orcid {} has an invalid length {}, is not 19", orcid, orcid.length()); + return false; + } + int total = 0; + for (int i = 0; i < orcid.length() - 1; i++) { + if (orcid.charAt(i) == '-') { + continue; + } + int digit = Character.getNumericValue(orcid.charAt(i)); + total = (total + digit) * 2; + } + int remainder = total % 11; + int result = (12 - remainder) % 11; + final String check = result == 10 ? "X" : String.valueOf(result); + log.trace("orcid checksum is '{}'", check); + return orcid.substring(18).equals(check); + } + @Override @Transactional public User updateRoles(Long id, UserRolesDto data) @@ -187,19 +226,6 @@ public class UserServiceImpl implements UserService { return entity; } - @Override - @Transactional - public User updateToken(UserTokenModifyDto data, Principal principal) throws UserNotFoundException { - /* check */ - final User user = findByUsername(principal.getName()); - /* save */ - user.setInvenioToken(data.getInvenioToken()); - final User entity = userRepository.save(user); - log.info("Updated user with id {}", entity.getId()); - log.debug("updated user {}", entity); - return entity; - } - @Override @Transactional public User updateEmail(Long id, UserEmailDto data) throws UserNotFoundException { diff --git a/fda-metadata-db/api/src/main/java/at/tuwien/api/container/image/ImageBriefDto.java b/fda-metadata-db/api/src/main/java/at/tuwien/api/container/image/ImageBriefDto.java index 313e5b115ec2a262df4d90643b806cebe5bb18b3..0624a29e775d4f9d6a44d2fab1c832dc21a929e3 100644 --- a/fda-metadata-db/api/src/main/java/at/tuwien/api/container/image/ImageBriefDto.java +++ b/fda-metadata-db/api/src/main/java/at/tuwien/api/container/image/ImageBriefDto.java @@ -22,10 +22,6 @@ public class ImageBriefDto { @Parameter(required = true, example = "mariadb") private String repository; - @ToString.Exclude - @Parameter(required = true, example = "base64:aaaa") - private String logo; - @NotBlank @Parameter(required = true, example = "10.5") private String tag; diff --git a/fda-metadata-db/api/src/main/java/at/tuwien/api/container/image/ImageDto.java b/fda-metadata-db/api/src/main/java/at/tuwien/api/container/image/ImageDto.java index 2fbae1b8a18b704380472361a3032cf4ee90830d..832d2e586cf19696af7f77ee01fe19ffd3bb274b 100644 --- a/fda-metadata-db/api/src/main/java/at/tuwien/api/container/image/ImageDto.java +++ b/fda-metadata-db/api/src/main/java/at/tuwien/api/container/image/ImageDto.java @@ -36,10 +36,6 @@ public class ImageDto { @Parameter(required = true, example = "org.postgresql.Driver") private String driverClass; - @ToString.Exclude - @Parameter(required = true) - private String logo; - @JsonProperty("date_formats") private List<ImageDateDto> dateFormats; diff --git a/fda-metadata-db/api/src/main/java/at/tuwien/api/user/UserBriefDto.java b/fda-metadata-db/api/src/main/java/at/tuwien/api/user/UserBriefDto.java index 5040bc5c26777138b715a8ea1c09f58d808c4986..167d9e50be8f53725c2d6291b7c287478df691f6 100644 --- a/fda-metadata-db/api/src/main/java/at/tuwien/api/user/UserBriefDto.java +++ b/fda-metadata-db/api/src/main/java/at/tuwien/api/user/UserBriefDto.java @@ -42,9 +42,11 @@ public class UserBriefDto { @Parameter(name = "last name") private String lastname; - @ToString.Exclude - @JsonIgnore - private String invenioToken; + @Parameter(name = "affiliation") + private String affiliation; + + @Parameter(name = "orcid") + private String orcid; @NotNull @Parameter(name = "mail address") diff --git a/fda-metadata-db/api/src/main/java/at/tuwien/api/user/UserDto.java b/fda-metadata-db/api/src/main/java/at/tuwien/api/user/UserDto.java index 7a70a14adef3746fce6fc882d7ca086805bf3c34..de6281ff33bb3eb4ecd361d3051b5f4fc316ccd9 100644 --- a/fda-metadata-db/api/src/main/java/at/tuwien/api/user/UserDto.java +++ b/fda-metadata-db/api/src/main/java/at/tuwien/api/user/UserDto.java @@ -43,6 +43,12 @@ public class UserDto { @Parameter(name = "last name") private String lastname; + @Parameter(name = "affiliation") + private String affiliation; + + @Parameter(name = "orcid") + private String orcid; + @Parameter(name = "list of containers") private List<ContainerDto> containers; @@ -52,13 +58,6 @@ public class UserDto { @Parameter(name = "list of identifiers") private List<ContainerDto> identifiers; - @ToString.Exclude - @JsonIgnore - private String invenioToken; - - @JsonProperty("has_invenio_token") - private Boolean hasInvenioToken; - @ToString.Exclude @JsonIgnore @Parameter(name = "password hash") diff --git a/fda-metadata-db/api/src/main/java/at/tuwien/api/user/UserUpdateDto.java b/fda-metadata-db/api/src/main/java/at/tuwien/api/user/UserUpdateDto.java index 1e76d2c7923a63f320235f7c85f9647f61dec9f1..e7fb05063f277a21a62fd8bf0a4d11764c34d244 100644 --- a/fda-metadata-db/api/src/main/java/at/tuwien/api/user/UserUpdateDto.java +++ b/fda-metadata-db/api/src/main/java/at/tuwien/api/user/UserUpdateDto.java @@ -31,4 +31,10 @@ public class UserUpdateDto { @Parameter(name = "last name") private String lastname; + @Parameter(name = "affiliation") + private String affiliation; + + @Parameter(name = "orcid") + private String orcid; + } diff --git a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/User.java b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/User.java index 8d832c77ff3bf13f04aa7948ceb93adb3847d50a..9024d66090faf539a87317ff08ab45193a0ec243 100644 --- a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/User.java +++ b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/User.java @@ -51,9 +51,11 @@ public class User { @Column(name = "main_email", unique = true, nullable = false) private String email; - @ToString.Exclude - @Column(name = "invenio_token") - private String invenioToken; + @Column + private String affiliation; + + @Column + private String orcid; @Column(name = "main_email_verified", nullable = false) private Boolean emailVerified; diff --git a/fda-metadata-db/setup-schema.sql b/fda-metadata-db/setup-schema.sql index bfd746d5c5ff9240f6dd00573f99d2bb7b2cab0c..ed96b7636eaa9ccf37c0f586e076314714ff7939 100644 --- a/fda-metadata-db/setup-schema.sql +++ b/fda-metadata-db/setup-schema.sql @@ -149,12 +149,13 @@ CREATE TABLE IF NOT EXISTS mdb_users First_name VARCHAR(50), Last_name VARCHAR(50), Gender gender, - Preceding_titles VARCHAR(50), - Postpositioned_title VARCHAR(50), + Preceding_titles VARCHAR(255), + Postpositioned_title VARCHAR(255), + orcid VARCHAR(16), + affiliation VARCHAR(255), Main_Email VARCHAR(255) not null, main_email_verified bool not null default false, password VARCHAR(255) not null, - invenio_token VARCHAR(255), created timestamp without time zone NOT NULL DEFAULT NOW(), last_modified timestamp without time zone, PRIMARY KEY (UserID), diff --git a/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/QueryEndpoint.java b/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/QueryEndpoint.java index 1f64b414818f8cc45a1b8bf5d5cb124ded52b550..118d5ae77bca603f830b8102af73e2fcddbe3a34 100644 --- a/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/QueryEndpoint.java +++ b/fda-query-service/rest-service/src/main/java/at/tuwien/endpoint/QueryEndpoint.java @@ -96,7 +96,7 @@ public class QueryEndpoint extends AbstractEndpoint { @NotNull @PathVariable("queryId") Long queryId, @NotNull Principal principal) throws QueryStoreException, QueryNotFoundException, DatabaseNotFoundException, ImageNotSupportedException, - ContainerNotFoundException, TableMalformedException, FileStorageException, NotAllowedException { + ContainerNotFoundException, TableMalformedException, FileStorageException, NotAllowedException, QueryMalformedException { if (!hasQueryPermission(databaseId, queryId, "QUERY_EXPORT", principal)) { log.error("Missing export query permission"); throw new NotAllowedException("Missing export query permission"); diff --git a/fda-query-service/services/src/main/java/at/tuwien/mapper/QueryMapper.java b/fda-query-service/services/src/main/java/at/tuwien/mapper/QueryMapper.java index dd157660706c436660109b399e61ed748fbf65de..04b477670263eb844d2f05767bb798da5b4e464a 100644 --- a/fda-query-service/services/src/main/java/at/tuwien/mapper/QueryMapper.java +++ b/fda-query-service/services/src/main/java/at/tuwien/mapper/QueryMapper.java @@ -14,7 +14,6 @@ import at.tuwien.querystore.Query; import at.tuwien.entities.database.table.Table; import at.tuwien.entities.database.table.columns.TableColumn; import at.tuwien.exception.ImageNotSupportedException; -import net.sf.jsqlparser.statement.select.FromItem; import net.sf.jsqlparser.statement.select.SelectItem; import org.mapstruct.Mapper; import org.mapstruct.Mapping; @@ -29,8 +28,8 @@ import java.text.Normalizer; import java.time.*; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; -import java.time.temporal.ChronoUnit; import java.util.*; +import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -40,6 +39,9 @@ public interface QueryMapper { org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(QueryMapper.class); + DateTimeFormatter mariaDbFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + .withZone(ZoneId.of("UTC")); + @Deprecated @Mappings({ @Mapping(source = "query", target = "statement") @@ -297,27 +299,39 @@ public interface QueryMapper { }); query.append("FROM `") .append(table.getInternalName()) - .append("` INTO OUTFILE '/tmp/") - .append(filename) - .append("' CHARACTER SET utf8"); + .append("`"); if (timestamp != null) { query.append(" FOR SYSTEM_TIME AS OF TIMESTAMP'") - .append(LocalDateTime.ofInstant(timestamp, ZoneId.of("Europe/Vienna"))) + .append(mariaDbFormatter.format(timestamp)) .append("'"); } + query.append(" INTO OUTFILE '/tmp/") + .append(filename) + .append("' CHARACTER SET utf8"); query.append(";"); return query.toString(); } - default String queryToRawExportQuery(Query query, String filename) { + default String queryToRawExportQuery(Query query, String filename) throws QueryMalformedException { if (query.getQuery().contains(";")) { log.trace("Remove ending ; from statement [{}]", query.getQuery()); query.setQuery(query.getQuery().substring(0, query.getQuery().indexOf(";"))); } - final StringBuilder statement = new StringBuilder(query.getQuery()) - .append(" FOR SYSTEM_TIME AS OF TIMESTAMP'") - .append(LocalDateTime.ofInstant(query.getExecution(), ZoneId.of("Europe/Vienna"))) - .append("' INTO OUTFILE '/tmp/") + /* insert the FOR SYSTEM_TIME ... part after the FROM in the query */ + final StringBuilder versionPart = new StringBuilder(" FOR SYSTEM_TIME AS OF TIMESTAMP'") + .append(mariaDbFormatter.format(query.getExecution())) + .append("' "); + final Pattern pattern = Pattern.compile("from `?[a-zA-Z0-9_]+`?", Pattern.CASE_INSENSITIVE) /* https://mariadb.com/kb/en/columnstore-naming-conventions/ */; + final Matcher matcher = pattern.matcher(query.getQuery()); + if (!matcher.find()) { + log.error("Failed to find 'from' clause in query"); + throw new QueryMalformedException("Failed to find from clause"); + } + log.debug("found group from {} to {} in '{}'", matcher.start(), matcher.end(), query.getQuery()); + final StringBuilder statement = new StringBuilder(query.getQuery().substring(0, matcher.end(0))) + .append(versionPart) + .append(query.getQuery().substring(matcher.end(0))) + .append(" INTO OUTFILE '/tmp/") .append(filename) .append("' CHARACTER SET utf8 FIELDS TERMINATED BY ',';"); log.trace("raw export query: [{}]", statement); diff --git a/fda-query-service/services/src/main/java/at/tuwien/service/QueryService.java b/fda-query-service/services/src/main/java/at/tuwien/service/QueryService.java index 7ae593261fd1f4451e53e7e644e825d302ee3958..64cc05e1198b9dbd8a19dd8a2a8b78430c4a9d7e 100644 --- a/fda-query-service/services/src/main/java/at/tuwien/service/QueryService.java +++ b/fda-query-service/services/src/main/java/at/tuwien/service/QueryService.java @@ -127,7 +127,7 @@ public interface QueryService { */ ExportResource findOne(Long containerId, Long databaseId, Long queryId) throws DatabaseNotFoundException, ImageNotSupportedException, TableMalformedException, - ContainerNotFoundException, FileStorageException, QueryStoreException, QueryNotFoundException; + ContainerNotFoundException, FileStorageException, QueryStoreException, QueryNotFoundException, QueryMalformedException; /** * Count the total tuples for a given table id within a container-database id tuple at a given time. diff --git a/fda-query-service/services/src/main/java/at/tuwien/service/impl/QueryServiceImpl.java b/fda-query-service/services/src/main/java/at/tuwien/service/impl/QueryServiceImpl.java index 10ce827b9e4405a1021955a040ce08d915fba6df..4e1c9f0692af9eb4af9e47ec46638c0fa3209516 100644 --- a/fda-query-service/services/src/main/java/at/tuwien/service/impl/QueryServiceImpl.java +++ b/fda-query-service/services/src/main/java/at/tuwien/service/impl/QueryServiceImpl.java @@ -206,7 +206,7 @@ public class QueryServiceImpl extends HibernateConnector implements QueryService @Transactional(readOnly = true) public ExportResource findOne(Long containerId, Long databaseId, Long queryId) throws DatabaseNotFoundException, ImageNotSupportedException, TableMalformedException, - ContainerNotFoundException, FileStorageException, QueryStoreException, QueryNotFoundException { + ContainerNotFoundException, FileStorageException, QueryStoreException, QueryNotFoundException, QueryMalformedException { /* find */ final Database database = databaseService.find(databaseId); final Query query = storeService.findOne(containerId, databaseId, queryId); diff --git a/fda-ui/components/DBToolbar.vue b/fda-ui/components/DBToolbar.vue index 375cb79746c193b7056758f6ac616932333b7f85..7ffba3adc339d338b9e5f0964c0bb44bb3b150b0 100644 --- a/fda-ui/components/DBToolbar.vue +++ b/fda-ui/components/DBToolbar.vue @@ -7,13 +7,13 @@ </v-toolbar-title> <v-spacer /> <v-toolbar-title> - <v-btn class="mr-2" :disabled="!token" :to="`/container/${$route.params.container_id}/database/${databaseId}/table/import`"> + <v-btn class="mr-2 mb-1" :disabled="!token" :to="`/container/${$route.params.container_id}/database/${databaseId}/table/import`"> <v-icon left>mdi-cloud-upload</v-icon> Import CSV </v-btn> - <v-btn color="secondary" class="mr-2 white--text" :disabled="!token" :to="`/container/${$route.params.container_id}/database/${databaseId}/query/create`"> + <v-btn color="secondary" class="mr-2 mb-1 white--text" :disabled="!token" :to="`/container/${$route.params.container_id}/database/${databaseId}/query/create`"> <v-icon left>mdi-wrench</v-icon> Create Subset </v-btn> - <v-btn color="primary" :disabled="!token" :to="`/container/${$route.params.container_id}/database/${databaseId}/table/create`"> + <v-btn color="primary" class="mb-1" :disabled="!token" :to="`/container/${$route.params.container_id}/database/${databaseId}/table/create`"> <v-icon left>mdi-table-large-plus</v-icon> Create Table </v-btn> </v-toolbar-title> diff --git a/fda-ui/components/TableList.vue b/fda-ui/components/TableList.vue index ebb6ca3aacfe03faeabd42182c28344f2db3e553..cbcfb4478a5b01cc39fe3b6ea75028996a078d02 100644 --- a/fda-ui/components/TableList.vue +++ b/fda-ui/components/TableList.vue @@ -11,7 +11,7 @@ <v-expansion-panel-header> {{ item.name }} </v-expansion-panel-header> - <v-expansion-panel-content> + <v-expansion-panel-content class="mb-2"> <v-row dense> <v-col> <v-list dense> diff --git a/fda-ui/components/TableSchema.vue b/fda-ui/components/TableSchema.vue index f4b5347517361bc0a62c6239eec18517c054cce9..78aa0699f115c0e446fc8699e67949d432c005ea 100644 --- a/fda-ui/components/TableSchema.vue +++ b/fda-ui/components/TableSchema.vue @@ -106,12 +106,6 @@ export default { return [] } }, - form: { - type: Boolean, - default () { - return false - } - }, back: { type: Boolean, default () { @@ -146,7 +140,6 @@ export default { } }, mounted () { - this.valid = this.form this.loadDateFormats() }, methods: { diff --git a/fda-ui/components/dialogs/PersistQuery.vue b/fda-ui/components/dialogs/PersistQuery.vue index 1634dcd2001e6db30cf9ef0eb38b0cd69b2218dc..507ccf5a9927f92014bf260e1d7022cc6bdcae0d 100644 --- a/fda-ui/components/dialogs/PersistQuery.vue +++ b/fda-ui/components/dialogs/PersistQuery.vue @@ -75,6 +75,7 @@ <v-col cols="3"> <v-text-field v-model="creator.orcid" + :rules="[v => validateOrcid(v) || $t('Invalid ORCID')]" name="orcid" label="ORCID" /> </v-col> @@ -152,7 +153,7 @@ </template> <script> -import { formatDateUTC } from '@/utils' +import { formatDateUTC, isValidOrcid } from '@/utils' export default { data () { return { @@ -173,6 +174,12 @@ export default { is_public: null, publisher: null }, + user: { + firstname: null, + lastname: null, + affiliation: null, + orcid: null + }, relatedTypes: [ { value: 'DOI' }, { value: 'URL' }, @@ -257,7 +264,7 @@ export default { }, mounted () { this.loadUser() - this.addCreator() + .then(() => this.addCreatorSelf()) this.loadDatabase() }, methods: { @@ -265,6 +272,19 @@ export default { this.$parent.$parent.$parent.persistQueryDialog = false this.$emit('close', { action: 'closed' }) }, + validateOrcid (orcid) { + return isValidOrcid(orcid) + }, + addCreatorSelf () { + if (!this.user.firstname || !this.user.lastname) { + this.addCreator() + } + this.identifier.creators.push({ + name: `${this.user.lastname}, ${this.user.firstname}`, + orcid: this.user.orcid, + affiliation: this.user.affiliation + }) + }, addCreator () { this.identifier.creators.push({ name: null, @@ -326,6 +346,7 @@ export default { res = await this.$axios.put('/api/auth', null, { headers: this.headers }) + this.user = res.data console.debug('user data', res.data) } catch (err) { this.$toast.error('Failed load user data') diff --git a/fda-ui/components/query/Builder.vue b/fda-ui/components/query/Builder.vue index 984d5e606e510b4f8ed9e18c241277f307a7263c..ff784984cfa7600a667c18f5b4dc1bad6c0eb9a8 100644 --- a/fda-ui/components/query/Builder.vue +++ b/fda-ui/components/query/Builder.vue @@ -1,28 +1,27 @@ <template> <div> <v-toolbar flat> - <v-toolbar-title>Create Query</v-toolbar-title> + <v-toolbar-title>Create Subset</v-toolbar-title> <v-spacer /> <v-toolbar-title> - <v-btn v-if="false" :disabled="!canExecute || !token" color="blue-grey white--text" @click="save"> - Save without execution - </v-btn> <v-btn :disabled="!canExecute || !token" color="primary" @click="execute"> <v-icon left>mdi-run</v-icon> Execute </v-btn> </v-toolbar-title> </v-toolbar> - <v-tabs - v-model="tabs" - centered> - <v-tab> - Create Subset - </v-tab> - <v-tab> - Raw SQL - </v-tab> - </v-tabs> + <v-toolbar flat> + <v-tabs + color="primary" + v-model="tabs"> + <v-tab> + Simple + </v-tab> + <v-tab> + Expert + </v-tab> + </v-tabs> + </v-toolbar> <v-card flat> <v-tabs-items v-model="tabs"> <v-tab-item> diff --git a/fda-ui/components/query/Raw.vue b/fda-ui/components/query/Raw.vue index 73160fbf516b7ffcb4042d2457fa6d613633034a..f5a62ae2e3af5b9df8e9af6bee644a60e36e8714 100644 --- a/fda-ui/components/query/Raw.vue +++ b/fda-ui/components/query/Raw.vue @@ -28,7 +28,7 @@ export default { }, data () { return { - content: this.value || 'SELECT `id` FROM "myTable"', + content: this.value || '-- MariaDB 10.5 Query\n', theme: 'xcode' } }, @@ -53,7 +53,7 @@ export default { methods: { editorInit (editor) { editor.setOptions({ - fontSize: '11pt', + fontSize: '12pt', readOnly: this.disabled, behavioursEnabled: !this.disabled }) diff --git a/fda-ui/pages/container/_container_id/database/_database_id/info.vue b/fda-ui/pages/container/_container_id/database/_database_id/info.vue index e9ee0abe79786a818341bf3ab60a05f10d43c5ba..0a3af4e5a74361841fd655645530d068bae5e266 100644 --- a/fda-ui/pages/container/_container_id/database/_database_id/info.vue +++ b/fda-ui/pages/container/_container_id/database/_database_id/info.vue @@ -92,7 +92,7 @@ <script> import DBToolbar from '@/components/DBToolbar' import EditDB from '@/components/dialogs/EditDB' -import { formatTimestampUTCLabel } from '@/utils' +import { formatTimestampUTCLabel, formatUser } from '@/utils' export default { components: { @@ -165,18 +165,7 @@ export default { return this.database.publication === null ? '(none)' : this.database.publication }, creator () { - if (this.database.creator.firstname && this.database.creator.lastname) { - let creator = '' - if (this.database.creator.titles_before) { - creator += (this.database.creator.titles_before + ' ') - } - creator += (this.database.creator.firstname + ' ' + this.database.creator.lastname) - if (this.database.creator.titles_after) { - creator += (this.database.creator.titles_after + ' ') - } - return creator - } - return this.database.creator.username + return formatUser(this.database.creator) } }, mounted () { diff --git a/fda-ui/pages/container/_container_id/database/_database_id/query/_query_id/index.vue b/fda-ui/pages/container/_container_id/database/_database_id/query/_query_id/index.vue index 65adfc6c8df715f0315e629939e06b92f6ccb715..b7ef2bdbad23610c8f746cf20035012860e92add 100644 --- a/fda-ui/pages/container/_container_id/database/_database_id/query/_query_id/index.vue +++ b/fda-ui/pages/container/_container_id/database/_database_id/query/_query_id/index.vue @@ -8,13 +8,19 @@ </v-toolbar-title> <v-spacer /> <v-toolbar-title> - <v-btn v-if="!identifier.id && !loadingIdentifier" color="secondary" class="mr-2" :disabled="!executionUTC || !token" @click.stop="openDialog()"> + <v-btn v-if="!identifier.id && !loadingIdentifier" color="secondary" class="mr-2" :disabled="error || !executionUTC || !token" @click.stop="openDialog()"> <v-icon left>mdi-content-save-outline</v-icon> Save </v-btn> - <v-btn v-if="result_visibility" color="primary" :loading="downloadLoading" @click.stop="download"> + <v-btn v-if="result_visibility" :disabled="error" color="primary" :loading="downloadLoading" @click.stop="download"> <v-icon left>mdi-download</v-icon> Data .csv </v-btn> - <v-btn v-if="identifier.id" color="secondary" class="ml-2" :loading="metadataLoading" @click.stop="metadata"> + <v-btn + v-if="identifier.id" + :disabled="error" + color="secondary" + class="ml-2" + :loading="metadataLoading" + @click.stop="metadata"> <v-icon left>mdi-code-tags</v-icon> Metadata .xml </v-btn> </v-toolbar-title> @@ -52,10 +58,10 @@ <v-skeleton-loader v-if="loadingDatabase" type="text" class="skeleton-small" /> <span v-if="!loadingDatabase">{{ database.publisher }}</span> </v-list-item-content> - <v-list-item-title v-if="database.license.identifier" class="mt-2"> + <v-list-item-title v-if="database.license" class="mt-2"> Database License </v-list-item-title> - <v-list-item-content v-if="database.license.identifier"> + <v-list-item-content v-if="database.license"> <v-skeleton-loader v-if="loadingDatabase" type="text" class="skeleton-xsmall" /> <a v-if="!loadingDatabase" :href="database.license.uri">{{ database.license.identifier }}</a> </v-list-item-content> diff --git a/fda-ui/pages/container/_container_id/database/_database_id/table/create.vue b/fda-ui/pages/container/_container_id/database/_database_id/table/create.vue index e882415dddfb95f528f3c924d659c364a037f2fb..2345caf0abdf821f9c8fbe84de81b75622ef5d02 100644 --- a/fda-ui/pages/container/_container_id/database/_database_id/table/create.vue +++ b/fda-ui/pages/container/_container_id/database/_database_id/table/create.vue @@ -51,7 +51,7 @@ </v-stepper-step> <v-stepper-content step="2"> - <TableSchema :form="valid" :columns="tableCreate.columns" @close="schemaClose" /> + <TableSchema :back="true" :columns="tableCreate.columns" @close="schemaClose" /> </v-stepper-content> </v-stepper> </div> @@ -153,7 +153,11 @@ export default { this.loading = false }, schemaClose (event) { - console.trace('schema closed', event) + console.debug('schema closed', event) + if (!event.success) { + this.step = 1 + return + } this.createTable() } } diff --git a/fda-ui/pages/container/_container_id/database/_database_id/table/import.vue b/fda-ui/pages/container/_container_id/database/_database_id/table/import.vue index bd10a893222e5d4f60f8a9ed1bd21d716e679f37..7c8e38a4b553dc9f94377eb9d6577788052775ad 100644 --- a/fda-ui/pages/container/_container_id/database/_database_id/table/import.vue +++ b/fda-ui/pages/container/_container_id/database/_database_id/table/import.vue @@ -171,7 +171,7 @@ </v-stepper-step> <v-stepper-content step="4"> - <TableSchema :form="validStep4" :back="true" :columns="tableCreate.columns" @close="schemaClose" /> + <TableSchema :back="true" :columns="tableCreate.columns" @close="schemaClose" /> </v-stepper-content> <v-stepper-step diff --git a/fda-ui/pages/container/index.vue b/fda-ui/pages/container/index.vue index 18d492ae9a604152717fdc4b4f9ceade37f3e300..ae455fe553a2a7e71303ef02500019324254207e 100644 --- a/fda-ui/pages/container/index.vue +++ b/fda-ui/pages/container/index.vue @@ -64,7 +64,7 @@ <script> import { mdiDatabaseArrowRightOutline } from '@mdi/js' import CreateDB from '@/components/dialogs/CreateDB' -import { formatTimestampUTCLabel } from '@/utils' +import { formatTimestampUTCLabel, formatUser } from '@/utils' export default { components: { @@ -105,18 +105,7 @@ export default { }, methods: { formatCreator (creator) { - if (creator.firstname && creator.lastname) { - let name = '' - if (creator.titles_before) { - name += creator.titles_before + ' ' - } - name += creator.firstname + ' ' + creator.lastname - if (creator.titles_after) { - name += ' ' + creator.titles_after - } - return name - } - return creator.username + return formatUser(creator) }, async loadContainers () { this.createDbDialog = false diff --git a/fda-ui/pages/user/index.vue b/fda-ui/pages/user/index.vue index 8d090a47c8c8a3fea22826b6b396dd0312abb405..85cf94f44da9f32271aa553d4163e5e6e25e635e 100644 --- a/fda-ui/pages/user/index.vue +++ b/fda-ui/pages/user/index.vue @@ -90,6 +90,24 @@ label="Titles After" /> </v-col> </v-row> + <v-row dense> + <v-col cols="5"> + <v-text-field + v-model="user.affiliation" + hint="e.g. University of xyz" + label="Affiliation" /> + </v-col> + </v-row> + <v-row dense> + <v-col cols="5"> + <v-text-field + v-model="user.orcid" + :rules="[v => validateOrcid(v) || $t('Invalid ORCID')]" + maxlength="19" + hint="e.g. 0000-0002-1825-0097" + label="ORCID" /> + </v-col> + </v-row> <v-row dense> <v-col cols="5"> <v-btn @@ -136,6 +154,7 @@ </div> </template> <script> +import { isValidOrcid } from '@/utils' export default { data () { return { @@ -151,7 +170,9 @@ export default { firstname: null, titles_after: null, titles_before: null, - email_verified: false + email_verified: false, + affiliation: null, + orcid: null }, reset: { password: null @@ -213,6 +234,12 @@ export default { } this.loading = false }, + validateOrcid (orcid) { + if (!orcid) { + return true + } + return isValidOrcid(orcid) + }, async resend () { try { this.loading = true @@ -248,20 +275,19 @@ export default { titles_before: this.user.titles_before, titles_after: this.user.titles_after, firstname: this.user.firstname, - lastname: this.user.lastname + lastname: this.user.lastname, + affiliation: this.user.affiliation, + orcid: this.user.orcid }, this.config) console.debug('update', res.data) this.error = false this.$toast.success('Successfully updated user info') } catch (err) { console.error('update', err) + this.$toast.error('Failed to update user info') this.error = true } this.loading = false - }, - setToken () { - this.user.has_invenio_token = false - this.api.invenio_token = '' } } } diff --git a/fda-ui/utils/index.js b/fda-ui/utils/index.js index d61b3165e553eeb4cfcde6e3c14ed1e902aa8a03..44d82b1ef560064c37cef242a7cc4140ee8ab5aa 100644 --- a/fda-ui/utils/index.js +++ b/fda-ui/utils/index.js @@ -28,6 +28,47 @@ function isNonNegativeInteger (str) { return str >>> 0 === parseFloat(str) } +/** + * https://support.orcid.org/hc/en-us/articles/360006897674-Structure-of-the-ORCID-Identifier + * @param str The ORCID + * @returns {boolean} True if ORCID is valid, false otherwise + */ +function isValidOrcid (str) { + if (str == null) { + return false + } + if (str.length !== 19) { + return false + } + let total = 0 + for (let i = 0; i < str.length; i++) { + const digit = parseInt(str.charAt(i)) + if (isNaN(digit)) { + continue + } + total = (total + digit) * 2 + } + const remainder = total % 11 + const result = (12 - remainder) % 11 + const check = result === 10 ? 'X' : result.toString() + return str.substr(18) === check +} + +function formatUser (user) { + if (user.firstname && user.lastname) { + let name = '' + if (user.titles_before) { + name += user.titles_before + ' ' + } + name += user.firstname + ' ' + user.lastname + if (user.titles_after) { + name += ' ' + user.titles_after + } + return name + } + return user.username +} + function formatDateUTC (str) { if (str === null) { return null @@ -65,5 +106,7 @@ module.exports = { formatTimestampUTC, formatTimestampUTCLabel, formatDateUTC, - isNonNegativeInteger + isNonNegativeInteger, + isValidOrcid, + formatUser }