diff --git a/fda-authentication-service/dbrepo-realm.json b/fda-authentication-service/dbrepo-realm.json index c9ef98fe877d5521855642509e06805fab4d51ff..4fbca0146ae43df33caa79539ab5578be27a9b1a 100644 --- a/fda-authentication-service/dbrepo-realm.json +++ b/fda-authentication-service/dbrepo-realm.json @@ -870,7 +870,7 @@ "otpPolicyLookAheadWindow" : 1, "otpPolicyPeriod" : 30, "otpPolicyCodeReusable" : false, - "otpSupportedApplications" : [ "totpAppGoogleName", "totpAppMicrosoftAuthenticatorName", "totpAppFreeOTPName" ], + "otpSupportedApplications" : [ "totpAppGoogleName", "totpAppFreeOTPName", "totpAppMicrosoftAuthenticatorName" ], "webAuthnPolicyRpEntityName" : "keycloak", "webAuthnPolicySignatureAlgorithms" : [ "ES256" ], "webAuthnPolicyRpId" : "", @@ -1072,6 +1072,20 @@ "claim.name" : "aud", "access.tokenResponse.claim" : "false" } + }, { + "id" : "8ae79e43-b2b7-4bb9-a420-b498690dd8c3", + "name" : "given name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "false", + "user.attribute" : "firstName", + "id.token.claim" : "false", + "access.token.claim" : "true", + "claim.name" : "user.firstname", + "jsonType.label" : "String" + } }, { "id" : "ef081a47-f023-4056-958c-4194d3878d8c", "name" : "username", @@ -1086,9 +1100,23 @@ "claim.name" : "client_id", "jsonType.label" : "String" } + }, { + "id" : "99e3b48b-86ff-4e5b-8652-fcd2738b0ad1", + "name" : "family name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "true", + "user.attribute" : "lastName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "user.lastname", + "jsonType.label" : "String" + } } ], - "defaultClientScopes" : [ "profile", "roles", "attributes" ], - "optionalClientScopes" : [ "rabbitmq.read:*/*", "web-origins", "acr", "rabbitmq.write:*/*", "address", "phone", "offline_access", "microprofile-jwt", "email", "rabbitmq.configure:*/*" ] + "defaultClientScopes" : [ "roles", "attributes" ], + "optionalClientScopes" : [ "rabbitmq.read:*/*", "web-origins", "acr", "rabbitmq.write:*/*", "address", "phone", "offline_access", "profile", "microprofile-jwt", "email", "rabbitmq.configure:*/*" ] }, { "id" : "25741f6b-4867-4138-8238-6345c6ba8702", "clientId" : "rabbitmq-client", @@ -1815,7 +1843,7 @@ "subType" : "anonymous", "subComponents" : { }, "config" : { - "allowed-protocol-mapper-types" : [ "oidc-usermodel-property-mapper", "saml-role-list-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-full-name-mapper", "oidc-usermodel-attribute-mapper", "saml-user-attribute-mapper", "saml-user-property-mapper" ] + "allowed-protocol-mapper-types" : [ "oidc-usermodel-attribute-mapper", "saml-user-attribute-mapper", "saml-user-property-mapper", "oidc-address-mapper", "oidc-full-name-mapper", "saml-role-list-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-property-mapper" ] } }, { "id" : "1849e52a-b8c9-44a8-af3d-ee19376a1ed1", @@ -1841,7 +1869,7 @@ "subType" : "authenticated", "subComponents" : { }, "config" : { - "allowed-protocol-mapper-types" : [ "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-property-mapper", "oidc-usermodel-attribute-mapper", "saml-role-list-mapper", "oidc-full-name-mapper", "saml-user-property-mapper", "saml-user-attribute-mapper" ] + "allowed-protocol-mapper-types" : [ "saml-role-list-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-attribute-mapper", "saml-user-attribute-mapper", "oidc-full-name-mapper", "oidc-address-mapper", "oidc-usermodel-property-mapper", "saml-user-property-mapper" ] } } ], "org.keycloak.userprofile.UserProfileProvider" : [ { @@ -1899,7 +1927,7 @@ "internationalizationEnabled" : false, "supportedLocales" : [ ], "authenticationFlows" : [ { - "id" : "ffedba28-fef3-4e64-9b37-b3270858aa2a", + "id" : "9b2ffbe1-91b5-4815-b2c6-fdb8d5cf522e", "alias" : "Account verification options", "description" : "Method with which to verity the existing account", "providerId" : "basic-flow", @@ -1921,7 +1949,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "34232f15-241f-4827-866b-2b03720a6885", + "id" : "d48e99bc-ce6f-4474-b1f1-2b87c578522d", "alias" : "Authentication Options", "description" : "Authentication options.", "providerId" : "basic-flow", @@ -1950,7 +1978,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "871385ad-06e0-4bf8-b366-ffc256389f1c", + "id" : "61b23580-7996-49c4-8370-77bb1532c818", "alias" : "Browser - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -1972,7 +2000,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "a9fbd9c0-b524-40fb-bc90-23317dc3d611", + "id" : "598a4244-04b4-4a8d-9c99-1e1a41f1243b", "alias" : "Direct Grant - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -1994,7 +2022,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "cf030c87-aeab-453b-a50c-0a4454a5feb9", + "id" : "e7ee21a3-baf9-4259-b4c2-7ca8742d0521", "alias" : "First broker login - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -2016,7 +2044,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "4c156a62-d234-4af7-a609-3c7a0b58d7cf", + "id" : "718cf803-48b1-4a96-83b5-bad0ad92cdbb", "alias" : "Handle Existing Account", "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider", "providerId" : "basic-flow", @@ -2038,7 +2066,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "db5115da-e49d-4572-af14-a7cb12cf4424", + "id" : "2dae43c5-af72-4a1c-b315-798892e76982", "alias" : "Reset - Conditional OTP", "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", "providerId" : "basic-flow", @@ -2060,7 +2088,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "37d5aaaa-2514-4973-b5ae-89ac84bc8600", + "id" : "ca2e30ea-389c-403a-89de-950bbc488ad4", "alias" : "User creation or linking", "description" : "Flow for the existing/non-existing user alternatives", "providerId" : "basic-flow", @@ -2083,7 +2111,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "725b445b-02ee-47ba-9ea2-2c3b8007a025", + "id" : "b11e2f51-ef59-4393-be70-064ed4e1321b", "alias" : "Verify Existing Account by Re-authentication", "description" : "Reauthentication of existing account", "providerId" : "basic-flow", @@ -2105,7 +2133,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "5af07663-b611-464c-aefc-8da55b76d100", + "id" : "9a39c4b1-ba3c-403c-9620-b93e5a9da467", "alias" : "browser", "description" : "browser based authentication", "providerId" : "basic-flow", @@ -2141,7 +2169,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "80ebd402-2fa1-42f3-b6b7-885c85ce1849", + "id" : "6c1d6c8e-e593-40d6-89c8-7b6044790717", "alias" : "clients", "description" : "Base authentication for clients", "providerId" : "client-flow", @@ -2177,7 +2205,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "ab147f84-a2bf-4c35-83a4-fc662c9fb4f2", + "id" : "f650a4f6-e0b8-47da-8416-b805d7cb8535", "alias" : "direct grant", "description" : "OpenID Connect Resource Owner Grant", "providerId" : "basic-flow", @@ -2206,7 +2234,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "87ecaa80-a8b6-47e5-af44-1915880a4ef7", + "id" : "363ceb8b-0902-4f97-9006-93d4e6fa3d9a", "alias" : "docker auth", "description" : "Used by Docker clients to authenticate against the IDP", "providerId" : "basic-flow", @@ -2221,7 +2249,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "d79a5c9e-68a8-430e-97b0-197055caa873", + "id" : "41584462-4e61-45d3-bf42-cf1f19266804", "alias" : "first broker login", "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", "providerId" : "basic-flow", @@ -2244,7 +2272,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "4df39289-f633-466c-9dfd-2ca6ff9445d0", + "id" : "e723d622-3bf1-4202-bf51-69de9548ec20", "alias" : "forms", "description" : "Username, password, otp and other auth forms.", "providerId" : "basic-flow", @@ -2266,7 +2294,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "63948a28-ff5b-4a84-8f61-7d1a1e80773e", + "id" : "e6f26e36-d5cd-47e7-be78-7b9d21187a42", "alias" : "http challenge", "description" : "An authentication flow based on challenge-response HTTP Authentication Schemes", "providerId" : "basic-flow", @@ -2288,7 +2316,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "514330ab-3731-4245-86c0-9155d84fb714", + "id" : "e548e0c5-596a-467f-a23b-00b4ddbf68d3", "alias" : "registration", "description" : "registration flow", "providerId" : "basic-flow", @@ -2304,7 +2332,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "f66aecac-f51a-4c80-8869-338d89054615", + "id" : "0cdcba23-b485-416e-873b-f1695646bef8", "alias" : "registration form", "description" : "registration form", "providerId" : "form-flow", @@ -2340,7 +2368,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "7c404e92-1b71-4a64-8d86-763232fc01ea", + "id" : "ed2d6f0c-4414-49e4-bda1-217cb40e168f", "alias" : "reset credentials", "description" : "Reset credentials for a user if they forgot their password or something", "providerId" : "basic-flow", @@ -2376,7 +2404,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "a69efd34-ba60-4ff2-852a-1704ae3de2a4", + "id" : "a5d09fd7-e988-485b-a5e8-bb9a54e34c42", "alias" : "saml ecp", "description" : "SAML ECP Profile Authentication Flow", "providerId" : "basic-flow", @@ -2392,13 +2420,13 @@ } ] } ], "authenticatorConfig" : [ { - "id" : "65bc1552-4100-4b2e-b765-4393f09a1af1", + "id" : "da63f903-8393-4686-8187-6ca865a79448", "alias" : "create unique user config", "config" : { "require.password.update.after.registration" : "false" } }, { - "id" : "7f26c4ed-6eb2-4cab-8ffa-c4da830ffa26", + "id" : "96a14ace-debb-42f0-8dff-701891d6048a", "alias" : "review profile config", "config" : { "update.profile.on.first.login" : "missing" diff --git a/fda-container-service/rest-service/src/main/java/at/tuwien/endpoints/ContainerEndpoint.java b/fda-container-service/rest-service/src/main/java/at/tuwien/endpoints/ContainerEndpoint.java index cdec045020c3b5cc554e4fc81d6bfae8087e2a01..68c240908fdd3af73b52ddd6c0263b670910791a 100644 --- a/fda-container-service/rest-service/src/main/java/at/tuwien/endpoints/ContainerEndpoint.java +++ b/fda-container-service/rest-service/src/main/java/at/tuwien/endpoints/ContainerEndpoint.java @@ -23,6 +23,7 @@ import javax.validation.Valid; import javax.validation.constraints.NotNull; import java.security.Principal; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; @@ -52,10 +53,23 @@ public class ContainerEndpoint { @RequestParam(required = false) Integer limit) { log.debug("endpoint find all containers, principal={}, limit={}", principal, limit); final List<Container> containers = containerService.getAll(limit); + final List<com.github.dockerjava.api.model.Container> list = containerService.list(); + final List<ContainerBriefDto> dtos = containers.stream() + .map(containerMapper::containerToDatabaseContainerBriefDto) + .peek(container -> { + final Optional<com.github.dockerjava.api.model.Container> optional = list.stream() + .filter(c -> c.getId().equals(container.getHash())) + .findFirst(); + optional.ifPresent(value -> { + final String state = value.getState(); + log.trace("container {} has status {}", container.getId(), state); + container.setRunning(state.equals("running")); + }); + }) + .collect(Collectors.toList()); + log.trace("find all containers resulted in containers {}", dtos); return ResponseEntity.ok() - .body(containers.stream() - .map(containerMapper::containerToDatabaseContainerBriefDto) - .collect(Collectors.toList())); + .body(dtos); } @PostMapping diff --git a/fda-container-service/services/src/main/java/at/tuwien/mapper/ContainerMapper.java b/fda-container-service/services/src/main/java/at/tuwien/mapper/ContainerMapper.java index 57104de0122ab93e82837291854de0ae8c7c1830..335ecdc23d7dc020ac50bb3c7b58668332cf484a 100644 --- a/fda-container-service/services/src/main/java/at/tuwien/mapper/ContainerMapper.java +++ b/fda-container-service/services/src/main/java/at/tuwien/mapper/ContainerMapper.java @@ -6,6 +6,7 @@ import at.tuwien.api.container.ContainerDto; import at.tuwien.api.container.ContainerStateDto; import at.tuwien.entities.container.Container; import at.tuwien.entities.container.image.ContainerImage; +import at.tuwien.entities.user.User; import com.github.dockerjava.api.command.InspectContainerResponse; import org.mapstruct.*; @@ -14,7 +15,7 @@ import java.util.Locale; import java.util.Objects; import java.util.regex.Pattern; -@Mapper(componentModel = "spring", uses = {ImageMapper.class, DatabaseMapper.class}) +@Mapper(componentModel = "spring", uses = {ImageMapper.class, DatabaseMapper.class, UserMapper.class}) public interface ContainerMapper { org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ContainerMapper.class); @@ -22,6 +23,7 @@ public interface ContainerMapper { default String containerCreateRequestDtoToDockerImage(ContainerCreateRequestDto data) { final String image = data.getRepository() + ":" + data.getTag(); log.trace("mapped container request {} to image {}", data, image); + final User user; return image; } diff --git a/fda-container-service/services/src/main/java/at/tuwien/mapper/UserMapper.java b/fda-container-service/services/src/main/java/at/tuwien/mapper/UserMapper.java index cfcab8f9975751ae5a3e50dec28285a9f2bd0ddf..d92e24f645f1e7fe23023e350b9a1928c9ee590e 100644 --- a/fda-container-service/services/src/main/java/at/tuwien/mapper/UserMapper.java +++ b/fda-container-service/services/src/main/java/at/tuwien/mapper/UserMapper.java @@ -4,7 +4,12 @@ import at.tuwien.api.auth.TokenIntrospectDto; import at.tuwien.api.user.GrantedAuthorityDto; import at.tuwien.api.user.UserBriefDto; import at.tuwien.api.user.UserDetailsDto; +import at.tuwien.api.user.UserDto; +import at.tuwien.entities.user.User; +import at.tuwien.entities.user.UserAttribute; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -16,6 +21,18 @@ public interface UserMapper { org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(UserMapper.class); + /* keep */ + @Mappings({ + @Mapping(target = "orcid", expression = "java(data.getAttributes().stream().filter(a -> a.getName().equals(\"orcid\")).findFirst().get().getValue())") + }) + UserBriefDto userToUserBriefDto(User data); + + /* keep */ + @Mappings({ + @Mapping(target = "orcid", expression = "java(data.getAttributes().stream().filter(a -> a.getName().equals(\"orcid\")).findFirst().get().getValue())") + }) + UserDto userToUserDto(User data); + UserDetailsDto userBriefDtoToUserDetailsDto(UserBriefDto data); default UserDetailsDto tokenIntrospectDtoToUserDetailsDto(TokenIntrospectDto data) { diff --git a/fda-container-service/services/src/main/java/at/tuwien/service/ContainerService.java b/fda-container-service/services/src/main/java/at/tuwien/service/ContainerService.java index 26654fc34f51bb974b91036fadeb26eca12a1f55..24400fa3549581fa098fbc101e9b2cfae17fe64f 100644 --- a/fda-container-service/services/src/main/java/at/tuwien/service/ContainerService.java +++ b/fda-container-service/services/src/main/java/at/tuwien/service/ContainerService.java @@ -63,6 +63,8 @@ public interface ContainerService { */ List<Container> getAll(Integer limit); + List<com.github.dockerjava.api.model.Container> list(); + /** * @param containerId * @return diff --git a/fda-container-service/services/src/main/java/at/tuwien/service/impl/ContainerServiceImpl.java b/fda-container-service/services/src/main/java/at/tuwien/service/impl/ContainerServiceImpl.java index 9753d9687e7ec0113b00bde7cede4a4d3e41b1e6..ff1c9c22a92b423de396384aad61d124118157b0 100644 --- a/fda-container-service/services/src/main/java/at/tuwien/service/impl/ContainerServiceImpl.java +++ b/fda-container-service/services/src/main/java/at/tuwien/service/impl/ContainerServiceImpl.java @@ -32,7 +32,6 @@ import org.springframework.transaction.annotation.Transactional; import java.security.Principal; import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; @Log4j2 @Service @@ -227,6 +226,13 @@ public class ContainerServiceImpl implements ContainerService { return containers; } + @Override + public List<com.github.dockerjava.api.model.Container> list() { + return dockerClient.listContainersCmd() + .withShowAll(true) + .exec(); + } + @Override @Transactional public Container start(Long containerId) throws ContainerNotFoundException, diff --git a/fda-metadata-db/api/src/main/java/at/tuwien/api/container/ContainerBriefDto.java b/fda-metadata-db/api/src/main/java/at/tuwien/api/container/ContainerBriefDto.java index b8a1b241b3395afa4ddf876576c038ed77247626..77b76c1dd2a59b4bd62033d187717995a4f692a2 100644 --- a/fda-metadata-db/api/src/main/java/at/tuwien/api/container/ContainerBriefDto.java +++ b/fda-metadata-db/api/src/main/java/at/tuwien/api/container/ContainerBriefDto.java @@ -38,6 +38,10 @@ public class ContainerBriefDto { @Schema(example = "air-quality") private String internalName; + @NotNull + @Schema(example = "true") + private Boolean running; + @org.springframework.data.annotation.Transient private DatabaseBriefDto database; diff --git a/fda-metadata-db/api/src/main/java/at/tuwien/api/container/ContainerDto.java b/fda-metadata-db/api/src/main/java/at/tuwien/api/container/ContainerDto.java index ca2bcded38f067c9927600d7e277a27117120ce8..e4d32aeb5df225b3b74625935640a144dd6a928a 100644 --- a/fda-metadata-db/api/src/main/java/at/tuwien/api/container/ContainerDto.java +++ b/fda-metadata-db/api/src/main/java/at/tuwien/api/container/ContainerDto.java @@ -46,6 +46,10 @@ public class ContainerDto { @JsonProperty("ip_address") private String ipAddress; + @NotNull + @Schema(example = "true") + private Boolean running; + private ImageBriefDto image; private Integer port; 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 6d198b70979e860dcec789a96706a479650bb5b4..d11d345a0d66457a56beaacf025b02a15c57a10d 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 @@ -29,6 +29,9 @@ public class UserBriefDto { @Schema(example = "Josiah Carberry") private String name; + @Schema(example = "0000-0002-1825-0097") + private String orcid; + @JsonProperty("given_name") @Schema(example = "Josiah") private String firstname; 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 3796e6c80f13f5d0b3cf6fcb9f15aa88178e61ee..d3b081ade0f4f4a67fdf7c60726ec22d8ef4b20d 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 @@ -28,6 +28,9 @@ public class UserDto { @Schema(example = "Josiah Carberry") private String name; + @Schema(example = "0000-0002-1825-0097") + private String orcid; + @JsonProperty("given_name") @Schema(example = "Josiah") private String firstname; diff --git a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/container/Container.java b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/container/Container.java index 1286cf7ea7ba36c909924f0c831d1ac96272f2b1..797ffda9926f259af70e630f68b4baac3f4c29a3 100644 --- a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/container/Container.java +++ b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/container/Container.java @@ -3,20 +3,14 @@ package at.tuwien.entities.container; import at.tuwien.entities.container.image.ContainerImage; import at.tuwien.entities.database.Database; import at.tuwien.entities.user.User; -import com.fasterxml.jackson.annotation.JsonFormat; import lombok.*; import org.hibernate.annotations.GenericGenerator; -import org.hibernate.annotations.SQLDelete; -import org.hibernate.annotations.Where; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import javax.persistence.*; import java.time.Instant; -import java.util.List; - -import com.fasterxml.jackson.annotation.JsonIgnore; @Data @Entity diff --git a/fda-ui/api/authentication.service.js b/fda-ui/api/authentication.service.js index 43bbf8274277da3ef53d03edddb200dd8edef9f4..33dcadd1f25e9dfe138247b46eee372f25767296 100644 --- a/fda-ui/api/authentication.service.js +++ b/fda-ui/api/authentication.service.js @@ -65,11 +65,9 @@ class AuthenticationService { const authentication = response.data // eslint-disable-next-line camelcase const { access_token, refresh_token } = authentication - console.debug('response authenticate', authentication) store().commit('SET_TOKEN', access_token) store().commit('SET_REFRESH_TOKEN', refresh_token) - const user = UserMapper.tokenToUser(access_token) - store().commit('SET_USER', user) + store().commit('SET_ROLES', UserMapper.tokenToRoles(access_token)) resolve(authentication) }).catch((error) => { console.error('Failed to authenticate', error) diff --git a/fda-ui/api/container.service.js b/fda-ui/api/container.service.js index 34e069a804f4499a57f9cda1b40752530b9d12e0..48adb5427bad97840ba73756d9683d50f40c730c 100644 --- a/fda-ui/api/container.service.js +++ b/fda-ui/api/container.service.js @@ -26,7 +26,8 @@ class ContainerService { const container = response.data console.debug('response container', container) resolve(container) - }).catch((error) => { + }) + .catch((error) => { const { code, message } = error console.error('Failed to load container', error) Vue.$toast.error(`[${code}] Failed to load container: ${message}`) @@ -42,7 +43,8 @@ class ContainerService { const container = response.data console.debug('response container', container) resolve(container) - }).catch((error) => { + }) + .catch((error) => { const { code, message } = error console.error('Failed to create container', error) Vue.$toast.error(`[${code}] Failed to create container: ${message}`) @@ -58,11 +60,18 @@ class ContainerService { const container = response.data console.debug('response container', container) resolve(container) - }).catch((error) => { - const { code, message } = error - console.error('Failed to modify container', error) - Vue.$toast.error(`[${code}] Failed to modify container: ${message}`) - reject(error) + }) + .catch((error) => { + const { code, message, response } = error + const { status } = response + if (status === 409) { + console.warn('Failed to modify container', error) + reject(error) + } else { + console.error('Failed to modify container', error) + Vue.$toast.error(`[${code}] Failed to modify container: ${message}`) + reject(error) + } }) }) } diff --git a/fda-ui/api/database.service.js b/fda-ui/api/database.service.js new file mode 100644 index 0000000000000000000000000000000000000000..1d0273c505a988ead4dd6338065ea92f8129de9a --- /dev/null +++ b/fda-ui/api/database.service.js @@ -0,0 +1,147 @@ +import Vue from 'vue' +import api from '@/api' + +class DatabaseService { + findAll (id) { + return new Promise((resolve, reject) => { + api.get(`/api/container/${id}/database`, { headers: { Accept: 'application/json' } }) + .then((response) => { + const databases = response.data + console.debug('response databases', databases) + resolve(databases) + }) + .catch((error) => { + const { code, message } = error + console.error('Failed to load databases', error) + Vue.$toast.error(`[${code}] Failed to load databases: ${message}`) + reject(error) + }) + }) + } + + findOne (id, databaseId) { + return new Promise((resolve, reject) => { + api.get(`/api/container/${id}/database/${databaseId}`, { headers: { Accept: 'application/json' } }) + .then((response) => { + const database = response.data + console.debug('response database', database) + resolve(database) + }).catch((error) => { + const { code, message } = error + console.error('Failed to load database', error) + Vue.$toast.error(`[${code}] Failed to load database: ${message}`) + reject(error) + }) + }) + } + + create (id, data) { + return new Promise((resolve, reject) => { + api.post(`/api/container/${id}/database`, data, { headers: { Accept: 'application/json' } }) + .then((response) => { + const database = response.data + console.debug('response database', database) + resolve(database) + }).catch((error) => { + const { code, message } = error + console.error('Failed to create database', error) + Vue.$toast.error(`[${code}] Failed to create database: ${message}`) + reject(error) + }) + }) + } + + modifyVisibility (id, databaseId, isPublic) { + return new Promise((resolve, reject) => { + api.put(`/api/container/${id}/database/${databaseId}/visibility`, { is_public: isPublic }, { headers: { Accept: 'application/json' } }) + .then((response) => { + const database = response.data + console.debug('response database', database) + resolve(database) + }).catch((error) => { + const { code, message } = error + console.error('Failed to modify database visibility', error) + Vue.$toast.error(`[${code}] Failed to modify database visibility: ${message}`) + reject(error) + }) + }) + } + + modifyOwner (id, databaseId, username) { + return new Promise((resolve, reject) => { + api.put(`/api/container/${id}/database/${databaseId}/transfer`, { username }, { headers: { Accept: 'application/json' } }) + .then((response) => { + const database = response.data + console.debug('response database', database) + resolve(database) + }).catch((error) => { + const { code, message } = error + console.error('Failed to modify database owner', error) + Vue.$toast.error(`[${code}] Failed to modify database owner: ${message}`) + reject(error) + }) + }) + } + + checkAccess (id, databaseId) { + return new Promise((resolve, reject) => { + api.get(`/api/container/${id}/database/${databaseId}/access`, { headers: { Accept: 'application/json' } }) + .then((response) => { + const databases = response.data + console.debug('response databases', databases) + resolve(databases) + }) + .catch((error) => { + const { code, message } = error + console.error('Failed to check database access', error) + Vue.$toast.error(`[${code}] Failed to check database access: ${message}`) + reject(error) + }) + }) + } + + modifyAccess (id, databaseId, username, type) { + return new Promise((resolve, reject) => { + api.put(`/api/container/${id}/database/${databaseId}/access/${username}`, { type }, { headers: { Accept: 'application/json' } }) + .then((response) => { + const database = response.data + console.debug('response database', database) + resolve(database) + }) + .catch((error) => { + const { code, message } = error + console.error('Failed to modify database access', error) + Vue.$toast.error(`[${code}] Failed to modify database access: ${message}`) + reject(error) + }) + }) + } + + revokeAccess (id, databaseId, username) { + return new Promise((resolve, reject) => { + api.delete(`/api/container/${id}/database/${databaseId}/access/${username}`, { headers: { Accept: 'application/json' } }) + .then(() => resolve()) + .catch((error) => { + const { code, message } = error + console.error('Failed to revoke database access', error) + Vue.$toast.error(`[${code}] Failed to revoke database access: ${message}`) + reject(error) + }) + }) + } + + giveAccess (id, databaseId, username, type) { + return new Promise((resolve, reject) => { + api.post(`/api/container/${id}/database/${databaseId}/access/${username}`, { username, type }, { headers: { Accept: 'application/json' } }) + .then(() => resolve()) + .catch((error) => { + const { code, message } = error + console.error('Failed to give database access', error) + Vue.$toast.error(`[${code}] Failed to give database access: ${message}`) + reject(error) + }) + }) + } +} + +export default new DatabaseService() diff --git a/fda-ui/api/database/index.js b/fda-ui/api/database/index.js deleted file mode 100644 index dee8179bf5dbfefba9426f060ebef5664caa4e49..0000000000000000000000000000000000000000 --- a/fda-ui/api/database/index.js +++ /dev/null @@ -1,43 +0,0 @@ -const axios = require('axios/dist/browser/axios.cjs') - -export function createDatabase (token, container) { - const payload = { - name: container.name, - is_public: container.is_public ? container.is_public : true - } - return axios.post(`/api/container/${container.id}/database`, payload, { - headers: { - Authorization: `Bearer ${token}` - } - }) -} - -export function modifyVisibility (token, containerId, databaseId, isPublic) { - const payload = { - is_public: isPublic - } - return axios.put(`/api/container/${containerId}/database/${databaseId}/visibility`, payload, { - headers: { - Authorization: `Bearer ${token}` - } - }) -} - -export function modifyOwnership (token, containerId, databaseId, username) { - const payload = { - username - } - return axios.put(`/api/container/${containerId}/database/${databaseId}/transfer`, payload, { - headers: { - Authorization: `Bearer ${token}` - } - }) -} - -export function findDatabase (token, containerId, databaseId) { - return axios.get(`/api/container/${containerId}/database/${databaseId}`, { - headers: { - Authorization: `Bearer ${token}` - } - }) -} diff --git a/fda-ui/api/identifier.service.js b/fda-ui/api/identifier.service.js new file mode 100644 index 0000000000000000000000000000000000000000..549cd0eabd4e9282ead1788e44fbdf2d9d780268 --- /dev/null +++ b/fda-ui/api/identifier.service.js @@ -0,0 +1,23 @@ +import Vue from 'vue' +import api from '@/api' + +class IdentifierService { + findPid (id) { + return new Promise((resolve, reject) => { + api.get(`/api/pid/${id}`, { headers: { Accept: 'application/json' } }) + .then((response) => { + const identifier = response.data + console.debug('response identifier', identifier) + resolve(identifier) + }) + .catch((error) => { + const { code, message } = error + console.error('Failed to load identifier', error) + Vue.$toast.error(`[${code}] Failed to load identifier: ${message}`) + reject(error) + }) + }) + } +} + +export default new IdentifierService() diff --git a/fda-ui/api/query.service.js b/fda-ui/api/query.service.js new file mode 100644 index 0000000000000000000000000000000000000000..8fc66756bacb5c95f7be93600f68c5438ea86d18 --- /dev/null +++ b/fda-ui/api/query.service.js @@ -0,0 +1,73 @@ +import Vue from 'vue' +import api from '@/api' + +class QueryService { + findAll (id, databaseId) { + return new Promise((resolve, reject) => { + api.get(`/api/container/${id}/database/${databaseId}/query`, { headers: { Accept: 'application/json' } }) + .then((response) => { + const queries = response.data + console.debug('response queries', queries) + resolve(queries) + }) + .catch((error) => { + const { code, message } = error + console.error('Failed to load queries', error) + Vue.$toast.error(`[${code}] Failed to load queries: ${message}`) + reject(error) + }) + }) + } + + findOne (id, databaseId, queryId) { + return new Promise((resolve, reject) => { + api.get(`/api/container/${id}/database/${databaseId}/query/${queryId}`, { headers: { Accept: 'application/json' } }) + .then((response) => { + const query = response.data + console.debug('response query', query) + resolve(query) + }).catch((error) => { + const { code, message } = error + console.error('Failed to load query', error) + Vue.$toast.error(`[${code}] Failed to load query: ${message}`) + reject(error) + }) + }) + } + + persist (id, databaseId, queryId) { + return new Promise((resolve, reject) => { + api.put(`/api/container/${id}/database/${databaseId}/query/${queryId}`, {}, { headers: { Accept: 'application/json' } }) + .then((response) => { + const query = response.data + console.debug('response query', query) + resolve(query) + }) + .catch((error) => { + const { code, message } = error + console.error('Failed to persist query', error) + Vue.$toast.error(`[${code}] Failed to persist query: ${message}`) + reject(error) + }) + }) + } + + export (id, databaseId, queryId) { + return new Promise((resolve, reject) => { + api.put(`/api/container/${id}/database/${databaseId}/query/${queryId}/export`, {}, { headers: { Accept: 'text/csv' } }) + .then((response) => { + const query = response.data + console.debug('response export', query) + resolve(query) + }) + .catch((error) => { + const { code, message } = error + console.error('Failed to export query', error) + Vue.$toast.error(`[${code}] Failed to export query: ${message}`) + reject(error) + }) + }) + } +} + +export default new QueryService() diff --git a/fda-ui/api/query/index.js b/fda-ui/api/query/index.js deleted file mode 100644 index e76cc2da4fb545a56bcf506e94018f2469a05315..0000000000000000000000000000000000000000 --- a/fda-ui/api/query/index.js +++ /dev/null @@ -1,17 +0,0 @@ -const axios = require('axios/dist/browser/axios.cjs') - -export function findQuery (token, containerId, databaseId, queryId) { - return axios.get(`/api/container/${containerId}/database/${databaseId}/query/${queryId}`, { - headers: { - Authorization: `Bearer ${token}` - } - }) -} - -export function persistQuery (token, containerId, databaseId, queryId) { - return axios.put(`/api/container/${containerId}/database/${databaseId}/query/${queryId}`, {}, { - headers: { - Authorization: `Bearer ${token}` - } - }) -} diff --git a/fda-ui/api/table.service.js b/fda-ui/api/table.service.js new file mode 100644 index 0000000000000000000000000000000000000000..36a932200a39debe8fe7ab9d135089a2d8348f9d --- /dev/null +++ b/fda-ui/api/table.service.js @@ -0,0 +1,79 @@ +import Vue from 'vue' +import api from '@/api' + +/** + * Service class for interaction with Table Service in the back end. + * + * @author Martin Weise + */ +class TableService { + findAll (id, databaseId) { + return new Promise((resolve, reject) => { + api.get(`/api/container/${id}/database/${databaseId}/table`, { headers: { Accept: 'application/json' } }) + .then((response) => { + const tables = response.data + console.debug('response tables', tables) + resolve(tables) + }) + .catch((error) => { + const { code, message } = error + console.error('Failed to load tables', error) + Vue.$toast.error(`[${code}] Failed to load tables: ${message}`) + reject(error) + }) + }) + } + + findOne (id, databaseId, tableId) { + return new Promise((resolve, reject) => { + api.get(`/api/container/${id}/database/${databaseId}/table/${tableId}`, { headers: { Accept: 'application/json' } }) + .then((response) => { + const table = response.data + console.debug('response table', table) + resolve(table) + }) + .catch((error) => { + const { code, message } = error + console.error('Failed to load table', error) + Vue.$toast.error(`[${code}] Failed to load table: ${message}`) + reject(error) + }) + }) + } + + create (id, databaseId, data) { + return new Promise((resolve, reject) => { + api.post(`/api/container/${id}/database/${databaseId}/table`, data, { headers: { Accept: 'application/json' } }) + .then((response) => { + const table = response.data + console.debug('response table', table) + resolve(table) + }) + .catch((error) => { + const { code, message } = error + console.error('Failed to create table', error) + Vue.$toast.error(`[${code}] Failed to create table: ${message}`) + reject(error) + }) + }) + } + + importCsv (id, databaseId, tableId, data) { + return new Promise((resolve, reject) => { + api.post(`/api/container/${id}/database/${databaseId}/table/${tableId}/import`, data, { headers: { Accept: 'application/json' } }) + .then((response) => { + const table = response.data + console.debug('response table', table) + resolve(table) + }) + .catch((error) => { + const { code, message } = error + console.error('Failed to import csv to table', error) + Vue.$toast.error(`[${code}] Failed to import csv to table: ${message}`) + reject(error) + }) + }) + } +} + +export default new TableService() diff --git a/fda-ui/api/table/index.js b/fda-ui/api/table/index.js deleted file mode 100644 index 097d003972837b4d640125704831315256360bfb..0000000000000000000000000000000000000000 --- a/fda-ui/api/table/index.js +++ /dev/null @@ -1,25 +0,0 @@ -const axios = require('axios/dist/browser/axios.cjs') - -export function listTables (token, containerId, databaseId) { - return axios.get(`/api/container/${containerId}/database/${databaseId}/table`, { - headers: { - Authorization: `Bearer ${token}` - } - }) -} - -export function createTable (token, containerId, databaseId, payload) { - return axios.post(`/api/container/${containerId}/database/${databaseId}/table`, payload, { - headers: { - Authorization: `Bearer ${token}` - } - }) -} - -export function dataImport (token, containerId, databaseId, tableId, payload) { - return axios.post(`/api/container/${containerId}/database/${databaseId}/table/${tableId}/data/import`, payload, { - headers: { - Authorization: `Bearer ${token}` - } - }) -} diff --git a/fda-ui/api/user.mapper.js b/fda-ui/api/user.mapper.js index dec8f31910d956c07a2946ae3416b56d8be3098d..31a690e509785299c785fc22ff116c0ab513b448 100644 --- a/fda-ui/api/user.mapper.js +++ b/fda-ui/api/user.mapper.js @@ -1,31 +1,24 @@ import jwtDecode from 'jwt-decode' class UserMapper { - tokenToUser (token) { + tokenToRoles (token) { const data = jwtDecode(token) - return { - id: data.sub, - firstname: data.given_name || null, - lastname: data.family_name || null, - username: data.client_id, - roles: data.realm_access.roles || [], - attributes: data.attributes || [] - } + return data.realm_access.roles || [] } - tokenToRoles (token) { + tokenToUserId (token) { const data = jwtDecode(token) - if (!data) { - return [] - } - return data.realm_access.roles || [] + return data.sub } - getThemeDark (user) { - if (!user || !user.attributes || user.attributes.filter(a => a.name === 'theme_dark').length === 0) { - return false + userInfoToUser (data) { + const obj = Object.assign({}, data) + obj.attributes = { + theme_dark: data.attributes.filter(a => a.name === 'theme_dark')[0].value === 'true', + orcid: data.attributes.filter(a => a.name === 'orcid')[0].value, + affiliation: data.attributes.filter(a => a.name === 'affiliation')[0].value } - return user.attributes.filter(a => a.name === 'theme_dark')[0].value === 'true' + return obj } } diff --git a/fda-ui/api/user.service.js b/fda-ui/api/user.service.js index c86f1b938972077337916c072f793fb3eb724542..e324cf68f3b351bca19cade29fe1aedd388d5f49 100644 --- a/fda-ui/api/user.service.js +++ b/fda-ui/api/user.service.js @@ -1,5 +1,6 @@ import Vue from 'vue' import api from '@/api' +import UserMapper from '@/api/user.mapper' class UserService { findAll () { @@ -23,8 +24,8 @@ class UserService { return new Promise((resolve, reject) => { api.get(`/api/user/${id}`, { headers: { Accept: 'application/json' } }) .then((response) => { - const user = response.data - console.debug('response user', user) + const user = UserMapper.userInfoToUser(response.data) + console.debug('response user', response.data, 'mapped user', user) resolve(user) }).catch((error) => { const { code, message } = error @@ -35,6 +36,22 @@ class UserService { }) } + updateInformation (id, data) { + return new Promise((resolve, reject) => { + api.put(`/api/user/${id}`, data, { headers: { Accept: 'application/json' } }) + .then((response) => { + const user = UserMapper.userInfoToUser(response.data) + console.debug('response user', response.data, 'mapped user', user) + resolve(user) + }).catch((error) => { + const { code, message } = error + console.error('Failed to update user information', error) + Vue.$toast.error(`[${code}] Failed to update user information: ${message}`) + reject(error) + }) + }) + } + create (data) { return new Promise((resolve, reject) => { api.post('/api/user', data, { headers: { Accept: 'application/json' } }) @@ -63,7 +80,7 @@ class UserService { updatePassword (id, password) { return new Promise((resolve, reject) => { - api.post(`/api/user/${id}/password`, { password }, { headers: { Accept: 'application/json' } }) + api.put(`/api/user/${id}/password`, { password }, { headers: { Accept: 'application/json' } }) .then(() => resolve()) .catch((error) => { const { code, message } = error @@ -76,7 +93,7 @@ class UserService { updateTheme (id, themeDark) { return new Promise((resolve, reject) => { - api.post(`/api/user/${id}/theme`, { theme_dark: themeDark }, { headers: { Accept: 'application/json' } }) + api.put(`/api/user/${id}/theme`, { theme_dark: themeDark }, { headers: { Accept: 'application/json' } }) .then(() => resolve()) .catch((error) => { const { code, message } = error diff --git a/fda-ui/api/user/index.js b/fda-ui/api/user/index.js deleted file mode 100644 index b313891ff34081f2c91e94d52e26ff1112c1325d..0000000000000000000000000000000000000000 --- a/fda-ui/api/user/index.js +++ /dev/null @@ -1,89 +0,0 @@ -// eslint-disable-next-line camelcase -import jwt_decode from 'jwt-decode' -import api from '../index' -const qs = require('qs') - -export function updateUser (token, userId, data) { - return api.put(`/api/user/${userId}`, data, { - headers: { - Authorization: `Bearer ${token}` - } - }) -} - -export function updateUserPassword (token, userId, password) { - const payload = { - password - } - return api.put(`/api/user/${userId}/password`, payload, { - headers: { - Authorization: `Bearer ${token}` - } - }) -} - -export function toggleUserTheme (token, userId, themeDark) { - const payload = { - theme_dark: themeDark - } - return api.put(`/api/user/${userId}/theme`, payload, { - headers: { - Authorization: `Bearer ${token}` - } - }) -} - -export function findUser (token) { - const user = tokenToUser(token) - return api.get(`/api/user/${user.id}`, { - headers: { - Authorization: `Bearer ${token}` - } - }) -} - -export function refresh (clientSecret, token) { - const payload = { - client_id: 'dbrepo-client', - grant_type: 'refresh_token', - client_secret: clientSecret, - refresh_token: token - } - return api.post('/api/auth/realms/dbrepo/protocol/openid-connect/token', qs.stringify(payload), { - headers: { ContentType: 'application/form-data' } - }) -} - -export function tokenToUser (token) { - const data = jwt_decode(token) - return { - id: data.sub, - firstname: data.given_name || null, - lastname: data.family_name || null, - username: data.client_id, - roles: data.realm_access.roles || [] - } -} - -export function tokenToExp (token) { - const data = jwt_decode(token) - if (!data) { - return new Date() - } - return new Date(data.exp * 1000) -} - -export function tokenToRoles (token) { - const data = jwt_decode(token) - if (!data) { - return [] - } - return data.realm_access.roles || [] -} - -export function getThemeDark (user) { - if (!user || !user.attributes || user.attributes.filter(a => a.name === 'theme_dark').length === 0) { - return false - } - return user.attributes.filter(a => a.name === 'theme_dark')[0].value === 'true' -} diff --git a/fda-ui/components/DatabaseList.vue b/fda-ui/components/DatabaseList.vue index 01d50e8cef1cf3d407cffffee21fc2ee79d2d424..7dab9d6bddd8e3b203ed650cd8840320b7666d90 100644 --- a/fda-ui/components/DatabaseList.vue +++ b/fda-ui/components/DatabaseList.vue @@ -27,14 +27,23 @@ </div> <div v-text="identifierDescription(container)" /> </v-card-text> - <v-card-text v-if="canInit(container)" class="db-buttons"> + <v-card-text v-if="needsStart(container) || needsDatabase(container)" class="db-buttons"> <v-btn + v-if="needsStart(container)" small secondary :loading="container?.loading" - @click.stop="initDatabase(container)"> + @click.stop="startContainer(container).then(() => createDatabase(container))"> Start </v-btn> + <v-btn + v-else-if="needsDatabase(container)" + small + secondary + :loading="container?.loading" + @click.stop="createDatabase(container)"> + Create Database + </v-btn> </v-card-text> </v-card> <v-toolbar v-if="false" flat> @@ -51,7 +60,7 @@ <script> import { formatCreators, formatUser, formatYearUTC, isResearcher } from '@/utils' -import { createDatabase } from '@/api/database' +import DatabaseService from '@/api/database.service' import ContainerService from '@/api/container.service' export default { @@ -107,7 +116,16 @@ export default { const creators = formatCreators(container) return creators || this.formatUser(container.creator) }, - canInit (container) { + needsStart (container) { + if (!this.user) { + return false + } + if (container.creator.username !== this.user.username) { + return false + } + return container.running === false + }, + needsDatabase (container) { if (!this.user) { return false } @@ -122,10 +140,6 @@ export default { hasIdentifier (container) { return container.database && container.database.identifier }, - async initDatabase (container) { - await this.startContainer(container) - .then(() => this.createDatabase(container)) - }, identifierCreated (container) { if (!container || !container.database || !container.database.identifier) { return null @@ -148,26 +162,23 @@ export default { }) this.loadingContainers = false }, - async startContainer (container) { + startContainer (container) { container.loading = true - await ContainerService.modify(container.id, 'start') - container.loading = false + return new Promise((resolve, reject) => { + ContainerService.modify(container.id, 'start') + .then(() => resolve()) + .finally(() => { + container.loading = false + }) + }) }, - async createDatabase (container) { - try { - container.loading = true - const res = await createDatabase(this.token, container) - container.database = res.data - console.debug('created database', container.database) - this.error = false - } catch (error) { - console.error('create database', error) - const { message } = error.response - this.error = true - console.error('Failed to create database', error) - this.$toast.error(`${message}`) - } - container.loading = false + createDatabase (container) { + container.loading = true + DatabaseService.create(container.id, { name: container.name, is_public: true }) + .then((database) => { + container.loading = false + this.$router.push(`/container/${container.id}/database/${database.id}`) + }) }, link (container) { if (!container.database || !container.database.id) { diff --git a/fda-ui/components/dialogs/CreateDB.vue b/fda-ui/components/dialogs/CreateDB.vue index 44adbbdb217cd94bf2784108a7d053f678614954..b2877b02809394dfec9831d4d500fa76f442a3f1 100644 --- a/fda-ui/components/dialogs/CreateDB.vue +++ b/fda-ui/components/dialogs/CreateDB.vue @@ -56,7 +56,7 @@ <script> import { notEmpty } from '@/utils' import ContainerService from '@/api/container.service' -import { createDatabase } from '@/api/database' +import DatabaseService from '@/api/database.service' export default { data () { @@ -139,35 +139,41 @@ export default { .then(() => this.startContainer(this.container) .then(() => this.createDatabase(this.container))) }, - async createContainer () { + createContainer () { this.createContainerDto.repository = this.engine.repository this.createContainerDto.tag = this.engine.tag this.loading = true - this.container = await ContainerService.create(this.createContainerDto) - this.error = false - this.loading = false + return new Promise((resolve, reject) => { + ContainerService.create(this.createContainerDto) + .then((container) => { + this.container = container + this.loading = false + resolve(container) + }) + .catch(error => reject(error)) + }) }, - async startContainer (container) { + startContainer (container) { this.loading = true - await ContainerService.modify(container.id, 'start') - this.loading = false + return new Promise((resolve, reject) => { + ContainerService.modify(container.id, 'start') + .then(() => { + this.loading = false + resolve() + }) + .catch(error => reject(error)) + }) }, - async createDatabase (container) { - try { - this.loading = true - this.createDatabaseDto.id = container.id - this.createDatabaseDto.name = container.name - const res = await createDatabase(this.token, this.createDatabaseDto) - container.database = res.data - console.debug('created database', container.database) - this.error = false - this.$emit('close', { success: true }) - } catch (error) { - console.error('create database', error) - this.error = true - this.$toast.error('Failed to create database') - } - this.loading = false + createDatabase (container) { + this.loading = true + DatabaseService.create(container.id, { name: container.name, is_public: true }) + .then((database) => { + container.database = database + this.$emit('close', { success: true }) + }) + .finally(() => { + this.loading = false + }) }, notEmpty } diff --git a/fda-ui/components/dialogs/EditAccess.vue b/fda-ui/components/dialogs/EditAccess.vue index bae918a20f67726c7e91896837d3137f38b8dc4d..5d063ca56bd86f2687072453368dd5499a91077f 100644 --- a/fda-ui/components/dialogs/EditAccess.vue +++ b/fda-ui/components/dialogs/EditAccess.vue @@ -21,7 +21,7 @@ <v-col> <v-autocomplete v-model="modify.username" - :items="eligableUsers" + :items="eligibleUsers" :loading="loadingUsers" :rules="[v => !!v || $t('Required')]" required @@ -66,6 +66,8 @@ </v-card-actions> </v-card> </v-form> + <pre>{{ eligibleUsers }}</pre> + <pre>{{ modify.username }}</pre> </div> </template> @@ -77,6 +79,12 @@ export default { default () { return null } + }, + accessType: { + type: String, + default () { + return null + } } }, data () { @@ -126,7 +134,11 @@ export default { } return this.types }, - eligableUsers () { + eligibleUsers () { + if (this.accessType) { + /* this is a modification, list only the edited user as eligible */ + return [{ username: this.username, id: '00000' }] + } return this.users.filter(u => !this.database.accesses.map(a => a.user.id).includes(u.id)) }, buttonColor () { @@ -155,16 +167,15 @@ export default { } }, watch: { - username (val) { - if (!val || this.users.length === 0) { - this.modify.username = null - } - this.selectUser() + username () { + this.init() + }, + accessType () { + this.init() } }, mounted () { - this.loadUsers() - .then(() => this.selectUser()) + this.init() }, methods: { submit () { @@ -248,11 +259,20 @@ export default { } this.loadingUsers = false }, - selectUser () { - const optional = this.users.filter(u => u.username === this.username) - if (optional.length > 0) { - this.modify.username = optional[0] + init () { + if (!this.username) { + this.modify.username = null + this.loadUsers() + } else { + this.modify.username = this.username + /* eligible users are computed separately */ + } + if (!this.accessType) { + this.modify.type = null + } else { + this.modify.type = this.accessType } + this.$refs.form.reset() } } } diff --git a/fda-ui/components/icons/OrcidIcon.vue b/fda-ui/components/icons/OrcidIcon.vue index 39714ddbf8917245c24d531073038d81cfee4f02..ed3b7d72d0ff83b07b06f151ea09f74f7dbf0214 100644 --- a/fda-ui/components/icons/OrcidIcon.vue +++ b/fda-ui/components/icons/OrcidIcon.vue @@ -1,5 +1,5 @@ <template> - <a :href="link"> + <a :href="link" target="_blank"> <svg xmlns="http://www.w3.org/2000/svg" width="16" fill="#a6ce39" viewBox="0 0 512 512"> <!--! Font Awesome Pro 6.1.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --> <path diff --git a/fda-ui/layouts/default.vue b/fda-ui/layouts/default.vue index 9b8acb966419c1c31d365b74716070ab7c299d86..e7f52db13a801355b2c8dfdd9f623e3c097117ad 100644 --- a/fda-ui/layouts/default.vue +++ b/fda-ui/layouts/default.vue @@ -54,43 +54,48 @@ <v-icon>mdi-magnify</v-icon> </v-btn> <v-spacer /> - <v-btn - v-if="!token" - class="mr-2" - color="secondary" - to="/login"> - <v-icon left>mdi-login</v-icon> Login - </v-btn> - <v-btn - v-if="!token" - class="mr-2" - color="primary" - to="/signup"> - <v-icon left>mdi-account-plus</v-icon> Signup - </v-btn> - <v-menu v-if="user" bottom offset-y left> - <template v-slot:activator="{ on, attrs }"> - <v-btn - icon - v-bind="attrs" - v-on="on"> - <v-icon>mdi-dots-vertical</v-icon> - </v-btn> - </template> - <v-list> - <v-list-item - v-for="locale in availableLocales" - :key="locale.code" - :to="switchLocalePath(locale.code)"> - <v-list-item-title>{{ locale.name }}</v-list-item-title> - </v-list-item> - <v-list-item - v-if="token" - @click="logout"> - Logout - </v-list-item> - </v-list> - </v-menu> + <div v-if="!user"> + <v-btn + class="mr-2" + color="secondary" + to="/login"> + <v-icon left>mdi-login</v-icon> Login + </v-btn> + <v-btn + class="mr-2" + color="primary" + to="/signup"> + <v-icon left>mdi-account-plus</v-icon> Signup + </v-btn> + </div> + <div v-else> + <v-btn to="/user" plain> + {{ user.username }} + </v-btn> + <v-menu bottom offset-y left> + <template v-slot:activator="{ on, attrs }"> + <v-btn + icon + v-bind="attrs" + v-on="on"> + <v-icon>mdi-dots-vertical</v-icon> + </v-btn> + </template> + <v-list> + <v-list-item + v-for="locale in availableLocales" + :key="locale.code" + :to="switchLocalePath(locale.code)"> + <v-list-item-title>{{ locale.name }}</v-list-item-title> + </v-list-item> + <v-list-item + v-if="token" + @click="logout"> + Logout + </v-list-item> + </v-list> + </v-menu> + </div> </v-app-bar> </v-form> <v-main> @@ -111,16 +116,20 @@ </v-card-text> </v-card> </v-footer> - <pre>{{ $store.state }}</pre> </v-app> </template> <script> import { isDeveloper } from '@/utils' import AuthenticationService from '@/api/authentication.service' +import DatabaseService from '@/api/database.service' +import TableService from '@/api/table.service' +import IdentifierService from '@/api/identifier.service' export default { name: 'DefaultLayout', + components: { + }, data () { return { drawer: false, @@ -203,9 +212,6 @@ export default { }, '$route.params.database_id': { handler (id, oldId) { - if (this.user) { - this.setTheme() - } if (id !== oldId) { this.loadDatabase() // this.loadAccess() @@ -225,17 +231,16 @@ export default { } }, mounted () { - // this.loadUser() - // this.setTheme() - // this.loadDatabase() - // .then(() => { - // this.loadIdentifier() - // this.loadTable() - // }) - // this.loadAccess() + if (this.refreshToken) { + AuthenticationService.authenticateToken(this.refreshToken) + } if (this.$route.query && this.$route.query.q) { this.search = this.$route.query.q } + if (!this.user) { + return + } + this.$vuetify.theme.dark = this.user.attributes.theme_dark }, methods: { submit () { @@ -257,119 +262,68 @@ export default { this.$vuetify.theme.dark = false this.$router.push('/container') }, - // async loadUser () { - // if (!this.token) { - // return - // } - // try { - // this.loadingUser = true - // const res = await findUser(this.token) - // const user = res.data - // console.debug('user', user) - // this.$store.commit('SET_USER', user) - // const roles = tokenToRoles(this.token) - // this.$store.commit('SET_ROLES', roles) - // this.$vuetify.theme.dark = getThemeDark(user) - // this.loading = false - // } catch (error) { - // console.error('Failed to load user', error) - // const { status } = error.response - // if (status === 401) { - // console.error('Token expired', error) - // this.$toast.warning('Login has expired') - // this.logout() - // } else { - // console.error('user data', error) - // this.$toast.error('Failed to load user') - // this.error = true - // } - // } - // this.loadingUser = false - // }, - // async loadDatabase () { - // if (!this.$route.params.container_id || !this.$route.params.database_id) { - // return - // } - // try { - // this.loading = true - // const res = await this.$axios.get(`/api/container/${this.$route.params.container_id}/database/${this.$route.params.database_id}`, this.config) - // this.$store.commit('SET_DATABASE', res.data) - // console.debug('database', this.database) - // } catch (err) { - // console.error('Could not load database', err) - // this.$toast.error('Could not load database') - // } - // this.loading = false - // }, - // async loadTable () { - // if (!this.$route.params.container_id || !this.$route.params.database_id || !this.$route.params.table_id) { - // return - // } - // try { - // this.loading = true - // const res = await this.$axios.get(`/api/container/${this.$route.params.container_id}/database/${this.$route.params.database_id}/table/${this.$route.params.table_id}`, this.config) - // this.$store.commit('SET_TABLE', res.data) - // console.debug('table', this.table) - // } catch (error) { - // const { status } = error.response - // if (status === 405) { - // const table = this.database.tables.filter(t => t.id === Number(this.$route.params.table_id))[0] - // this.$store.commit('SET_TABLE', table) - // } else { - // const { message } = error.response.data - // console.error('Failed to load table', error) - // this.$toast.error(`Failed to load table: ${message}`) - // } - // } - // this.loading = false - // }, - // async loadAccess () { - // if (!this.$route.params.container_id || !this.$route.params.database_id) { - // return - // } - // if (!this.token) { - // return - // } - // try { - // this.loading = true - // const res = await this.$axios.get(`/api/container/${this.$route.params.container_id}/database/${this.$route.params.database_id}/access`, this.config) - // this.access = res.data - // this.$store.commit('SET_ACCESS', res.data) - // console.debug('access', this.access) - // } catch (err) { - // this.$store.commit('SET_ACCESS', null) - // const { status } = err.response - // if (status !== 401 && status !== 403) { - // console.error('Failed to check access', err) - // this.$toast.error('Failed to check access') - // } - // } - // this.loading = false - // }, - // async loadIdentifier () { - // if (!this.database || 'identifier' in this.database) { - // return - // } - // try { - // this.loading = true - // const res = await this.$axios.get(`/api/pid/${this.database.identifier.id}`, this.config) - // const db = this.database - // db.identifier = res.data - // this.$store.commit('SET_DATABASE', db) - // } catch (err) { - // console.error('Failed to load identifier', err) - // this.$toast.error('Failed to load identifier') - // } - // this.loading = false - // }, - retrieve () { - this.$router.push({ path: '/search', query: { q: this.search } }) + loadDatabase () { + if (!this.$route.params.container_id || !this.$route.params.database_id) { + this.$store.commit('SET_DATABASE', null) + return + } + this.loading = true + DatabaseService.findOne(this.$route.params.container_id, this.$route.params.database_id) + .then((database) => { + this.$store.commit('SET_DATABASE', database) + this.loading = false + this.loadTable() + }) + .catch(() => { + this.loading = false + }) }, - setTheme () { - if (!this.user || !this.user.theme_dark) { + loadTable () { + if (!this.$route.params.container_id || !this.$route.params.database_id || !this.$route.params.table_id) { return } - this.$vuetify.theme.dark = this.user.theme_dark + this.loading = true + TableService.findOne(this.$route.params.container_id, this.$route.params.database_id, this.$route.params.table_id) + .then((table) => { + this.$store.commit('SET_TABLE', table) + }) + .finally(() => { + this.loading = false + }) + }, + loadAccess () { + if (!this.$route.params.container_id || !this.$route.params.database_id) { + return + } + if (!this.token) { + return + } + this.loading = true + DatabaseService.checkAccess(this.$route.params.container_id, this.$route.params.database_id) + .then((access) => { + this.$store.commit('SET_ACCESS', access) + this.loading = false + }) + .catch(() => { + this.loading = false + }) + }, + loadIdentifier () { + if (!this.database || 'identifier' in this.database) { + return + } + this.loading = true + IdentifierService.findPid(this.database.identifier.id) + .then((identifier) => { + this.database.identifier = identifier + this.$store.commit('SET_DATABASE', this.database) + }) + .finally(() => { + this.loading = false + }) + }, + retrieve () { + this.$router.push({ path: '/search', query: { q: this.search } }) } } } 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 264b049ac42b4b1eff211caed58e932394950d1d..b3349c8c4e362a8bf5f620ec2a7d74735486a3a6 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 @@ -221,7 +221,7 @@ import Persist from '@/components/dialogs/Persist' import Citation from '@/components/identifier/Citation' import { formatTimestampUTCLabel, formatDateUTC } from '@/utils' -import { findQuery, persistQuery } from '@/api/query' +import QueryService from '@/api/query.service' export default { name: 'QueryShow', @@ -402,54 +402,40 @@ export default { this.downloadLoading = false this.metadataLoading = false }, - async downloadData () { + downloadData () { this.downloadLoading = true - try { - const config = this.config - config.headers.Accept = 'text/csv' - const res = await this.$axios.get(`/api/container/${this.$route.params.container_id}/database/${this.$route.params.database_id}/query/${this.$route.params.query_id}/export`, config) - console.debug('export query data', res) - const url = window.URL.createObjectURL(new Blob([res.data])) - const link = document.createElement('a') - link.href = url - link.setAttribute('download', 'subset.csv') - document.body.appendChild(link) - link.click() - } catch (err) { - console.error('Could not export query data', err) - this.$toast.error('Could not export query data') - this.error = true - } - this.downloadLoading = false + QueryService.export(this.$route.params.container_id, this.$route.params.database_id, this.$route.params.query_id) + .then((data) => { + const url = window.URL.createObjectURL(new Blob([data])) + const link = document.createElement('a') + link.href = url + link.setAttribute('download', 'subset.csv') + document.body.appendChild(link) + link.click() + }) + .finally(() => { + this.downloadLoading = false + }) }, - async loadQuery () { + loadQuery () { this.loadingQuery = true - try { - const res = await findQuery(this.token, this.$route.params.container_id, this.$route.params.database_id, this.$route.params.query_id) - console.info('load query', res.data) - this.query = res.data - } catch (err) { - const { statusText, status } = err.response - if (status !== 401 && status !== 405) { - console.error('Failed to load query with status', status, 'and message', statusText) - this.$toast.error('Failed to load query: ' + statusText) - } - this.error = true - } - this.loadingQuery = false + QueryService.findOne(this.$route.params.container_id, this.$route.params.database_id, this.$route.params.query_id) + .then((query) => { + this.query = query + }) + .finally(() => { + this.loadingQuery = false + }) }, - async save () { + save () { this.loadingSave = true - try { - const res = await persistQuery(this.token, this.$route.params.container_id, this.$route.params.database_id, this.$route.params.query_id) - console.info('persisted query', res.data) - this.query = res.data - } catch (error) { - console.error('Failed to persisted query', error) - this.$toast.error('Failed to persisted query') - this.error = true - } - this.loadingSave = false + QueryService.persist(this.$route.params.container_id, this.$route.params.database_id, this.$route.params.query_id) + .then((query) => { + this.query = query + }) + .finally(() => { + this.loadingSave = false + }) }, openDialog () { this.persistQueryDialog = true diff --git a/fda-ui/pages/container/_container_id/database/_database_id/settings.vue b/fda-ui/pages/container/_container_id/database/_database_id/settings.vue index 7a69a85091e52b6c460be216522c3064b0c29976..28cf44a2158a2bbf6cc6c25691ce528d2e5728a4 100644 --- a/fda-ui/pages/container/_container_id/database/_database_id/settings.vue +++ b/fda-ui/pages/container/_container_id/database/_database_id/settings.vue @@ -15,7 +15,9 @@ </template> <template v-slot:item.action="{ item }"> <v-btn + v-if="item.user.username !== user.username" x-small + :disabled="!canModifyAccess" @click="modifyAccess(item)"> Modify </v-btn> @@ -24,6 +26,7 @@ <v-card-text> <v-btn small + :disabled="!canCreateAccess" color="warning" class="black--text" @click="giveAccess"> @@ -85,7 +88,7 @@ <v-dialog v-model="editAccessDialog" max-width="640"> - <EditAccess :username="username" @close-dialog="closeDialog" /> + <EditAccess :username="username" :access-type="accessType" @close-dialog="closeDialog" /> </v-dialog> </div> </template> @@ -93,7 +96,8 @@ <script> import DBToolbar from '@/components/DBToolbar' import EditAccess from '@/components/dialogs/EditAccess' -import { modifyVisibility, modifyOwnership } from '@/api/database' +import DatabaseService from '@/api/database.service' +import UserService from '@/api/user.service' export default { components: { @@ -105,6 +109,7 @@ export default { dialogDelete: false, confirm: null, username: null, + accessType: null, users: [], loading: false, loadingUsers: false, @@ -183,6 +188,18 @@ export default { return false } return this.roles.includes('modify-database-owner') + }, + canModifyAccess () { + if (!this.isOwner) { + return false + } + return this.roles.includes('update-database-access') + }, + canCreateAccess () { + if (!this.isOwner) { + return false + } + return this.roles.includes('create-database-access') } }, watch: { @@ -204,70 +221,53 @@ export default { }, methods: { closeDialog (event) { - if (event.success) { - this.loadDatabase() - } - this.loadDatabase() + this.reloadDatabase() this.editAccessDialog = false }, - async updateDatabaseVisibility () { - try { - this.loading = true - await modifyVisibility(this.token, this.$route.params.container_id, this.$route.params.database_id, this.modifyVisibility.is_public) - this.$toast.success('Successfully updated the database visibility') - location.reload() - } catch (error) { - console.error('Failed to update database visibility', error) - this.$toast.error('Failed to update database visibility') - } - this.loading = false + updateDatabaseVisibility () { + this.loading = true + DatabaseService.modifyVisibility(this.$route.params.container_id, this.$route.params.database_id, this.modifyVisibility.is_public) + .then(() => { + this.$toast.success('Successfully updated the database visibility') + location.reload() + }) + .finally(() => { + this.loading = false + }) }, - async updateDatabaseOwner () { - try { - this.loading = true - await modifyOwnership(this.token, this.$route.params.container_id, this.$route.params.database_id, this.modifyOwner.username) - this.$toast.success('Successfully updated the database owner') - } catch (error) { - console.error('Failed to update database owner', error) - this.$toast.error('Failed to update database owner') - } - this.loading = false + updateDatabaseOwner () { + this.loading = true + DatabaseService.modifyOwner(this.$route.params.container_id, this.$route.params.database_id, this.modifyOwner.username) + .then(() => { + this.$toast.success('Successfully updated the database owner') + location.reload() + }) + .finally(() => { + this.loading = false + }) }, giveAccess () { this.username = null + this.accessType = null this.editAccessDialog = true }, modifyAccess (item) { this.username = item.user.username + this.accessType = item.type this.editAccessDialog = true }, - async loadUsers () { + loadUsers () { this.loadingUsers = true - try { - const res = await this.$axios.get('/api/user', this.config) - this.users = res.data - console.debug('users', this.users) - } catch (error) { - console.error('Failed to load users', error) - const { message } = error.response.data - this.$toast.error(`Failed to load users: ${message}`) - } - this.loadingUsers = false + UserService.findAll() + .then((users) => { + this.users = users + }) + .finally(() => { + this.loadingUsers = false + }) }, - async loadDatabase () { - if (!this.$route.params.container_id || !this.$route.params.database_id) { - return - } - try { - this.loading = true - const res = await this.$axios.get(`/api/container/${this.$route.params.container_id}/database/${this.$route.params.database_id}`, this.config) - this.$store.commit('SET_DATABASE', res.data) - console.debug('database', this.database) - } catch (err) { - console.error('Could not load database', err) - this.$toast.error('Could not load database') - } - this.loading = false + reloadDatabase () { + this.$store.dispatch('reloadDatabase') } } } diff --git a/fda-ui/pages/container/_container_id/database/_database_id/table/_table_id/schema.vue b/fda-ui/pages/container/_container_id/database/_database_id/table/_table_id/schema.vue index 72022b57c94af3625d59739cbba55c117b764262..3f2e2818c750cbbb587c758f4e19ec2a371736bc 100644 --- a/fda-ui/pages/container/_container_id/database/_database_id/table/_table_id/schema.vue +++ b/fda-ui/pages/container/_container_id/database/_database_id/table/_table_id/schema.vue @@ -72,6 +72,7 @@ </template> <script> import TableToolbar from '@/components/TableToolbar' +import TableService from '@/api/table.service' export default { components: { @@ -196,24 +197,22 @@ export default { const { success } = event console.debug('closed dialog', event) if (success) { - this.loadTable() + this.$store.dispatch('reloadTable') } this.dialogSemantic = false }, - async loadTable () { + loadTable () { if (!this.$route.params.container_id || !this.$route.params.database_id || !this.$route.params.table_id) { return } - try { - this.loading = true - const res = await this.$axios.get(`/api/container/${this.$route.params.container_id}/database/${this.$route.params.database_id}/table/${this.$route.params.table_id}`, this.config) - this.$store.commit('SET_TABLE', res.data) - console.debug('table', this.table) - } catch (err) { - console.error('Could not load table', err) - this.$toast.error('Could not load table') - } - this.loading = false + this.loading = true + TableService.findOne(this.$route.params.container_id, this.$route.params.database_id, this.$route.params.table_id) + .then((table) => { + this.$store.commit('SET_TABLE', table) + }) + .finally(() => { + this.loading = false + }) } } } 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 49fd24d9d8cc26ede48cfe6b1277d38b93465c93..e85a9d20187075bf6fce24bf001da095fe8075d7 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 @@ -61,8 +61,7 @@ <script> import TableSchema from '@/components/TableSchema' import { notEmpty } from '@/utils' -import { createTable } from '@/api/table' -import { findDatabase } from '@/api/database' +import TableService from '@/api/table.service' export default { components: { @@ -152,24 +151,14 @@ export default { submit () { this.$refs.form.validate() }, - async createTable () { - try { - this.loading = true - const res = await createTable(this.token, this.$route.params.container_id, this.$route.params.database_id, this.tableCreate) - if (res.status === 201) { - this.error = false + createTable () { + this.loading = true + TableService.create(this.$route.params.container_id, this.$route.params.database_id, this.tableCreate) + .then(async (table) => { this.$toast.success('Table created') - await this.loadDatabase() - await this.$router.push(`/container/${this.$route.params.container_id}/database/${this.databaseId}/table/${res.data.id}`) - } else { - this.error = true - this.$toast.error(`Could not create table: status ${res.status}`) - } - } catch (err) { - this.error = true - console.error('could not create table', err) - this.$toast.error('Could not create table') - } + await this.$store.dispatch('reloadDatabase') + await this.$router.push(`/container/${this.$route.params.container_id}/database/${this.databaseId}/table/${table.id}`) + }) }, schemaClose (event) { console.debug('schema closed', event) @@ -178,21 +167,6 @@ export default { return } this.createTable() - }, - async loadDatabase () { - if (!this.$route.params.container_id || !this.$route.params.database_id) { - return - } - try { - this.loading = true - const res = await findDatabase(this.token, this.$route.params.container_id, this.$route.params.database_id) - this.$store.commit('SET_DATABASE', res.data) - console.debug('database', this.database) - } catch (error) { - console.error('Could not load database', error) - this.$toast.error('Could not load database') - } - this.loading = false } } } 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 37d0ad3816f0492cb211387bae0be5a123c40d51..4c259e20b0458fc8b9aae167332688b324435a9d 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 @@ -188,7 +188,7 @@ import TableSchema from '@/components/TableSchema' import { notEmpty, isNonNegativeInteger, isResearcher } from '@/utils' import ContainerService from '@/api/container.service' -import { listTables, createTable, dataImport } from '@/api/table' +import TableService from '@/api/table.service' import { determineDataTypes } from '@/api/analyse' export default { @@ -354,18 +354,15 @@ export default { } this.loading = false }, - async listTables () { - try { - this.loading = true - const res = await listTables(this.token, this.$route.params.container_id, this.$route.params.database_id) - console.debug('tables', res.data) - this.tableNames = res.data.map(t => t.internal_name) - } catch (error) { - this.error = true - console.error('Failed to list tables', error) - this.$toast.error('Failed to list tables') - } - this.loading = false + listTables () { + this.loading = true + TableService.findAll(this.$route.params.container_id, this.$route.params.database_id) + .then((tables) => { + this.tableNames = tables.map(t => t.internal_name) + }) + .finally(() => { + this.loading = false + }) }, schemaClose (event) { console.debug('schema closed', event) @@ -385,7 +382,7 @@ export default { this.dateFormats = await ContainerService.findOne(this.$route.params.container_id).image.date_formats this.loading = true }, - async createTable () { + createTable () { /* make enum values to array */ const validColumns = this.tableCreate.columns.map((column) => { // validate `id` column: must be a PK @@ -405,36 +402,21 @@ export default { // bail out if there is a problem with one of the columns if (!validColumns.every(Boolean)) { return } - let createResult - try { - this.loading = true - createResult = await createTable(this.token, this.$route.params.container_id, this.$route.params.database_id, this.tableCreate) - this.newTableId = createResult.data.id - console.debug('created table', createResult.data) - } catch (err) { - this.loading = false - this.error = true - if (err.response.status === 409) { - this.$toast.error('Table name already exists') - } else { - this.$toast.error('Could not create table') - } - console.error('create table failed', err) - return - } - let insertResult - try { - insertResult = await dataImport(this.token, this.$route.params.container_id, this.$route.params.database_id, createResult.data.id, this.tableImport) - console.debug('inserted table', insertResult.data) - } catch (err) { - this.loading = false - this.error = true - console.error('insert table failed', err) - this.$toast.error('Could not insert csv into table') - return - } - this.loading = false - this.step = 5 + TableService.create(this.$route.params.container_id, this.$route.params.database_id, this.tableCreate) + .then((table) => { + this.newTableId = table.id + TableService.importCsv(this.$route.params.container_id, this.$route.params.database_id, table.id, this.tableImport) + .then(() => { + this.$toast.success('Successfully created table from import!') + this.step = 5 + }) + .finally(() => { + this.loading = false + }) + }) + .catch(() => { + this.loading = false + }) } } } diff --git a/fda-ui/pages/container/index.vue b/fda-ui/pages/container/index.vue index a77355ab5f2936e6030593dd40a1ebe5b1c53e3f..9e4eb1ffff64834a148762b382d457b47329a09c 100644 --- a/fda-ui/pages/container/index.vue +++ b/fda-ui/pages/container/index.vue @@ -25,7 +25,6 @@ import { mdiDatabaseArrowRightOutline } from '@mdi/js' import CreateDB from '@/components/dialogs/CreateDB' import DatabaseList from '@/components/DatabaseList' -import { tokenToRoles } from '@/api/user' export default { components: { @@ -56,6 +55,9 @@ export default { user () { return this.$store.state.user }, + roles () { + return this.$store.state.roles + }, config () { if (this.token === null) { return {} @@ -68,8 +70,7 @@ export default { if (!this.token) { return false } - const roles = tokenToRoles(this.token) - return roles.includes('create-container') && roles.includes('create-database') + return this.roles.includes('create-container') && this.roles.includes('create-database') } }, methods: { diff --git a/fda-ui/pages/login.vue b/fda-ui/pages/login.vue index d77f7df45a85fe8363992d6964a2086f84eaf777..66351469bcc28af905037d164153f1b2d6b5e24c 100644 --- a/fda-ui/pages/login.vue +++ b/fda-ui/pages/login.vue @@ -59,6 +59,7 @@ <script> import AuthenticationService from '@/api/authentication.service' +import UserService from '@/api/user.service' import UserMapper from '@/api/user.mapper' export default { data () { @@ -103,10 +104,13 @@ export default { this.loading = true AuthenticationService.authenticatePlain(this.username, this.password) .then(() => { - const themeDark = UserMapper.getThemeDark(this.user) - console.debug('theme_dark', themeDark) - this.$vuetify.theme.dark = themeDark - this.$router.push('/container') + const userId = UserMapper.tokenToUserId(this.token) + UserService.findOne(userId) + .then((user) => { + this.$store.commit('SET_USER', user) + this.$vuetify.theme.dark = UserMapper.getThemeDark(this.user) + this.$router.push('/container') + }) }) .catch(() => { this.loading = false diff --git a/fda-ui/pages/user/authentication.vue b/fda-ui/pages/user/authentication.vue index 0843233abfe277a94703ec14830f79f8a867109b..92af24111141ae469c878c4e6eb8df2132e550e2 100644 --- a/fda-ui/pages/user/authentication.vue +++ b/fda-ui/pages/user/authentication.vue @@ -51,7 +51,7 @@ <script> import UserToolbar from '@/components/UserToolbar' -import { updateUserPassword } from '@/api/user' +import UserService from '@/api/user.service' export default { components: { @@ -89,17 +89,16 @@ export default { methods: { submit () { }, - async changePassword () { - try { - this.loadingUpdate = true - const res = await updateUserPassword(this.token, this.user.id, this.password) - console.debug('password', res.data) - this.$toast.success('Successfully changed the password') - } catch (error) { - console.error('Failed to update password', error) - this.$toast.error('Failed to update password') - } - this.loadingUpdate = false + changePassword () { + this.loadingUpdate = true + UserService.updatePassword(this.user.id, this.password) + .then(() => { + this.$toast.success('Successfully changed the password') + this.loadingUpdate = false + }) + .catch(() => { + this.loadingUpdate = false + }) } } } diff --git a/fda-ui/pages/user/info.vue b/fda-ui/pages/user/info.vue index 104c4216d1dca4a7728f8f966c0611b61005c932..7ade357f0a6578ce8c7e682521ffb5454fc880b7 100644 --- a/fda-ui/pages/user/info.vue +++ b/fda-ui/pages/user/info.vue @@ -3,7 +3,7 @@ <UserToolbar /> <v-tabs-items v-model="tab"> <v-tab-item> - <div v-if="canModifyInformation"> + <div> <v-card flat> <v-card-title>User Information</v-card-title> <v-card-text> @@ -26,24 +26,42 @@ <v-col md="6"> <v-text-field v-model="model.firstname" - :rules="[v => !!v || $t('Required')]" - required - label="Firstname *" /> + :disabled="!canModifyInformation" + label="Firstname" /> </v-col> </v-row> <v-row dense> <v-col md="6"> <v-text-field v-model="model.lastname" - :rules="[v => !!v || $t('Required')]" - required - label="Lastname *" /> + :disabled="!canModifyInformation" + label="Lastname" /> + </v-col> + </v-row> + <v-row dense> + <v-col md="6"> + <v-text-field + v-model="model.affiliation" + :disabled="!canModifyInformation" + hint="e.g. University of xyz" + label="Affiliation" /> + </v-col> + </v-row> + <v-row dense> + <v-col md="6"> + <v-text-field + v-model="model.orcid" + :disabled="!canModifyInformation" + maxlength="19" + hint="e.g. 0000-0002-1825-0097" + label="ORCID" /> </v-col> </v-row> <v-row> <v-col> <v-btn small + :disabled="!canModifyInformation" color="primary" :loading="loading" @click="updateInfo"> @@ -51,25 +69,6 @@ </v-btn> </v-col> </v-row> - <!-- <v-row dense>--> - <!-- <v-col md="6">--> - <!-- <v-text-field--> - <!-- v-model="model.affiliation"--> - <!-- disabled--> - <!-- hint="e.g. University of xyz"--> - <!-- label="Affiliation" />--> - <!-- </v-col>--> - <!-- </v-row>--> - <!-- <v-row dense>--> - <!-- <v-col md="6">--> - <!-- <v-text-field--> - <!-- v-model="model.orcid"--> - <!-- disabled--> - <!-- maxlength="19"--> - <!-- hint="e.g. 0000-0002-1825-0097"--> - <!-- label="ORCID" />--> - <!-- </v-col>--> - <!-- </v-row>--> </v-form> </v-card-text> </v-card> @@ -99,7 +98,7 @@ <script> import UserToolbar from '@/components/UserToolbar' -import { tokenToRoles, updateUser, toggleUserTheme, getThemeDark, findUser } from '@/api/user' +import UserService from '@/api/user.service' export default { components: { @@ -130,7 +129,7 @@ export default { return this.$store.state.user }, roles () { - return tokenToRoles(this.token) + return this.$store.state.roles }, config () { if (this.token === null) { @@ -150,86 +149,55 @@ export default { return this.roles.includes('modify-user-information') } }, - watch: { - user () { - this.init() - } - }, mounted () { this.init() }, methods: { submit () { }, - async updateInfo () { - try { - this.loadingUpdate = true - const payload = { - firstname: this.model.firstname, - lastname: this.model.lastname - } - const res = await updateUser(this.token, this.user.id, payload) - console.info('Updated user information') - const user = res.data - console.debug('user', user) - this.$store.commit('SET_USER', user) - this.error = false - this.$toast.success('Successfully updated user information') - } catch (error) { - console.error('update', error) - this.$toast.error('Failed to update user info') - this.error = true + updateInfo () { + this.loadingUpdate = true + const payload = { + firstname: this.model.firstname, + lastname: this.model.lastname, + orcid: this.model.orcid, + affiliation: this.model.affiliation } - this.loadingUpdate = false + UserService.updateInformation(this.user.id, payload) + .then(() => { + console.info('Updated user information') + this.$toast.success('Successfully updated user information') + this.reloadUser() + }) + .finally(() => { + this.loadingUpdate = false + }) }, - async toggleTheme () { - try { - await toggleUserTheme(this.token, this.user.id, this.theme_dark) - await this.loadUser() - this.$vuetify.theme.dark = this.theme_dark - } catch (error) { - const { message } = error.response - console.error('Failed to update theme', error) - this.$toast.error('Failed to update theme: ' + message) - this.error = true - } + reloadUser () { + this.$store.dispatch('reloadUser') }, - async loadUser () { - if (!this.token) { - return - } - try { - const res = await findUser(this.token) - const user = res.data - console.debug('user', user) - this.$store.commit('SET_USER', user) - const roles = tokenToRoles(this.token) - this.$store.commit('SET_ROLES', roles) - this.$vuetify.theme.dark = getThemeDark(user) - this.loading = false - } catch (error) { - console.error('Failed to load user', error) - const { status } = error.response - if (status === 401) { - console.error('Token expired', error) - this.$toast.warning('Login has expired') - this.logout() - } else { - console.error('user data', error) - this.$toast.error('Failed to load user') - this.error = true - } - } + toggleTheme () { + UserService.updateTheme(this.user.id, this.theme_dark) + .then(() => { + this.reloadUser() + this.$vuetify.theme.dark = this.theme_dark + }) }, init () { if (!this.user) { + console.warn('Object user is not yet available') return } - this.theme_dark = getThemeDark(this.user) - this.model.id = this.user.id - this.model.username = this.user.username - this.model.firstname = this.user.given_name - this.model.lastname = this.user.family_name + this.reloadUser() + this.theme_dark = this.user.attributes.theme_dark + this.model = { + id: this.user.id, + username: this.user.username, + firstname: this.user.given_name, + lastname: this.user.family_name, + orcid: this.user.attributes.orcid, + affiliation: this.user.attributes.affiliation + } } } } diff --git a/fda-ui/plugins/axios.js b/fda-ui/plugins/axios.js index f627cf9093166194f6bbe47b51baba5a0d9360c6..5de5da3b202469b256c7ab811d438e21f170df42 100644 --- a/fda-ui/plugins/axios.js +++ b/fda-ui/plugins/axios.js @@ -25,7 +25,7 @@ api.interceptors.request.use((config) => { return config }) } - console.debug('interceptor inject authorization header', exp) + console.debug('interceptor inject authorization header') config.headers.Authorization = `Bearer ${token}` return config }) diff --git a/fda-ui/store/index.js b/fda-ui/store/index.js index 3de9499b7a4ecf8e0be4a51881e7a45a8b164d96..13f9e4e59be7936e970b9f272308195d785c9211 100644 --- a/fda-ui/store/index.js +++ b/fda-ui/store/index.js @@ -1,5 +1,8 @@ import Vue from 'vue' import Vuex, { Store } from 'vuex' +import UserService from '@/api/user.service' +import DatabaseService from '@/api/database.service' +import TableService from '@/api/table.service' Vue.use(Vuex) @@ -25,28 +28,53 @@ const store = new Store({ }, mutations: { SET_TOKEN (state, token) { + console.debug('set state token', token) state.token = token }, SET_REFRESH_TOKEN (state, refreshToken) { + console.debug('set state refreshToken', refreshToken) state.refreshToken = refreshToken }, SET_ROLES (state, roles) { + console.debug('set state roles', roles) state.roles = roles }, SET_USER (state, user) { + console.debug('set state user', user) state.user = user }, SET_DATABASE (state, database) { + console.debug('set state database', database) state.database = database }, SET_TABLE (state, table) { + console.debug('set state table', table) state.table = table }, SET_ACCESS (state, access) { + console.debug('set state access', access) state.access = access } }, actions: { + reloadUser ({ state, commit }) { + UserService.findOne(state.user.id) + .then((user) => { + commit('SET_USER', user) + }) + }, + reloadDatabase ({ state, commit }) { + DatabaseService.findOne(state.database.container.id, state.database.id) + .then((database) => { + commit('SET_DATABASE', database) + }) + }, + reloadTable ({ state, commit }) { + TableService.findOne(state.database.container.id, state.database.id, state.table.id) + .then((table) => { + commit('SET_TABLE', table) + }) + } } }) export default () => store diff --git a/fda-user-service/rest-service/src/main/java/at/tuwien/endpoint/UserEndpoint.java b/fda-user-service/rest-service/src/main/java/at/tuwien/endpoint/UserEndpoint.java index 2340c468874e170ba3edd68e0ae824751d20157e..5316b9e062ffae5a99c06d24bedc5c98f66599c3 100644 --- a/fda-user-service/rest-service/src/main/java/at/tuwien/endpoint/UserEndpoint.java +++ b/fda-user-service/rest-service/src/main/java/at/tuwien/endpoint/UserEndpoint.java @@ -104,7 +104,7 @@ public class UserEndpoint { public ResponseEntity<UserDto> modify(@NotNull @PathVariable("id") String id, @NotNull @Valid @RequestBody UserUpdateDto data, @NotNull Principal principal) - throws UserNotFoundException, ForeignUserException { + throws UserNotFoundException, ForeignUserException, UserAttributeNotFoundException { log.debug("endpoint modify a user, id={}, data={}, principal={}", id, data, principal); final UserDto dto = userMapper.userToUserDto(userService.modify(id, data, principal)); log.trace("modify user resulted in dto {}", dto); @@ -120,7 +120,7 @@ public class UserEndpoint { public ResponseEntity<UserDto> theme(@NotNull @PathVariable("id") String id, @NotNull @Valid @RequestBody UserThemeSetDto data, @NotNull Principal principal) - throws UserNotFoundException, ForeignUserException { + throws UserNotFoundException, ForeignUserException, UserAttributeNotFoundException { log.debug("endpoint modify a user theme, id={}, data={}, principal={}", id, data, principal); final User user = userService.toggleTheme(id, data, principal); final UserDto dto = userMapper.userToUserDto(user); diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/UserAttributeNotFoundException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/UserAttributeNotFoundException.java new file mode 100644 index 0000000000000000000000000000000000000000..f0d3249868a20ff1211226b4fefd2eade255f3db --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/exception/UserAttributeNotFoundException.java @@ -0,0 +1,21 @@ +package at.tuwien.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.NOT_FOUND) +public class UserAttributeNotFoundException extends Exception { + + public UserAttributeNotFoundException(String msg) { + super(msg); + } + + public UserAttributeNotFoundException(String msg, Throwable thr) { + super(msg, thr); + } + + public UserAttributeNotFoundException(Throwable thr) { + super(thr); + } + +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/mapper/UserMapper.java b/fda-user-service/services/src/main/java/at/tuwien/mapper/UserMapper.java index 1313e12b3b702a085222b5b77b52fdd228fb034f..e4fe5c286258089991df210c7099437c2670765b 100644 --- a/fda-user-service/services/src/main/java/at/tuwien/mapper/UserMapper.java +++ b/fda-user-service/services/src/main/java/at/tuwien/mapper/UserMapper.java @@ -6,6 +6,7 @@ import at.tuwien.api.user.UserBriefDto; import at.tuwien.api.user.UserDetailsDto; import at.tuwien.api.user.UserDto; import at.tuwien.entities.user.User; +import at.tuwien.entities.user.UserAttribute; import org.mapstruct.Mapper; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -29,4 +30,12 @@ public interface UserMapper { log.trace("mapped granted authority {} to granted authority {}", data, authority); return authority; } + + default UserAttribute tripleToUserAttribute(String userId, String name, String value) { + return UserAttribute.builder() + .userId(userId) + .name(name) + .value(value) + .build(); + } } diff --git a/fda-user-service/services/src/main/java/at/tuwien/repository/jpa/UserAttributeRepository.java b/fda-user-service/services/src/main/java/at/tuwien/repository/jpa/UserAttributeRepository.java index b3cf3ecedceee547dc556209959dad5cd6c4627f..08077e1525c52223220c166f6a8b4abc4f08270a 100644 --- a/fda-user-service/services/src/main/java/at/tuwien/repository/jpa/UserAttributeRepository.java +++ b/fda-user-service/services/src/main/java/at/tuwien/repository/jpa/UserAttributeRepository.java @@ -4,11 +4,14 @@ import at.tuwien.entities.user.UserAttribute; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; @Repository public interface UserAttributeRepository extends JpaRepository<UserAttribute, String> { + List<UserAttribute> findByUser(String userId); + Optional<UserAttribute> findByUserIdAndName(String userId, String name); } diff --git a/fda-user-service/services/src/main/java/at/tuwien/service/UserAttributeService.java b/fda-user-service/services/src/main/java/at/tuwien/service/UserAttributeService.java new file mode 100644 index 0000000000000000000000000000000000000000..e3471adb395d5c3a56fc8775c977340b48fecb69 --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/service/UserAttributeService.java @@ -0,0 +1,16 @@ +package at.tuwien.service; + +import at.tuwien.entities.user.UserAttribute; +import at.tuwien.exception.UserAttributeNotFoundException; + +import java.util.List; + +public interface UserAttributeService { + List<UserAttribute> findAll(String userId); + + UserAttribute find(String userId, String name) throws UserAttributeNotFoundException; + + UserAttribute update(String userId, String name, String value) throws UserAttributeNotFoundException; + + UserAttribute create(UserAttribute userAttribute); +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/service/UserService.java b/fda-user-service/services/src/main/java/at/tuwien/service/UserService.java index fc4367b76898d55f332020fd19c75655913c0085..4f64b63e02c50d587ce298ce0f6743aa67444ed7 100644 --- a/fda-user-service/services/src/main/java/at/tuwien/service/UserService.java +++ b/fda-user-service/services/src/main/java/at/tuwien/service/UserService.java @@ -7,10 +7,7 @@ import at.tuwien.api.user.UserUpdateDto; import at.tuwien.entities.auth.Realm; import at.tuwien.entities.user.Role; import at.tuwien.entities.user.User; -import at.tuwien.exception.ForeignUserException; -import at.tuwien.exception.RemoteUnavailableException; -import at.tuwien.exception.UserAlreadyExistsException; -import at.tuwien.exception.UserNotFoundException; +import at.tuwien.exception.*; import java.security.Principal; import java.util.List; @@ -38,12 +35,12 @@ public interface UserService { User create(SignupRequestDto data, Realm realm, Role role) throws RemoteUnavailableException, UserNotFoundException, UserAlreadyExistsException; - User modify(String id, UserUpdateDto data, Principal principal) throws UserNotFoundException, ForeignUserException; + User modify(String id, UserUpdateDto data, Principal principal) throws UserNotFoundException, ForeignUserException, UserAttributeNotFoundException; User updatePassword(String id, UserPasswordDto data, Principal principal) throws UserNotFoundException, ForeignUserException; - User toggleTheme(String id, UserThemeSetDto data, Principal principal) throws UserNotFoundException, ForeignUserException; + User toggleTheme(String id, UserThemeSetDto data, Principal principal) throws UserNotFoundException, ForeignUserException, UserAttributeNotFoundException; User find(String id) throws UserNotFoundException; } diff --git a/fda-user-service/services/src/main/java/at/tuwien/service/impl/UserAttributeServiceImpl.java b/fda-user-service/services/src/main/java/at/tuwien/service/impl/UserAttributeServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..6771f33a9cbfffef8377a0c931b74d248799a45b --- /dev/null +++ b/fda-user-service/services/src/main/java/at/tuwien/service/impl/UserAttributeServiceImpl.java @@ -0,0 +1,51 @@ +package at.tuwien.service.impl; + +import at.tuwien.entities.user.UserAttribute; +import at.tuwien.exception.UserAttributeNotFoundException; +import at.tuwien.repository.jpa.UserAttributeRepository; +import at.tuwien.service.UserAttributeService; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Log4j2 +@Service +public class UserAttributeServiceImpl implements UserAttributeService { + + private final UserAttributeRepository userAttributeRepository; + + @Autowired + public UserAttributeServiceImpl(UserAttributeRepository userAttributeRepository) { + this.userAttributeRepository = userAttributeRepository; + } + + @Override + public List<UserAttribute> findAll(String userId) { + return userAttributeRepository.findByUser(userId); + } + + @Override + public UserAttribute find(String userId, String name) throws UserAttributeNotFoundException { + final Optional<UserAttribute> optional = userAttributeRepository.findByUserIdAndName(userId, name); + if (optional.isEmpty()) { + log.error("Failed to find user attribute with name {}", name); + throw new UserAttributeNotFoundException("Failed to find user attribute with name " + name); + } + return optional.get(); + } + + @Override + public UserAttribute update(String userId, String name, String value) throws UserAttributeNotFoundException { + final UserAttribute entity = find(userId, name); + entity.setValue(value); + return userAttributeRepository.save(entity); + } + + @Override + public UserAttribute create(UserAttribute userAttribute) { + return userAttributeRepository.save(userAttribute); + } +} diff --git a/fda-user-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java b/fda-user-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java index c3616f99e6814e1fc2b9160f53c1dfd280794cf9..569eb90a4f15d522a962371196ac534ae3372445 100644 --- a/fda-user-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java +++ b/fda-user-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java @@ -6,15 +6,13 @@ import at.tuwien.api.user.UserThemeSetDto; import at.tuwien.api.user.UserUpdateDto; import at.tuwien.entities.auth.Realm; import at.tuwien.entities.user.*; -import at.tuwien.exception.ForeignUserException; -import at.tuwien.exception.RemoteUnavailableException; -import at.tuwien.exception.UserAlreadyExistsException; -import at.tuwien.exception.UserNotFoundException; +import at.tuwien.exception.*; import at.tuwien.mapper.UserMapper; import at.tuwien.repository.jpa.CredentialRepository; import at.tuwien.repository.jpa.RoleMappingRepository; import at.tuwien.repository.jpa.UserAttributeRepository; import at.tuwien.repository.jpa.UserRepository; +import at.tuwien.service.UserAttributeService; import at.tuwien.service.UserService; import lombok.extern.log4j.Log4j2; import org.keycloak.common.util.Base64; @@ -46,19 +44,19 @@ public class UserServiceImpl implements UserService { private final UserMapper userMapper; private final UserRepository userRepository; + private final UserAttributeService userAttributeService; private final CredentialRepository credentialRepository; private final RoleMappingRepository roleMappingRepository; - private final UserAttributeRepository userAttributeRepository; @Autowired public UserServiceImpl(UserMapper userMapper, UserRepository userRepository, - CredentialRepository credentialRepository, RoleMappingRepository roleMappingRepository, - UserAttributeRepository userAttributeRepository) { + UserAttributeService userAttributeService, CredentialRepository credentialRepository, + RoleMappingRepository roleMappingRepository) { this.userMapper = userMapper; this.userRepository = userRepository; + this.userAttributeService = userAttributeService; this.credentialRepository = credentialRepository; this.roleMappingRepository = roleMappingRepository; - this.userAttributeRepository = userAttributeRepository; } @Override @@ -121,16 +119,16 @@ public class UserServiceImpl implements UserService { user.setRealmId(realm.getId()); user.setCreatedTimestamp(Instant.now().toEpochMilli()); user = userRepository.save(user); - UserAttribute userAttribute = UserAttribute.builder() - .userId(user.getId()) - .name("theme_dark") - .value("false") - .build(); - userAttribute = userAttributeRepository.save(userAttribute); + final UserAttribute userAttribute1 = userAttributeService.create(userMapper.tripleToUserAttribute(user.getId(), + "theme_dark", "false")); + final UserAttribute userAttribute2 = userAttributeService.create(userMapper.tripleToUserAttribute(user.getId(), + "orcid", "")); + final UserAttribute userAttribute3 = userAttributeService.create(userMapper.tripleToUserAttribute(user.getId(), + "affiliation", "")); credential.setUserId(user.getId()); credential = credentialRepository.save(credential); user.setCredentials(List.of(credential)); - user.setAttributes(List.of(userAttribute)); + user.setAttributes(List.of(userAttribute1, userAttribute2, userAttribute3)); final RoleMapping tmp2 = RoleMapping.builder() .userId(user.getId()) .roleId(role.getId()) @@ -144,7 +142,7 @@ public class UserServiceImpl implements UserService { @Override public User modify(String id, UserUpdateDto data, Principal principal) throws UserNotFoundException, - ForeignUserException { + ForeignUserException, UserAttributeNotFoundException { /* check */ User user = findById(id); if (!user.getUsername().equals(principal.getName())) { @@ -153,9 +151,12 @@ public class UserServiceImpl implements UserService { } user.setFirstname(data.getFirstname()); user.setLastname(data.getLastname()); - /* save */ + /* save in metadata database */ user = userRepository.save(user); log.info("Modified user with id {}", user.getId()); + /* modify attributes */ + userAttributeService.update(user.getId(), "orcid", data.getOrcid()); + userAttributeService.update(user.getId(), "affiliation", data.getAffiliation()); return user; } @@ -191,28 +192,15 @@ public class UserServiceImpl implements UserService { @Override public User toggleTheme(String id, UserThemeSetDto data, Principal principal) throws UserNotFoundException, - ForeignUserException { + ForeignUserException, UserAttributeNotFoundException { /* check */ final User user = findById(id); if (!user.getUsername().equals(principal.getName())) { log.error("Failed to modify user: attempting to modify other user"); throw new ForeignUserException("Failed to modify user: attempting to modify other user"); } - final Optional<UserAttribute> optional = userAttributeRepository.findByUserIdAndName(user.getId(), "theme_dark"); - if (optional.isEmpty()) { - final UserAttribute theme = UserAttribute.builder() - .user(user) - .value(data.getThemeDark().toString()) - .build(); - final UserAttribute attribute = userAttributeRepository.save(theme); - log.info("Updated theme by creating attribute with id {}", attribute); - return user; - } - final UserAttribute theme = optional.get(); - theme.setValue(data.getThemeDark().toString()); - final UserAttribute attribute = userAttributeRepository.save(theme); - user.setAttributes(List.of(attribute)); - log.info("Updated theme by updating attribute with id {}", attribute); + final UserAttribute entity = userAttributeService.update(user.getId(), "theme_dark", data.getThemeDark().toString()); + log.info("Updated theme by updating attribute with id {}", entity.getId()); return user; }