From 5bdb8d01f97fe3eb6dbb4e843f5072d1f56c3d92 Mon Sep 17 00:00:00 2001
From: Martin Weise <martin.weise@tuwien.ac.at>
Date: Thu, 21 Jul 2022 16:54:18 +0800
Subject: [PATCH] Various style fixes, fixed the .env.example

---
 .env.example                                  |  7 +--
 .../endpoints/AuthenticationEndpoint.java     |  3 +-
 .../at/tuwien/endpoints/UserEndpoint.java     | 37 +++++--------
 .../src/main/resources/application-docker.yml |  2 +-
 .../src/main/resources/application.yml        |  4 +-
 .../exception/OrcidMalformedException.java    | 20 +++++++
 .../java/at/tuwien/mapper/UserMapper.java     | 32 ++++++++++-
 .../java/at/tuwien/service/UserService.java   | 12 +----
 .../tuwien/service/impl/UserServiceImpl.java  | 54 ++++++++++++++-----
 .../api/container/image/ImageBriefDto.java    |  4 --
 .../tuwien/api/container/image/ImageDto.java  |  4 --
 .../java/at/tuwien/api/user/UserBriefDto.java |  8 +--
 .../main/java/at/tuwien/api/user/UserDto.java | 13 +++--
 .../at/tuwien/api/user/UserUpdateDto.java     |  6 +++
 .../java/at/tuwien/entities/user/User.java    |  8 +--
 fda-metadata-db/setup-schema.sql              |  7 +--
 .../at/tuwien/endpoint/QueryEndpoint.java     |  2 +-
 .../java/at/tuwien/mapper/QueryMapper.java    | 36 +++++++++----
 .../java/at/tuwien/service/QueryService.java  |  2 +-
 .../tuwien/service/impl/QueryServiceImpl.java |  2 +-
 fda-ui/components/DBToolbar.vue               |  6 +--
 fda-ui/components/TableList.vue               |  2 +-
 fda-ui/components/TableSchema.vue             |  7 ---
 fda-ui/components/dialogs/PersistQuery.vue    | 25 ++++++++-
 fda-ui/components/query/Builder.vue           | 27 +++++-----
 fda-ui/components/query/Raw.vue               |  4 +-
 .../database/_database_id/info.vue            | 15 +-----
 .../_database_id/query/_query_id/index.vue    | 16 ++++--
 .../database/_database_id/table/create.vue    |  8 ++-
 .../database/_database_id/table/import.vue    |  2 +-
 fda-ui/pages/container/index.vue              | 15 +-----
 fda-ui/pages/user/index.vue                   | 38 ++++++++++---
 fda-ui/utils/index.js                         | 45 +++++++++++++++-
 33 files changed, 308 insertions(+), 165 deletions(-)
 create mode 100644 fda-authentication-service/services/src/main/java/at/tuwien/exception/OrcidMalformedException.java

diff --git a/.env.example b/.env.example
index 0458959466..a5773760e7 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 3d21d5e1d1..48fb5183c6 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 c7665d6fd6..aa3e3a1cf0 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 a9589c3777..78f3bd5c16 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 a9f6e88309..bdb5b07e6d 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 0000000000..29c85b4914
--- /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 3819e30bce..05b1aa764b 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 bd9ecbe2e5..d7877d78a2 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 627a75dceb..1a258e19c8 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 313e5b115e..0624a29e77 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 2fbae1b8a1..832d2e586c 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 5040bc5c26..167d9e50be 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 7a70a14ade..de6281ff33 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 1e76d2c792..e7fb05063f 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 8d832c77ff..9024d66090 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 bfd746d5c5..ed96b7636e 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 1f64b41481..118d5ae77b 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 dd15766070..04b4776702 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 7ae593261f..64cc05e119 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 10ce827b9e..4e1c9f0692 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 375cb79746..7ffba3adc3 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 ebb6ca3aac..cbcfb4478a 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 f4b5347517..78aa0699f1 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 1634dcd200..507ccf5a99 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 984d5e606e..ff784984cf 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 73160fbf51..f5a62ae2e3 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 e9ee0abe79..0a3af4e5a7 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 65adfc6c8d..b7ef2bdbad 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 e882415ddd..2345caf0ab 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 bd10a89322..7c8e38a4b5 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 18d492ae9a..ae455fe553 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 8d090a47c8..85cf94f44d 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 d61b3165e5..44d82b1ef5 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
 }
-- 
GitLab