diff --git a/.env.unix.example b/.env.unix.example index 3ece8c28877959e1291dbcb8113f7f67bc45ad25..9152e53c7c1ff5b7761e36bad8a39a61ae44dcce 100644 --- a/.env.unix.example +++ b/.env.unix.example @@ -14,5 +14,5 @@ BROKER_PASSWORD=fda KEYCLOAK_ADMIN=fda KEYCLOAK_ADMIN_PASSWORD=fda BROKER_CONSUMERS=2 -WEBSITE=http://example.com +WEBSITE=http://localhost GATEWAY_ENDPOINT=http://gateway-service:9095 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index ea9260df449a4a37fa2d999955ad313a0af156cd..d7fad131454f448ab9a2d19f26a7f3df3fbe3e3c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -136,9 +136,8 @@ services: networks: core: ports: - - "9097:9097" - "8443:8443" - - "8080:8080" + - "8081:8080" env_file: - .env volumes: @@ -362,8 +361,6 @@ services: networks: core: public: - ports: - - "3000:3000" env_file: - .env volumes: @@ -375,3 +372,29 @@ services: condition: service_healthy logging: driver: json-file + + fda-proxy: + restart: no + container_name: dbrepo-proxy + hostname: proxy + build: ./fda-proxy + image: fda-proxy + networks: + core: + public: + ports: + - "80:80" + - "8080:8080" + - "3000:3000" + - "443:443" + env_file: + - .env + depends_on: + fda-search-service: + condition: service_started + fda-gateway-service: + condition: service_healthy + fda-ui: + condition: service_started + logging: + driver: json-file diff --git a/fda-authentication-service/dbrepo-realm.json b/fda-authentication-service/dbrepo-realm.json index 4fbca0146ae43df33caa79539ab5578be27a9b1a..afe3f517a9fe65ffb192f5156087ecbf1e470156 100644 --- a/fda-authentication-service/dbrepo-realm.json +++ b/fda-authentication-service/dbrepo-realm.json @@ -4,7 +4,7 @@ "notBefore" : 0, "defaultSignatureAlgorithm" : "RS256", "revokeRefreshToken" : false, - "refreshTokenMaxReuse" : 0, + "refreshTokenMaxReuse" : 1, "accessTokenLifespan" : 720, "accessTokenLifespanForImplicitFlow" : 900, "ssoSessionIdleTimeout" : 1800, @@ -870,7 +870,7 @@ "otpPolicyLookAheadWindow" : 1, "otpPolicyPeriod" : 30, "otpPolicyCodeReusable" : false, - "otpSupportedApplications" : [ "totpAppGoogleName", "totpAppFreeOTPName", "totpAppMicrosoftAuthenticatorName" ], + "otpSupportedApplications" : [ "totpAppFreeOTPName", "totpAppGoogleName", "totpAppMicrosoftAuthenticatorName" ], "webAuthnPolicyRpEntityName" : "keycloak", "webAuthnPolicySignatureAlgorithms" : [ "ES256" ], "webAuthnPolicyRpId" : "", @@ -1820,6 +1820,23 @@ "config" : { "allow-default-scopes" : [ "true" ] } + }, { + "id" : "1849e52a-b8c9-44a8-af3d-ee19376a1ed1", + "name" : "Trusted Hosts", + "providerId" : "trusted-hosts", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "host-sending-registration-request-must-match" : [ "true" ], + "client-uris-must-match" : [ "true" ] + } + }, { + "id" : "f565cb47-3bcf-4078-8f94-eb4179c375b8", + "name" : "Full Scope Disabled", + "providerId" : "scope", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } }, { "id" : "0efa669d-1017-4b4a-82e1-c2eaf72de2c9", "name" : "Allowed Client Scopes", @@ -1837,60 +1854,25 @@ "subComponents" : { }, "config" : { } }, { - "id" : "3ab11d74-5e76-408a-b85a-26bf8950f979", + "id" : "104ec5a9-025b-4c44-8ac0-82d22887ca3e", "name" : "Allowed Protocol Mapper Types", "providerId" : "allowed-protocol-mappers", - "subType" : "anonymous", - "subComponents" : { }, - "config" : { - "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", - "name" : "Trusted Hosts", - "providerId" : "trusted-hosts", - "subType" : "anonymous", + "subType" : "authenticated", "subComponents" : { }, "config" : { - "host-sending-registration-request-must-match" : [ "true" ], - "client-uris-must-match" : [ "true" ] + "allowed-protocol-mapper-types" : [ "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-attribute-mapper", "oidc-usermodel-property-mapper", "saml-role-list-mapper", "oidc-address-mapper", "saml-user-attribute-mapper", "oidc-full-name-mapper", "saml-user-property-mapper" ] } }, { - "id" : "f565cb47-3bcf-4078-8f94-eb4179c375b8", - "name" : "Full Scope Disabled", - "providerId" : "scope", - "subType" : "anonymous", - "subComponents" : { }, - "config" : { } - }, { - "id" : "104ec5a9-025b-4c44-8ac0-82d22887ca3e", + "id" : "3ab11d74-5e76-408a-b85a-26bf8950f979", "name" : "Allowed Protocol Mapper Types", "providerId" : "allowed-protocol-mappers", - "subType" : "authenticated", + "subType" : "anonymous", "subComponents" : { }, "config" : { - "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" ] + "allowed-protocol-mapper-types" : [ "saml-user-property-mapper", "oidc-usermodel-attribute-mapper", "oidc-usermodel-property-mapper", "saml-user-attribute-mapper", "oidc-full-name-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-address-mapper", "saml-role-list-mapper" ] } } ], - "org.keycloak.userprofile.UserProfileProvider" : [ { - "id" : "2abcde8e-479c-4b3f-85ca-08687ff4b0cf", - "providerId" : "declarative-user-profile", - "subComponents" : { }, - "config" : { } - } ], "org.keycloak.keys.KeyProvider" : [ { - "id" : "2f53ccf3-37b0-4d34-83e7-ed497499ee51", - "name" : "rsa-enc-generated", - "providerId" : "rsa-enc-generated", - "subComponents" : { }, - "config" : { - "privateKey" : [ "MIIEowIBAAKCAQEA3b1tNLfcjFLUw9UShVDNf+ZD8sQqb4YBaIXcSJTX/zDQUPiCp176BBGI3s4VplDArnOW+LumozmKogeoHEnGEIDW8ovgK5uMU9tSA2p0qqGBUMOdR8YATTIfCJe7qGiiuGa3WZy3sQLM70SuRzx02YU8gvUcvl2Js4KyqAziOUX/w3Wa59H9jjGNUXYyqaPWJp73eHzbVYWySzyLG22mVlcUtBx5siL5T2/Xu0p9z4l7/bapwwmOVi1ZrcHjbEAwdGEiSMGI/uWqAF+r1BRpmJLR7HNXcL3eK4/56VYLaiwSejfyYeRFMITEn/UxGYhcXZ5xMUUCG0TxjBhLYpTBuwIDAQABAoIBAA4dwebcxkrH99Poa8+WkiE7JgaS9sahx9OBI2xwJANoIU2TpzGuNLQZ76uLgB+rPWZTD9Xm5a1iJjwOyQ9/937TzPCk91D0tpgcusRikb8jx/6TGB9acL4kBjYUVCCHr3BA2G75MKKGtJ2OMvAbCQSosZj+r2VDwYFEPUkV2jheE5JHSBkwyIRrus3JCwu8gu5fyCg9z8ljcxJxI5HIsi4v8Z21aCw/cLj7h5cMt44wCjQz4rOfYNBEFeHDtlfR1QtWKgjm4ZHHJbKrzf9b2kQXclziceEbSM0tYbROEXKi+s0Zc+z3HEG89vv0vfN400clmzzIAijKY6gg3pPRWdECgYEA+lnWYbSlXDMNYx6RBXm1RnlMUYIm4oy4/9ljgnoGJ6WCn3SjFkgaDtiKfGIG1BSB85r04pAPANgcWHf5tWDnq0ARvBVG0BX2bKd++7B3D4d3CRYKCwm88SslJXv9dfHVhq4+zViFPiUWwT20A72jCuUCvL88y5fh/YBecfdh+jECgYEA4r5RD0NB9dMaeg5/jk/GEHIo4Z9KLc6FrSoOFo2xFkPOy1sgDpDOiNtypuWvniO7k7Ose3DS3hlfTMsKzIW/CgQJ20+Y4cvBWDaOsRxfjj7w3d+jH5OSJdKKSzTrgLKc9ZhlRzVXy0J0hipIA6HG5kdVdLXmh85ITmf1CbJhE6sCgYBjPVeBNbXTHZ2x6/z62aslO5IoQVqetb/kE82hfDOSZcao5Ph9Lam+ttH2ynkAevykj4mBgi+gWwqpey2uW7KaLPSaxShj9kDQA3mP1fzsV/u0y1rB02Nlin/YIxVvOqU1FT9p8SwoXVVu1sHUNck62VtDbN9xqUx5S/ikXrclEQKBgQCoTssOwEcK+Vty9KYcdfy4onTUHZBLdjxl8Iyqkxy7QTQUYRznkvesQPDXEDGO+kk3dyx2KKZt9Hl4IFNww2quPZcvcuMx4DQxjbXXpA8OIIxcta95NepLJwA+mRai3nKCH1A2TlNP7pFeMa5o+8IPly3Ix2lKr4Wepa4PN5i1pwKBgCZ1QP6XAOERl9NznNmU0rXVcvYNP4PIIfQWfvGsldZ4QKkmjjAGiS0/oYqdWs+UDRZyCRChaVjDXO9fk0PEG5OGKAj9nyiYCT/M8xtJ3UeP5ffZZvJ/vnye3QdDIo1e38ZzsWwJHmLYw7fRqY9W5Vxo0Vsy22U3CJY70KTxVdTy" ], - "keyUse" : [ "ENC" ], - "certificate" : [ "MIICmzCCAYMCBgGG3GWycDANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZkYnJlcG8wHhcNMjMwMzEzMTkxMzE3WhcNMzMwMzEzMTkxNDU3WjARMQ8wDQYDVQQDDAZkYnJlcG8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDdvW00t9yMUtTD1RKFUM1/5kPyxCpvhgFohdxIlNf/MNBQ+IKnXvoEEYjezhWmUMCuc5b4u6ajOYqiB6gcScYQgNbyi+Arm4xT21IDanSqoYFQw51HxgBNMh8Il7uoaKK4ZrdZnLexAszvRK5HPHTZhTyC9Ry+XYmzgrKoDOI5Rf/DdZrn0f2OMY1RdjKpo9Ymnvd4fNtVhbJLPIsbbaZWVxS0HHmyIvlPb9e7Sn3PiXv9tqnDCY5WLVmtweNsQDB0YSJIwYj+5aoAX6vUFGmYktHsc1dwvd4rj/npVgtqLBJ6N/Jh5EUwhMSf9TEZiFxdnnExRQIbRPGMGEtilMG7AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAK3kQ1VkQrzvSWvmXmazmNoA1ZiPzRDs1XhGUWxgsxzgPylr3dGBuqQbKvgnLUBQLSqlJHpI4fZflHswu1qrvVZYtekPcGef4WhcKAu2i1RwxrKa6RJQ1tRbrLuVYCzPv5p/DWgltWVn88aoLnqQn0SK/0PB/o4a4Cm7Kq2ZzCr1dACBr06LvOHsc7249OySmbG4HH+pLK6jVURhZ9VaObqAHe2FJBVVoIzURbdiRRURqumrIvbnpeaU1aFyg6ED5wTnXvmMPmVPt9F79mcB33bASO5wyu00X8t1hyN2Show2l2vxLACGUzVkTQt15s7uDLKE7qLmKSR3EuSGXWv3wA=" ], - "priority" : [ "100" ], - "algorithm" : [ "RSA-OAEP" ] - } - }, { "id" : "28ca0b6d-b2e2-4785-b04b-2391e6344e30", "name" : "aes-generated", "providerId" : "aes-generated", @@ -1911,6 +1893,18 @@ "priority" : [ "100" ], "algorithm" : [ "HS256" ] } + }, { + "id" : "2f53ccf3-37b0-4d34-83e7-ed497499ee51", + "name" : "rsa-enc-generated", + "providerId" : "rsa-enc-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEowIBAAKCAQEA3b1tNLfcjFLUw9UShVDNf+ZD8sQqb4YBaIXcSJTX/zDQUPiCp176BBGI3s4VplDArnOW+LumozmKogeoHEnGEIDW8ovgK5uMU9tSA2p0qqGBUMOdR8YATTIfCJe7qGiiuGa3WZy3sQLM70SuRzx02YU8gvUcvl2Js4KyqAziOUX/w3Wa59H9jjGNUXYyqaPWJp73eHzbVYWySzyLG22mVlcUtBx5siL5T2/Xu0p9z4l7/bapwwmOVi1ZrcHjbEAwdGEiSMGI/uWqAF+r1BRpmJLR7HNXcL3eK4/56VYLaiwSejfyYeRFMITEn/UxGYhcXZ5xMUUCG0TxjBhLYpTBuwIDAQABAoIBAA4dwebcxkrH99Poa8+WkiE7JgaS9sahx9OBI2xwJANoIU2TpzGuNLQZ76uLgB+rPWZTD9Xm5a1iJjwOyQ9/937TzPCk91D0tpgcusRikb8jx/6TGB9acL4kBjYUVCCHr3BA2G75MKKGtJ2OMvAbCQSosZj+r2VDwYFEPUkV2jheE5JHSBkwyIRrus3JCwu8gu5fyCg9z8ljcxJxI5HIsi4v8Z21aCw/cLj7h5cMt44wCjQz4rOfYNBEFeHDtlfR1QtWKgjm4ZHHJbKrzf9b2kQXclziceEbSM0tYbROEXKi+s0Zc+z3HEG89vv0vfN400clmzzIAijKY6gg3pPRWdECgYEA+lnWYbSlXDMNYx6RBXm1RnlMUYIm4oy4/9ljgnoGJ6WCn3SjFkgaDtiKfGIG1BSB85r04pAPANgcWHf5tWDnq0ARvBVG0BX2bKd++7B3D4d3CRYKCwm88SslJXv9dfHVhq4+zViFPiUWwT20A72jCuUCvL88y5fh/YBecfdh+jECgYEA4r5RD0NB9dMaeg5/jk/GEHIo4Z9KLc6FrSoOFo2xFkPOy1sgDpDOiNtypuWvniO7k7Ose3DS3hlfTMsKzIW/CgQJ20+Y4cvBWDaOsRxfjj7w3d+jH5OSJdKKSzTrgLKc9ZhlRzVXy0J0hipIA6HG5kdVdLXmh85ITmf1CbJhE6sCgYBjPVeBNbXTHZ2x6/z62aslO5IoQVqetb/kE82hfDOSZcao5Ph9Lam+ttH2ynkAevykj4mBgi+gWwqpey2uW7KaLPSaxShj9kDQA3mP1fzsV/u0y1rB02Nlin/YIxVvOqU1FT9p8SwoXVVu1sHUNck62VtDbN9xqUx5S/ikXrclEQKBgQCoTssOwEcK+Vty9KYcdfy4onTUHZBLdjxl8Iyqkxy7QTQUYRznkvesQPDXEDGO+kk3dyx2KKZt9Hl4IFNww2quPZcvcuMx4DQxjbXXpA8OIIxcta95NepLJwA+mRai3nKCH1A2TlNP7pFeMa5o+8IPly3Ix2lKr4Wepa4PN5i1pwKBgCZ1QP6XAOERl9NznNmU0rXVcvYNP4PIIfQWfvGsldZ4QKkmjjAGiS0/oYqdWs+UDRZyCRChaVjDXO9fk0PEG5OGKAj9nyiYCT/M8xtJ3UeP5ffZZvJ/vnye3QdDIo1e38ZzsWwJHmLYw7fRqY9W5Vxo0Vsy22U3CJY70KTxVdTy" ], + "keyUse" : [ "ENC" ], + "certificate" : [ "MIICmzCCAYMCBgGG3GWycDANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZkYnJlcG8wHhcNMjMwMzEzMTkxMzE3WhcNMzMwMzEzMTkxNDU3WjARMQ8wDQYDVQQDDAZkYnJlcG8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDdvW00t9yMUtTD1RKFUM1/5kPyxCpvhgFohdxIlNf/MNBQ+IKnXvoEEYjezhWmUMCuc5b4u6ajOYqiB6gcScYQgNbyi+Arm4xT21IDanSqoYFQw51HxgBNMh8Il7uoaKK4ZrdZnLexAszvRK5HPHTZhTyC9Ry+XYmzgrKoDOI5Rf/DdZrn0f2OMY1RdjKpo9Ymnvd4fNtVhbJLPIsbbaZWVxS0HHmyIvlPb9e7Sn3PiXv9tqnDCY5WLVmtweNsQDB0YSJIwYj+5aoAX6vUFGmYktHsc1dwvd4rj/npVgtqLBJ6N/Jh5EUwhMSf9TEZiFxdnnExRQIbRPGMGEtilMG7AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAK3kQ1VkQrzvSWvmXmazmNoA1ZiPzRDs1XhGUWxgsxzgPylr3dGBuqQbKvgnLUBQLSqlJHpI4fZflHswu1qrvVZYtekPcGef4WhcKAu2i1RwxrKa6RJQ1tRbrLuVYCzPv5p/DWgltWVn88aoLnqQn0SK/0PB/o4a4Cm7Kq2ZzCr1dACBr06LvOHsc7249OySmbG4HH+pLK6jVURhZ9VaObqAHe2FJBVVoIzURbdiRRURqumrIvbnpeaU1aFyg6ED5wTnXvmMPmVPt9F79mcB33bASO5wyu00X8t1hyN2Show2l2vxLACGUzVkTQt15s7uDLKE7qLmKSR3EuSGXWv3wA=" ], + "priority" : [ "100" ], + "algorithm" : [ "RSA-OAEP" ] + } }, { "id" : "2293ff99-3c6d-46d1-8635-5e679d5b134a", "name" : "rsa-generated", @@ -1927,7 +1921,7 @@ "internationalizationEnabled" : false, "supportedLocales" : [ ], "authenticationFlows" : [ { - "id" : "9b2ffbe1-91b5-4815-b2c6-fdb8d5cf522e", + "id" : "f687eb46-5fae-49f4-a38c-e34263c44f45", "alias" : "Account verification options", "description" : "Method with which to verity the existing account", "providerId" : "basic-flow", @@ -1949,7 +1943,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "d48e99bc-ce6f-4474-b1f1-2b87c578522d", + "id" : "f97dde75-21ae-4e8e-be0b-1e74f876a1dd", "alias" : "Authentication Options", "description" : "Authentication options.", "providerId" : "basic-flow", @@ -1978,7 +1972,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "61b23580-7996-49c4-8370-77bb1532c818", + "id" : "3e54a8dd-917e-424d-adb8-78e0e90fe18f", "alias" : "Browser - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -2000,7 +1994,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "598a4244-04b4-4a8d-9c99-1e1a41f1243b", + "id" : "f22e8af1-8e3c-4399-a8e1-3a0a285e7d65", "alias" : "Direct Grant - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -2022,7 +2016,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "e7ee21a3-baf9-4259-b4c2-7ca8742d0521", + "id" : "6889ac8b-1fb5-4504-a480-34bdf6d6e041", "alias" : "First broker login - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -2044,7 +2038,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "718cf803-48b1-4a96-83b5-bad0ad92cdbb", + "id" : "33eab85c-b74a-4e81-8624-fa068545c1f1", "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", @@ -2066,7 +2060,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "2dae43c5-af72-4a1c-b315-798892e76982", + "id" : "ae3d35c8-14e7-4723-86df-d0898fe5bec5", "alias" : "Reset - Conditional OTP", "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", "providerId" : "basic-flow", @@ -2088,7 +2082,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "ca2e30ea-389c-403a-89de-950bbc488ad4", + "id" : "8d60cb94-57a7-4150-979d-626358119e36", "alias" : "User creation or linking", "description" : "Flow for the existing/non-existing user alternatives", "providerId" : "basic-flow", @@ -2111,7 +2105,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "b11e2f51-ef59-4393-be70-064ed4e1321b", + "id" : "ff6c20e7-b379-4055-99b9-7a2e4024e138", "alias" : "Verify Existing Account by Re-authentication", "description" : "Reauthentication of existing account", "providerId" : "basic-flow", @@ -2133,7 +2127,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "9a39c4b1-ba3c-403c-9620-b93e5a9da467", + "id" : "373b8617-2016-4df5-bf1b-75b8a67ec8b2", "alias" : "browser", "description" : "browser based authentication", "providerId" : "basic-flow", @@ -2169,7 +2163,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "6c1d6c8e-e593-40d6-89c8-7b6044790717", + "id" : "61a55658-9750-44ce-8edf-51030643b9eb", "alias" : "clients", "description" : "Base authentication for clients", "providerId" : "client-flow", @@ -2205,7 +2199,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "f650a4f6-e0b8-47da-8416-b805d7cb8535", + "id" : "be7636a4-4efa-4f88-894f-fbefa8750f31", "alias" : "direct grant", "description" : "OpenID Connect Resource Owner Grant", "providerId" : "basic-flow", @@ -2234,7 +2228,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "363ceb8b-0902-4f97-9006-93d4e6fa3d9a", + "id" : "7fa9637a-ab90-42b4-95da-ed93c3480ba4", "alias" : "docker auth", "description" : "Used by Docker clients to authenticate against the IDP", "providerId" : "basic-flow", @@ -2249,7 +2243,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "41584462-4e61-45d3-bf42-cf1f19266804", + "id" : "48f53c92-8d4a-4e48-9bb1-de1daf756332", "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", @@ -2272,7 +2266,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "e723d622-3bf1-4202-bf51-69de9548ec20", + "id" : "34bcc343-9321-41d3-aa33-eb653181812d", "alias" : "forms", "description" : "Username, password, otp and other auth forms.", "providerId" : "basic-flow", @@ -2294,7 +2288,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "e6f26e36-d5cd-47e7-be78-7b9d21187a42", + "id" : "ff3d99ae-9bbd-46aa-9c29-0c55d3c5c9c4", "alias" : "http challenge", "description" : "An authentication flow based on challenge-response HTTP Authentication Schemes", "providerId" : "basic-flow", @@ -2316,7 +2310,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "e548e0c5-596a-467f-a23b-00b4ddbf68d3", + "id" : "103b0a30-7ad6-41ec-827f-bacdb155496f", "alias" : "registration", "description" : "registration flow", "providerId" : "basic-flow", @@ -2332,7 +2326,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "0cdcba23-b485-416e-873b-f1695646bef8", + "id" : "f080e6b0-a34e-49b6-bc17-c4777e147535", "alias" : "registration form", "description" : "registration form", "providerId" : "form-flow", @@ -2368,7 +2362,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "ed2d6f0c-4414-49e4-bda1-217cb40e168f", + "id" : "cb302fa5-c65a-48d9-94ea-1412ac567065", "alias" : "reset credentials", "description" : "Reset credentials for a user if they forgot their password or something", "providerId" : "basic-flow", @@ -2404,7 +2398,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "a5d09fd7-e988-485b-a5e8-bb9a54e34c42", + "id" : "b281b498-55a2-4909-9138-bc9bde251925", "alias" : "saml ecp", "description" : "SAML ECP Profile Authentication Flow", "providerId" : "basic-flow", @@ -2420,13 +2414,13 @@ } ] } ], "authenticatorConfig" : [ { - "id" : "da63f903-8393-4686-8187-6ca865a79448", + "id" : "78adce28-a91c-4e60-9816-d521ac667de1", "alias" : "create unique user config", "config" : { "require.password.update.after.registration" : "false" } }, { - "id" : "96a14ace-debb-42f0-8dff-701891d6048a", + "id" : "1474c494-141c-448d-a5ec-45c4e43a5939", "alias" : "review profile config", "config" : { "update.profile.on.first.login" : "missing" diff --git a/fda-gateway-service/rest-service/src/main/resources/application-local.yml b/fda-gateway-service/rest-service/src/main/resources/application-local.yml index f1c31c526a7c7ea57f54caf56dffba75a06ffa15..320e14ae2ff24ced3b20791aeb12d31c93d46524 100644 --- a/fda-gateway-service/rest-service/src/main/resources/application-local.yml +++ b/fda-gateway-service/rest-service/src/main/resources/application-local.yml @@ -22,10 +22,6 @@ spring: name: gateway-service cloud: loadbalancer.ribbon.enabled: false - gateway: - httpclient: - ssl: - useInsecureTrustManager: true management.endpoints.web.exposure.include: health,info,prometheus,gateway server: port: 9095 diff --git a/fda-identifier-service/Dockerfile b/fda-identifier-service/Dockerfile index 43768d4a7c6e293ed7e823df846c50e758e4e0d7..e0bab808145952fe5fdaeaa3acc4714bb152791a 100644 --- a/fda-identifier-service/Dockerfile +++ b/fda-identifier-service/Dockerfile @@ -26,7 +26,7 @@ ENV METADATA_DB=fda ENV METADATA_USERNAME=root ENV METADATA_PASSWORD=dbrepo ENV GATEWAY_ENDPOINT=http://gateway-service:9095 -ENV WEBSITE=http://localhost:3000 +ENV WEBSITE=http://localhost ENV LOG_LEVEL=debug ENV DBREPO_CLIENT_SECRET=client-secret ENV CLIENT_ID=dbrepo-client diff --git a/fda-metadata-db/api/src/main/java/at/tuwien/api/user/UserAttributeDto.java b/fda-metadata-db/api/src/main/java/at/tuwien/api/user/UserAttributeDto.java index ee3e1252788c06df6d94857eaf76bc84bc78c175..c5f7b7b0b34bc1d3036ed393e7ef1e979591c7b0 100644 --- a/fda-metadata-db/api/src/main/java/at/tuwien/api/user/UserAttributeDto.java +++ b/fda-metadata-db/api/src/main/java/at/tuwien/api/user/UserAttributeDto.java @@ -1,7 +1,6 @@ package at.tuwien.api.user; import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; @@ -21,7 +20,6 @@ public class UserAttributeDto { @NotNull @JsonIgnore - @JsonProperty("user_id") @Schema(example = "1ffc7b0e-9aeb-4e8b-b8f1-68f3936155b4") private String userId; diff --git a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/Credential.java b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/Credential.java index a189fb25d023b90a606762c3e19f97a78476fa8a..f3da481c38b003df917e0ef0003159c9a6fe5066 100644 --- a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/Credential.java +++ b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/Credential.java @@ -42,6 +42,7 @@ public class Credential { @Column(nullable = false) private Integer priority; + @ToString.Exclude @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) @JoinColumns({ @JoinColumn(name = "user_id", referencedColumnName = "id", insertable = false, updatable = false) diff --git a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/User.java b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/User.java index 75db0aa7076363314e89b5bbb522568464772c87..5aec9968769b5c22b491a87c6e4dbfd212c29923 100644 --- a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/User.java +++ b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/User.java @@ -69,6 +69,7 @@ public class User { private List<UserAttribute> attributes; @Column(nullable = false) + @ToString.Exclude @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "user") private List<Credential> credentials; diff --git a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/UserAttribute.java b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/UserAttribute.java index c0bf71be22e6c2ef7fd0b208a9a1ed5bb3e8aa4c..fbcfc82cf5512f55ca20cec5bac0089ffd4a1d36 100644 --- a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/UserAttribute.java +++ b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/UserAttribute.java @@ -19,14 +19,13 @@ import javax.persistence.*; public class UserAttribute { @Id - @JsonIgnore @EqualsAndHashCode.Include @GeneratedValue(generator = "attribute-uuid") @GenericGenerator(name = "attribute-uuid", strategy = "org.hibernate.id.UUIDGenerator") @Column(name = "ID", nullable = false, columnDefinition = "VARCHAR(36)") private String id; - @JsonIgnore + @ToString.Exclude @Column(name = "USER_ID", nullable = false) private String userId; diff --git a/fda-proxy/Dockerfile b/fda-proxy/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..6086e614d19011a1df5854c3af5881fa7350a237 --- /dev/null +++ b/fda-proxy/Dockerfile @@ -0,0 +1,4 @@ +FROM nginx:alpine AS runtime +MAINTAINER Martin Weise <martin.weise@tuwien.ac.at + +COPY ./dbrepo.conf /etc/nginx/conf.d/default.conf diff --git a/fda-proxy/dbrepo.conf b/fda-proxy/dbrepo.conf new file mode 100644 index 0000000000000000000000000000000000000000..ea6efa7759ba799c81ee96c144ed8d2e35d618e3 --- /dev/null +++ b/fda-proxy/dbrepo.conf @@ -0,0 +1,42 @@ +server { + listen 80 default_server; + server_name _; + + location /api { + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_pass http://gateway-service:9095; + proxy_read_timeout 90; + } + + location /pid { + rewrite /pid/(.*) /api/pid/$1 break; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_pass http://gateway-service:9095; + proxy_read_timeout 90; + } + + location /retrieve { + rewrite /retrieve/(.*) /$1 break; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_pass http://search-service:9200; + proxy_read_timeout 90; + } + + location / { + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_pass http://ui:3000/; + proxy_read_timeout 90; + } +} diff --git a/fda-query-service/services/src/main/java/at/tuwien/service/impl/RabbitMqServiceImpl.java b/fda-query-service/services/src/main/java/at/tuwien/service/impl/RabbitMqServiceImpl.java index 89bcb5425ec77f72cf971366b3bd70ec2e80c7dd..e814476f1d0032afd82b0fc8d6bf48df1581f788 100644 --- a/fda-query-service/services/src/main/java/at/tuwien/service/impl/RabbitMqServiceImpl.java +++ b/fda-query-service/services/src/main/java/at/tuwien/service/impl/RabbitMqServiceImpl.java @@ -69,16 +69,11 @@ public class RabbitMqServiceImpl implements MessageQueueService { for (Table table : tables) { final long consumerCount = consumers.stream().filter(c -> c.getQueue().getName().equals(table.getQueueName())).count(); if (consumerCount >= amqpConfig.getAmqpConsumers()) { - log.trace("listener table with name {} already has {} consumers (max. {})", table.getName(), - consumerCount, amqpConfig.getAmqpConsumers()); continue; } - log.debug("table with id {} has {} consumers, but needs {} in total", table.getId(), consumerCount, - amqpConfig.getAmqpConsumers()); for (long i = consumerCount; i < amqpConfig.getAmqpConsumers(); i++) { createConsumer(table.getQueueName(), table.getDatabase().getContainer().getId(), table.getDatabase().getId(), table.getId()); - log.trace("creating consumer #{}", i); } } } diff --git a/fda-ui/Dockerfile b/fda-ui/Dockerfile index 048398eebaa2a7b2b4b887c685df515a1adbaae5..729a01636095da65efede0bbee586c81025d7db4 100644 --- a/fda-ui/Dockerfile +++ b/fda-ui/Dockerfile @@ -8,7 +8,7 @@ ARG TAG=1.2 ENV NODE_ENV=production ENV HOST=0.0.0.0 -ENV API=http://gateway-service:9095 +ENV API=http://:8080 WORKDIR /app diff --git a/fda-ui/api/analyse.service.js b/fda-ui/api/analyse.service.js new file mode 100644 index 0000000000000000000000000000000000000000..fec2f309121356cd8015fd1c28b7ea6669c4b692 --- /dev/null +++ b/fda-ui/api/analyse.service.js @@ -0,0 +1,23 @@ +import Vue from 'vue' +import api from '@/api' + +class AnalyseService { + determineDataTypes (filepath) { + return new Promise((resolve, reject) => { + api.post('/api/analyse/determinedt', { filepath }, { headers: { Accept: 'application/json' } }) + .then((response) => { + const analysis = response.data + console.debug('response analysis', analysis) + resolve(analysis) + }) + .catch((error) => { + const { code, message } = error + console.error('Failed to load analysis', error) + Vue.$toast.error(`[${code}] Failed to load analysis: ${message}`) + reject(error) + }) + }) + } +} + +export default new AnalyseService() diff --git a/fda-ui/api/analyse/index.js b/fda-ui/api/analyse/index.js deleted file mode 100644 index 66ad332a56dfc7b58104f77c5c8fdd206e4f25a7..0000000000000000000000000000000000000000 --- a/fda-ui/api/analyse/index.js +++ /dev/null @@ -1,12 +0,0 @@ -const axios = require('axios/dist/browser/axios.cjs') - -export function determineDataTypes (token, filepath) { - const payload = { - filepath - } - return axios.post('/api/analyse/determinedt', payload, { - headers: { - Authorization: `Bearer ${token}` - } - }) -} diff --git a/fda-ui/api/authentication.service.js b/fda-ui/api/authentication.service.js index 33dcadd1f25e9dfe138247b46eee372f25767296..df567c8add375a689d7cf14ba5cdc7d74c574d6e 100644 --- a/fda-ui/api/authentication.service.js +++ b/fda-ui/api/authentication.service.js @@ -3,7 +3,7 @@ import store from '@/store' import qs from 'qs' import UserMapper from '@/api/user.mapper' import axios from 'axios' -import { clientSecret } from '@/config' +import { api as endpoint, clientSecret } from '@/config' /** * Service class for interaction with Authentication Service in the back end. @@ -25,7 +25,7 @@ class AuthenticationService { password, grant_type: 'password', client_secret: clientSecret, - scope: 'openid profile roles attributes' + scope: 'roles' } if (!username) { throw new Error('parameter username is empty') @@ -57,29 +57,42 @@ class AuthenticationService { _authenticate (payload) { return new Promise((resolve, reject) => { - axios.post('/api/auth/realms/dbrepo/protocol/openid-connect/token', qs.stringify(payload), { + const instance = axios.create({ + timeout: 10000, + params: {}, + baseURL: endpoint, headers: { 'Content-Type': 'application/x-www-form-urlencoded' } - }).then((response) => { - const authentication = response.data - // eslint-disable-next-line camelcase - const { access_token, refresh_token } = authentication - store().commit('SET_TOKEN', access_token) - store().commit('SET_REFRESH_TOKEN', refresh_token) - store().commit('SET_ROLES', UserMapper.tokenToRoles(access_token)) - resolve(authentication) - }).catch((error) => { - console.error('Failed to authenticate', error) - const { code, message, response } = error - const { status } = response - if (status === 401) { - Vue.$toast.error('Invalid username-password combination.') - } else { - Vue.$toast.error(`[${code}] Failed to authenticate: ${message}`) - } - reject(error) }) + instance.post('/api/auth/realms/dbrepo/protocol/openid-connect/token', qs.stringify(payload)) + .then((response) => { + const authentication = response.data + // eslint-disable-next-line camelcase + const { access_token, refresh_token } = authentication + store().commit('SET_TOKEN', access_token) + store().commit('SET_REFRESH_TOKEN', refresh_token) + store().commit('SET_ROLES', UserMapper.tokenToRoles(access_token)) + resolve(authentication) + }).catch((error) => { + console.error('Failed to authenticate', error) + const { code, message, response } = error + const { status, data } = response + if (status === 401) { + Vue.$toast.error('Invalid username-password combination.') + } else if (data?.error === 'invalid_grant') { + store().commit('SET_TOKEN', null) + store().commit('SET_REFRESH_TOKEN', null) + store().commit('SET_ROLES', []) + store().commit('SET_USER', null) + this.$vuetify.theme.dark = false + Vue.$toast.warning('Authentication expired.') + this.$router.push('/login') + } else { + Vue.$toast.error(`[${code}] Failed to authenticate: ${message}`) + } + reject(error) + }) }) } } diff --git a/fda-ui/api/index.js b/fda-ui/api/index.js index d26eeaab7be40a4f0b7450a251ba12f46fdb6e50..53c4143ccfe36402902d4ab9086c4f04a4194793 100644 --- a/fda-ui/api/index.js +++ b/fda-ui/api/index.js @@ -1,12 +1,13 @@ -import https from 'https' import axios from 'axios' - -const httpsAgent = new https.Agent({ rejectUnauthorized: false }) +import { api as endpoint } from '@/config' const instance = axios.create({ timeout: 10000, params: {}, - httpsAgent + baseURL: endpoint, + headers: { + 'Access-Control-Allow-Origin': '*' + } }) export default instance diff --git a/fda-ui/api/middleware.service.js b/fda-ui/api/middleware.service.js new file mode 100644 index 0000000000000000000000000000000000000000..165013f4963fb85a5630467c994c71d901545821 --- /dev/null +++ b/fda-ui/api/middleware.service.js @@ -0,0 +1,25 @@ +import Vue from 'vue' +import axios from 'axios' + +class MiddlewareService { + upload (file) { + return new Promise((resolve, reject) => { + const data = new FormData() + data.append('file', file) + axios.post('/server-middleware/upload', data, { headers: { 'Content-Type': 'multipart/form-data' } }) + .then((response) => { + const file = response.data + console.debug('response file', file) + resolve(file) + }) + .catch((error) => { + const { code, message } = error + console.error('Failed to create database', error) + Vue.$toast.error(`[${code}] Failed to create database: ${message}`) + reject(error) + }) + }) + } +} + +export default new MiddlewareService() diff --git a/fda-ui/api/query.service.js b/fda-ui/api/query.service.js index 8fc66756bacb5c95f7be93600f68c5438ea86d18..d74601bd533e3b457209282819c0ae09586a3b7c 100644 --- a/fda-ui/api/query.service.js +++ b/fda-ui/api/query.service.js @@ -52,13 +52,89 @@ class QueryService { }) } - export (id, databaseId, queryId) { + importCsv (id, databaseId, tableId, data) { + return new Promise((resolve, reject) => { + api.post(`/api/container/${id}/database/${databaseId}/table/${tableId}/data/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) + }) + }) + } + + insertTuple (id, databaseId, tableId, data) { + return new Promise((resolve, reject) => { + api.post(`/api/container/${id}/database/${databaseId}/table/${tableId}/data`, data, { headers: { Accept: 'text/csv' } }) + .then((response) => { + const tuple = response.data + console.debug('response insert tuple', tuple) + resolve(tuple) + }) + .catch((error) => { + const { code, message, response } = error + const { status } = response + if (status === 423) { + console.error('Database failed to accept tuple', error) + Vue.$toast.error(`Database failed to accept tuple: ${message}`) + } else { + console.error('Failed to insert tuple', error) + Vue.$toast.error(`[${code}] Failed to insert tuple: ${message}`) + } + reject(error) + }) + }) + } + + updateTuple (id, databaseId, tableId, data) { + return new Promise((resolve, reject) => { + api.put(`/api/container/${id}/database/${databaseId}/table/${tableId}/data`, data, { headers: { Accept: 'text/csv' } }) + .then((response) => { + const tuple = response.data + console.debug('response update tuple', tuple) + resolve(tuple) + }) + .catch((error) => { + const { code, message, response } = error + const { status } = response + if (status === 423) { + console.error('Database failed to accept tuple', error) + Vue.$toast.error(`Database failed to accept tuple: ${message}`) + } else { + console.error('Failed to update tuple', error) + Vue.$toast.error(`[${code}] Failed to update tuple: ${message}`) + } + reject(error) + }) + }) + } + + exportSubset (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) + resolve(response.data) + }) + .catch((error) => { + const { code, message } = error + console.error('Failed to export query', error) + Vue.$toast.error(`[${code}] Failed to export query: ${message}`) + reject(error) + }) + }) + } + + exportMetadata (id, mime) { + return new Promise((resolve, reject) => { + api.get(`/api/pid/${id}`, { headers: { Accept: mime } }) + .then((response) => { + resolve(response.data) }) .catch((error) => { const { code, message } = error diff --git a/fda-ui/api/table.service.js b/fda-ui/api/table.service.js index 36a932200a39debe8fe7ab9d135089a2d8348f9d..3b0b55379c93b10102ab3d117d3e106b607b5796 100644 --- a/fda-ui/api/table.service.js +++ b/fda-ui/api/table.service.js @@ -57,23 +57,6 @@ class TableService { }) }) } - - 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/components/DBToolbar.vue b/fda-ui/components/DBToolbar.vue index 026f7dce95417d0e4ba4a9d7addc3d4e5db350f3..973d45682b5a6ff83de0b69257da52ede2a7ae6e 100644 --- a/fda-ui/components/DBToolbar.vue +++ b/fda-ui/components/DBToolbar.vue @@ -29,16 +29,16 @@ <v-spacer /> <v-toolbar-title> <v-btn v-if="canImportCsv" class="mr-2 mb-1" :to="`/container/${$route.params.container_id}/database/${$route.params.database_id}/table/import`"> - <v-icon left>mdi-cloud-upload</v-icon> Import CSV + Import .csv </v-btn> <v-btn v-if="canCreateSubset" color="secondary" class="mb-1 white--text" :to="`/container/${$route.params.container_id}/database/${$route.params.database_id}/query/create`"> - <v-icon left>mdi-wrench</v-icon> Create Subset + Create Subset </v-btn> <v-btn v-if="canCreateView" color="secondary" class="ml-2 mr-2 mb-1 white--text" :to="`/container/${$route.params.container_id}/database/${$route.params.database_id}/view/create`"> - <v-icon left>mdi-view-carousel-outline</v-icon> Create View + Create View </v-btn> <v-btn v-if="canCreateTable" color="primary" class="mb-1" :to="`/container/${$route.params.container_id}/database/${$route.params.database_id}/table/create`"> - <v-icon left>mdi-table-large-plus</v-icon> Create Table + Create Table </v-btn> </v-toolbar-title> <template v-slot:extension> diff --git a/fda-ui/components/DatabaseList.vue b/fda-ui/components/DatabaseList.vue index 7dab9d6bddd8e3b203ed650cd8840320b7666d90..8fe1e8a0de97aa7c8b0755b71a4af4bcf1f39942 100644 --- a/fda-ui/components/DatabaseList.vue +++ b/fda-ui/components/DatabaseList.vue @@ -7,7 +7,7 @@ :to="link(container)" flat tile> - <v-divider class="mx-4" /> + <v-divider v-if="!$vuetify.theme.dark" class="mx-4" /> <v-card-title v-if="!hasDatabase(container)" v-text="container.name" /> <v-card-title v-if="hasDatabase(container)"> <a :href="`/container/${container.id}/database/${container.database.id}`">{{ container.name }}</a> diff --git a/fda-ui/components/TableSchema.vue b/fda-ui/components/TableSchema.vue index 2042009818d6417217b0e2ef4b5ffe30857565ae..82593a90b19456a39f6d6f2a25f8ae39c0f9f69d 100644 --- a/fda-ui/components/TableSchema.vue +++ b/fda-ui/components/TableSchema.vue @@ -3,7 +3,7 @@ <v-alert v-if="needsSequence" border="left" - color="amber lighten-4 black--text"> + color="info"> We create a column named <code>id</code> with a auto-increasing sequence starting at 1. Please specify a column with primary key if you don't want this behavior. </v-alert> <v-form ref="form" v-model="valid"> @@ -82,7 +82,7 @@ </v-row> </div> <div> - <v-btn x-small :loading="loading" @click="addColumn()"> + <v-btn x-small @click="addColumn()"> Add Column </v-btn> </div> @@ -90,7 +90,7 @@ <v-btn v-if="back" class="mt-10 mr-2 mb-1" @click="stepBack()"> Back </v-btn> - <v-btn color="primary" :loading="loading" :disabled="!valid" class="mt-10 mb-1" @click="submit()"> + <v-btn color="primary" :loading="localLoading" :disabled="!valid" class="mt-10 mb-1" @click="submit"> Continue </v-btn> </div> @@ -118,11 +118,17 @@ export default { default () { return false } + }, + loading: { + type: Boolean, + default () { + return false + } } }, data () { return { - loading: false, + localLoading: false, dateFormats: [], valid: true, finished: false, @@ -151,7 +157,13 @@ export default { return this.columns.filter(c => c.primary_key).length === 0 } }, + watch: { + loading () { + this.localLoading = this.loading + } + }, mounted () { + this.localLoading = this.loading this.loadContainer() .then(() => this.loadImage()) }, @@ -165,7 +177,7 @@ export default { async loadContainer () { const getUrl = `/api/container/${this.$route.params.container_id}` try { - this.loading = true + this.localLoading = true const res = await this.$axios.get(getUrl) this.container = res.data console.debug('retrieve container', this.container) @@ -173,12 +185,12 @@ export default { this.error = true console.error('retrieve image date formats failed', err) } - this.loading = false + this.localLoading = false }, async loadImage () { const getUrl = `/api/image/${this.container.image.id}` try { - this.loading = true + this.localLoading = true const res = await this.$axios.get(getUrl) this.dateFormats = res.data.date_formats console.debug('retrieve image date formats', this.dateFormats) @@ -186,10 +198,11 @@ export default { this.error = true console.error('retrieve image date formats failed', err) } - this.loading = false + this.localLoading = false }, submit () { this.finished = true + this.localLoading = true this.$emit('close', { success: true }) }, setOthers (column) { diff --git a/fda-ui/components/TableToolbar.vue b/fda-ui/components/TableToolbar.vue index 1469c3c3fbe22ce3143f25b79e8ea3ef786c4788..6203f5164214d5556d98cf28b8d97c93f1808dd6 100644 --- a/fda-ui/components/TableToolbar.vue +++ b/fda-ui/components/TableToolbar.vue @@ -12,22 +12,22 @@ <v-spacer /> <v-toolbar-title> <v-btn v-if="canAddTuple" class="mr-2 mb-1" @click="addTuple"> - <v-icon left>mdi-plus</v-icon> Add + Add </v-btn> <v-btn v-if="canEditTuple" color="warning" class="mr-2 mb-1 black--text" @click="editTuple"> - <v-icon left>mdi-pencil</v-icon> Edit + Edit </v-btn> <v-btn v-if="canDeleteTuple" color="error" class="mr-2 mb-1" :loading="loadingDelete" @click="deleteItems"> - <v-icon left>mdi-delete</v-icon> Delete<span v-if="selection.length > 1"> {{ selection.length }}</span> + Delete <span v-if="selection.length > 1"> {{ selection.length }}</span> </v-btn> <v-btn v-if="canExecuteQuery" class="mb-1" :to="`/container/${$route.params.container_id}/database/${$route.params.database_id}/query/create?tid=${$route.params.table_id}`" color="secondary"> - <v-icon left>mdi-wrench</v-icon> Create Subset + Create Subset </v-btn> <v-btn v-if="canCreateView" class="ml-2 mb-1" :to="`/container/${$route.params.container_id}/database/${$route.params.database_id}/view/create?tid=${$route.params.table_id}`" color="secondary"> - <v-icon left>mdi-view-carousel</v-icon> Create View + Create View </v-btn> <v-btn v-if="canImportCsv" class="ml-2 mb-1" :to="`/container/${$route.params.container_id}/database/${$route.params.database_id}/table/${$route.params.table_id}/import`"> - <v-icon left>mdi-cloud-upload</v-icon> Import csv + Import csv </v-btn> </v-toolbar-title> </v-toolbar> @@ -35,7 +35,7 @@ <v-tab :to="`/container/${$route.params.container_id}/database/${$route.params.database_id}/table/${$route.params.table_id}/info`"> Info </v-tab> - <v-tab v-if="canReadData" :to="`/container/${$route.params.container_id}/database/${$route.params.database_id}/table/${$route.params.table_id}/data`"> + <v-tab v-if="canViewTableData" :to="`/container/${$route.params.container_id}/database/${$route.params.database_id}/table/${$route.params.table_id}/data`"> Data </v-tab> <v-tab :to="`/container/${$route.params.container_id}/database/${$route.params.database_id}/table/${$route.params.table_id}/schema`"> @@ -138,17 +138,18 @@ export default { } return this.roles.includes('create-database-view') }, - canReadData () { + canViewTableData () { + /* view when database is public or when private: 1) view-table-data role present 2) access is at least read */ if (!this.database) { return false } if (this.database.is_public) { return true } - if (!this.roles) { + if (!this.roles || !this.roles.includes('view-table-data') || !this.access) { return false } - return this.roles.includes('view-table-data') + return this.access.type === 'read' || this.access.type === 'write_own' || this.access.type === 'write_all' }, canImportCsv () { if (!this.roles) { diff --git a/fda-ui/components/UserToolbar.vue b/fda-ui/components/UserToolbar.vue index 5eb2113386e259091babc67dbfdfff9c2a1e08eb..f8e7b086ca8200d40e44bdf70e58b4db4df90bd5 100644 --- a/fda-ui/components/UserToolbar.vue +++ b/fda-ui/components/UserToolbar.vue @@ -12,9 +12,6 @@ <v-tab to="/user/authentication"> Authentication </v-tab> - <v-tab v-if="false" to="/user/developer"> - Developer - </v-tab> </v-tabs> </div> </template> diff --git a/fda-ui/components/dialogs/CreateDB.vue b/fda-ui/components/dialogs/CreateDB.vue index b2877b02809394dfec9831d4d500fa76f442a3f1..e73cd6f336e9b332c0c2c127d5c3394bfb6cdd19 100644 --- a/fda-ui/components/dialogs/CreateDB.vue +++ b/fda-ui/components/dialogs/CreateDB.vue @@ -2,33 +2,35 @@ <div> <v-form ref="form" v-model="valid" autocomplete="off" @submit.prevent="submit"> <v-card> - <v-card-title> - Create Database - </v-card-title> + <v-card-title>Create Database</v-card-title> + <v-card-subtitle>Choose an expressive database name and select a database engine.</v-card-subtitle> <v-card-text> - <v-alert - border="left" - color="info"> - Choose an expressive database name and select a database engine. - </v-alert> - <v-text-field - id="database" - v-model="createContainerDto.name" - name="database" - label="Name *" - autofocus - :rules="[v => notEmpty(v) || $t('Required')]" - required /> - <v-select - id="engine" - v-model="engine" - name="engine" - label="Engine *" - :items="engines" - :item-text="item => `${item.repository}:${item.tag}`" - :rules="[v => !!v || $t('Required')]" - return-object - required /> + <v-row dense> + <v-col> + <v-text-field + id="database" + v-model="createContainerDto.name" + name="database" + label="Name *" + autofocus + :rules="[v => notEmpty(v) || $t('Required')]" + required /> + </v-col> + </v-row> + <v-row dense> + <v-col> + <v-select + id="engine" + v-model="engine" + name="engine" + label="Engine *" + :items="engines" + :item-text="item => `${item.repository}:${item.tag}`" + :rules="[v => !!v || $t('Required')]" + return-object + required /> + </v-col> + </v-row> </v-card-text> <v-card-actions> <v-spacer /> diff --git a/fda-ui/components/dialogs/EditAccess.vue b/fda-ui/components/dialogs/EditAccess.vue index 5d063ca56bd86f2687072453368dd5499a91077f..4f30ef9d85c74d9efbd52f6181a00840e999f9d7 100644 --- a/fda-ui/components/dialogs/EditAccess.vue +++ b/fda-ui/components/dialogs/EditAccess.vue @@ -4,22 +4,26 @@ <v-card> <v-progress-linear v-if="loading" :color="loadingColor" :indeterminate="!error" /> <v-card-title v-text="title" /> + <v-card-subtitle v-if="subtitle" v-text="subtitle" /> <v-card-text> - <v-alert - v-if="modify.type && modify.type !== 'revoke'" - border="left" - color="warning"> - <strong>Dangerous operation:</strong> you are giving this user access to <strong>{{ explanation }}</strong> in your database - </v-alert> - <v-alert - v-if="modify.type && modify.type === 'revoke'" - border="left" - color="error"> - <strong>Dangerous operation:</strong> you are <strong>revoking</strong> all access for this user to your database - </v-alert> + <div v-if="!isModification"> + <v-alert + v-if="modify.type && modify.type !== 'revoke'" + border="left" + color="warning"> + <strong>Dangerous operation:</strong> you are giving this user access to <strong>{{ explanation }}</strong> in your database + </v-alert> + <v-alert + v-if="modify.type && modify.type === 'revoke'" + border="left" + color="error"> + <strong>Dangerous operation:</strong> you are <strong>revoking</strong> all access for this user to your database + </v-alert> + </div> <v-row> <v-col> <v-autocomplete + v-if="!isModification" v-model="modify.username" :items="eligibleUsers" :loading="loadingUsers" @@ -30,7 +34,6 @@ hide-details item-text="username" item-value="username" - :disabled="isModification" single-line label="Username" /> </v-col> @@ -66,8 +69,6 @@ </v-card-actions> </v-card> </v-form> - <pre>{{ eligibleUsers }}</pre> - <pre>{{ modify.username }}</pre> </div> </template> @@ -125,7 +126,10 @@ export default { return this.$store.state.database }, title () { - return (!this.isModification ? 'Give' : 'Modify') + ' database access' + (!this.isModification ? '' : ` of ${this.username}`) + return (!this.isModification ? 'Give' : 'Modify') + ' database access' + }, + subtitle () { + return (this.isModification ? `User with username ${this.username}` : false) }, accessTypes () { if (!this.isModification) { @@ -135,10 +139,6 @@ export default { return this.types }, 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 () { @@ -272,7 +272,6 @@ export default { } else { this.modify.type = this.accessType } - this.$refs.form.reset() } } } diff --git a/fda-ui/components/dialogs/EditTuple.vue b/fda-ui/components/dialogs/EditTuple.vue index a3bf26d5f60d32e26954e764d98a33d233e92716..c8407c457c417db1bc7fc73407378ba7f850d62c 100644 --- a/fda-ui/components/dialogs/EditTuple.vue +++ b/fda-ui/components/dialogs/EditTuple.vue @@ -98,7 +98,7 @@ <v-btn v-if="edit" id="updateTuple" - class="mb-2" + class="mb-2 ml-3 mr-2" :disabled="!valid" color="primary" type="submit" @@ -112,6 +112,8 @@ </template> <script> +import QueryService from '@/api/query.service' + export default { props: { tuple: { @@ -145,7 +147,7 @@ export default { return this.$store.state.token }, title () { - return (this.edit ? 'Edit' : 'Add') + ' tuple' + return (this.edit ? 'Edit' : 'Add') + ' Tuple' } }, watch: { @@ -185,7 +187,7 @@ export default { validateTimestamp (val) { return /^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$/.test(val) }, - async updateTuple () { + updateTuple () { const constraints = {} this.columns .filter(c => c.is_primary_key) @@ -196,46 +198,29 @@ export default { data: this.localTuple, keys: constraints } - try { - await this.$axios.put(`/api/container/${this.$route.params.container_id}/database/${this.$route.params.database_id}/table/${this.$route.params.table_id}/data`, data, { - headers: { Authorization: `Bearer ${this.token}` } + QueryService.updateTuple(this.$route.params.container_id, this.$route.params.database_id, this.$route.params.table_id, data) + .then(() => { + this.$toast.success('Successfully updated tuple!') + this.$emit('close', { success: true }) }) - console.info('update result') - this.$toast.success('Successfully updated tuple!') - this.$emit('close', { success: true }) - } catch (error) { - console.error('Failed to update tuple', error) - const { message } = error.response.data - this.$toast.error('Failed to update tuple: ' + message) - } }, - async addTuple () { + addTuple () { const constraints = {} this.columns .filter(c => c.is_primary_key) .forEach((c) => { constraints[c.internal_name] = this.localTuple[c.internal_name] }) - try { - const res = await this.$axios.post(`/api/container/${this.$route.params.container_id}/database/${this.$route.params.database_id}/table/${this.$route.params.table_id}/data`, { - data: this.tuple - }, { - headers: { Authorization: `Bearer ${this.token}` } - }) - console.info('add result', res.data) - this.$toast.success('Successfully added tuple!') - this.$emit('close', { success: true }) - } catch (error) { - console.error('Failed to add tuple', error) - const { message, status } = error.response.data - if (status === 423) { - console.error('Database failed to accept tuple', message) - this.$toast.error(`Database failed to accept tuple: ${message}`) - } else { - console.error('Failed to add tuple', message) - this.$toast.error(`${message}`) + this.columns.forEach((column) => { + if (!(column.internal_name in this.localTuple)) { + this.localTuple[column.internal_name] = null } - } + }) + QueryService.insertTuple(this.$route.params.container_id, this.$route.params.database_id, this.$route.params.table_id, { data: this.localTuple }) + .then(() => { + this.$toast.success('Successfully added tuple!') + this.$emit('close', { success: true }) + }) } } } diff --git a/fda-ui/config.js b/fda-ui/config.js index bff062a685fa2d01f6692136b798ae069b467dd3..cb738d6280ada1d091e30afab9486c8d6ae36f63 100644 --- a/fda-ui/config.js +++ b/fda-ui/config.js @@ -1,7 +1,7 @@ const config = {} -config.api = process.env.API || 'http://localhost:3000/api' -config.search = process.env.SEARCH || 'http://localhost:9200' +config.api = process.env.API || 'http://localhost' +config.search = process.env.SEARCH || 'http://localhost/retrieve' config.sandbox = process.env.SANDBOX || false config.title = process.env.TITLE || 'Database Repository' config.icon = process.env.ICON || '/favicon.ico' diff --git a/fda-ui/layouts/default.vue b/fda-ui/layouts/default.vue index e7f52db13a801355b2c8dfdd9f623e3c097117ad..ce9934fec660d00f75a1b6fcc762e1fea25e37d8 100644 --- a/fda-ui/layouts/default.vue +++ b/fda-ui/layouts/default.vue @@ -214,7 +214,7 @@ export default { handler (id, oldId) { if (id !== oldId) { this.loadDatabase() - // this.loadAccess() + this.loadAccess() } }, deep: true, diff --git a/fda-ui/nuxt.config.js b/fda-ui/nuxt.config.js index 0800379cf2485b00f0efa323a78bb227cb17dddb..27277ba5076a6e82f5dc30b4425d146455e1e6f8 100644 --- a/fda-ui/nuxt.config.js +++ b/fda-ui/nuxt.config.js @@ -78,24 +78,6 @@ export default { defaultPublisher }, - proxy: { - '/api': api, - '/pid': { - target: api + '/api', - changeOrigin: true, - pathRewrite: { - '^/pid': '/pid' - } - }, - '/retrieve': { - target: search, - changeOrigin: true, - pathRewrite: { - '^/retrieve': '' - } - } - }, - serverMiddleware: [ { path: '/server-middleware', handler: path.resolve(__dirname, 'server-middleware/index.js') } ], diff --git a/fda-ui/package.json b/fda-ui/package.json index ff23e62ae94a9fff5f6ad2d24cc4092fa5e861d5..568ee827abb2ba00b51961cfac1c3110c9812147 100644 --- a/fda-ui/package.json +++ b/fda-ui/package.json @@ -47,6 +47,7 @@ "qs": "^6.11.1", "sql-formatter": "^6.1.1", "vue": "^2.6.12", + "vue-axios": "^3.5.2", "vue-chartjs": "^4.1.1", "vue-jwt-decode": "^0.1.0", "vue-toast-notification": "^0.5.4", 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 b3349c8c4e362a8bf5f620ec2a7d74735486a3a6..042ef0a7f0faa3947759f1cc74748d7051eca41f 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 @@ -17,10 +17,10 @@ <v-btn v-if="query.is_persisted && !query.identifier && canWrite" class="mb-1 mr-2" color="primary" :disabled="!executionUTC" @click.stop="openDialog()"> <v-icon left>mdi-content-save-outline</v-icon> Get PID </v-btn> - <v-btn v-if="result_visibility && !query.identifier && query.result_number" class="mb-1" :loading="downloadLoading" @click.stop="downloadData"> + <v-btn v-if="result_visibility && !query.identifier && query.result_number" class="mb-1" :loading="downloadLoading" @click.stop="downloadSubset"> <v-icon left>mdi-download</v-icon> Data .csv </v-btn> - <v-btn v-if="result_visibility && query.identifier && query.result_number" class="mb-1" :loading="downloadLoading" @click.stop="download('text/csv')"> + <v-btn v-if="result_visibility && query.identifier && query.result_number" class="mb-1" :loading="downloadLoading" @click.stop="downloadMetadata('text/csv')"> <v-icon left>mdi-download</v-icon> Data .csv </v-btn> <v-btn @@ -28,7 +28,7 @@ color="secondary" class="ml-2" :loading="metadataLoading" - @click.stop="download('text/xml')"> + @click.stop="downloadMetadata('text/xml')"> <v-icon left>mdi-code-tags</v-icon> Metadata .xml </v-btn> </v-toolbar-title> @@ -373,38 +373,33 @@ export default { this.$refs.queryResults.reExecute(this.query.id) this.$refs.queryResults.reExecuteCount(this.query.id) }, - async download (mime) { + downloadMetadata (mime) { if (mime === 'text/csv') { this.downloadLoading = true } else if (mime === 'text/xml') { this.metadataLoading = true } - try { - const config = this.config - config.headers.Accept = mime - const res = await this.$axios.get(`/api/pid/${this.query.identifier.id}`, config) - console.debug('export identifier', res) - const url = window.URL.createObjectURL(new Blob([res.data])) - const link = document.createElement('a') - link.href = url - if (mime === 'text/csv') { - link.setAttribute('download', 'subset.csv') - } else if (mime === 'text/xml') { - link.setAttribute('download', 'identifier.xml') - } - document.body.appendChild(link) - link.click() - } catch (err) { - console.error('Could not export identifier', err) - this.$toast.error('Could not export identifier') - this.error = true - } - this.downloadLoading = false - this.metadataLoading = false + QueryService.exportMetadata(this.query.identifier.id, mime) + .then((metadata) => { + const url = window.URL.createObjectURL(new Blob([metadata])) + const link = document.createElement('a') + link.href = url + if (mime === 'text/csv') { + link.setAttribute('download', 'subset.csv') + } else if (mime === 'text/xml') { + link.setAttribute('download', 'identifier.xml') + } + document.body.appendChild(link) + link.click() + }) + .finally(() => { + this.downloadLoading = false + this.metadataLoading = false + }) }, - downloadData () { + downloadSubset () { this.downloadLoading = true - QueryService.export(this.$route.params.container_id, this.$route.params.database_id, this.$route.params.query_id) + QueryService.exportSubset(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') @@ -419,13 +414,17 @@ export default { }, loadQuery () { this.loadingQuery = true - 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 - }) + return new Promise((resolve, reject) => { + QueryService.findOne(this.$route.params.container_id, this.$route.params.database_id, this.$route.params.query_id) + .then((query) => { + this.query = query + resolve(query) + }) + .catch(error => reject(error)) + .finally(() => { + this.loadingQuery = false + }) + }) }, save () { this.loadingSave = true diff --git a/fda-ui/pages/container/_container_id/database/_database_id/table/_table_id/data.vue b/fda-ui/pages/container/_container_id/database/_database_id/table/_table_id/data.vue index aa865b1e30c5f20f768978e77e64f8d01a70efc5..59e8aa9acf679c91e21f5f02161e790a4ace0cb5 100644 --- a/fda-ui/pages/container/_container_id/database/_database_id/table/_table_id/data.vue +++ b/fda-ui/pages/container/_container_id/database/_database_id/table/_table_id/data.vue @@ -1,5 +1,5 @@ <template> - <div v-if="canRead"> + <div v-if="canViewTableData"> <TableToolbar :selection="selection" @modified="modified" /> <v-toolbar :color="versionColor" flat> <v-toolbar-title> @@ -89,6 +89,9 @@ export default { token () { return this.$store.state.token }, + roles () { + return this.$store.state.roles + }, database () { return this.$store.state.database }, @@ -156,11 +159,15 @@ export default { } return this.access.type === 'write_all' }, - canRead () { - if (this.database?.is_public) { + canViewTableData () { + /* view when database is public or when private: 1) view-table-data role present 2) access is at least read */ + if (!this.database) { + return false + } + if (this.database.is_public) { return true } - if (!this.user || !this.access) { + if (!this.roles || !this.roles.includes('view-table-data') || !this.access) { return false } return this.access.type === 'read' || this.access.type === 'write_own' || this.access.type === 'write_all' diff --git a/fda-ui/pages/container/_container_id/database/_database_id/table/_table_id/import.vue b/fda-ui/pages/container/_container_id/database/_database_id/table/_table_id/import.vue index d7358dac361459693a72e58ba05d98906621f231..d7ddea1caf6b9eccccce3656b2b52ba60750ef7a 100644 --- a/fda-ui/pages/container/_container_id/database/_database_id/table/_table_id/import.vue +++ b/fda-ui/pages/container/_container_id/database/_database_id/table/_table_id/import.vue @@ -1,5 +1,5 @@ <template> - <div v-if="isResearcher"> + <div v-if="canInsertTableData"> <v-toolbar flat> <v-toolbar-title> <v-btn id="back-btn" class="mr-2" :to="`/container/${$route.params.container_id}/database/${$route.params.database_id}/table`"> @@ -10,7 +10,7 @@ {{ table.name }} </v-toolbar-title> </v-toolbar> - <v-stepper v-model="step" vertical flat> + <v-stepper v-model="step" vertical flat tile> <v-stepper-step :complete="step > 1" step="1"> Import Data </v-stepper-step> @@ -96,10 +96,14 @@ </v-stepper-content> </v-stepper> <v-breadcrumbs :items="items" class="pa-0 mt-2" /> + <pre>loading={{ loading }}</pre> </div> </template> <script> -const { isNonNegativeInteger, isResearcher } = require('@/utils') +import TableService from '@/api/table.service' +import MiddlewareService from '@/api/middleware.service' +import QueryService from '@/api/query.service' +const { isNonNegativeInteger } = require('@/utils') export default { name: 'TableImportCSV', @@ -149,12 +153,6 @@ export default { } }, computed: { - tableId () { - return this.$route.params.table_id - }, - databaseId () { - return this.$route.params.database_id - }, token () { return this.$store.state.token }, @@ -169,14 +167,14 @@ export default { user () { return this.$store.state.user }, - isResearcher () { - return isResearcher(this.user) - }, - fileConfig () { - return { headers: { 'Content-Type': 'multipart/form-data' } } + roles () { + return this.$store.state.roles }, - sharedFilesystem () { - return this.$config.sharedFilesystem + canInsertTableData () { + if (!this.roles) { + return false + } + return this.roles.includes('insert-table-data') } }, mounted () { @@ -185,54 +183,36 @@ export default { methods: { isNonNegativeInteger, uploadAndImport () { - this.upload() - .then(() => this.import()) + this.loading = true + MiddlewareService.upload(this.fileModel) + .then((file) => { + this.file = file + this.tableImport.location = `/tmp/${this.file.filename}` + QueryService.importCsv(this.$route.params.container_id, this.$route.params.database_id, this.$route.params.table_id, this.tableImport) + .then(() => { + this.$toast.success('Successfully imported data') + this.$router.push(`/container/${this.$route.params.container_id}/database/${this.$route.params.database_id}/table/${this.$route.params.table_id}`) + }) + .finally(() => { + this.loading = false + }) + }) + .catch(() => { + this.loading = false + }) }, submit () { this.$refs.form.validate() }, - async loadTableMetadata () { - this.loading = true - try { - const res = await this.$axios.get(`/api/container/${this.$route.params.container_id}/database/${this.databaseId}/table/${this.tableId}`, this.config) - console.debug('got table', res.data) - this.table = res.data - } catch (err) { - console.error('Could not insert data.', err) - } - this.loading = false - }, - async upload () { - this.loading = true - const data = new FormData() - data.append('file', this.fileModel) - try { - const res = await this.$axios.post('/server-middleware/upload', data, this.fileConfig) - console.debug('file upload', res.data) - this.file = res.data - } catch (err) { - console.error('Failed to upload .csv data', err) - console.debug('failed to upload .csv data, does the .csv contain a header line?') - this.$toast.error('Could not upload data') - } - this.loading = false - }, - async import () { + loadTableMetadata () { this.loading = true - const insertUrl = `/api/container/${this.$route.params.container_id}/database/${this.databaseId}/table/${this.tableId}/data/import` - this.tableImport.location = `/tmp/${this.file.filename}` - let insertResult - try { - insertResult = await this.$axios.post(insertUrl, this.tableImport, this.config) - console.debug('imported data', insertResult.data) - } catch (err) { - console.error('Could not import data.', err) - this.loading = false - return - } - this.$toast.success('Successfully imported data') - this.loading = false - this.$router.push(`/container/${this.$route.params.container_id}/database/${this.$route.params.database_id}/table/${this.$route.params.table_id}`) + TableService.findOne(this.$route.params.container_id, this.$route.params.database_id, this.$route.params.table_id) + .then((table) => { + this.table = table + }) + .finally(() => { + 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 4c259e20b0458fc8b9aae167332688b324435a9d..72a19a252f0c02ed3c30210f101a363ad4585e22 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 @@ -1,9 +1,16 @@ <template> <div v-if="canInsertTableData"> <v-toolbar flat> - <v-toolbar-title>Create Table Schema (and Import Data) from .csv/.tsv</v-toolbar-title> + <v-toolbar-title> + <v-btn id="back-btn" class="mr-2" :to="`/container/${$route.params.container_id}/database/${$route.params.database_id}/table`"> + <v-icon left>mdi-arrow-left</v-icon> + </v-btn> + </v-toolbar-title> + <v-toolbar-title> + Create Table Schema (and Import Data) from .csv/.tsv + </v-toolbar-title> </v-toolbar> - <v-stepper v-model="step" vertical flat> + <v-stepper v-model="step" vertical flat tile> <v-stepper-step :complete="step > 1" step="1"> Table Information </v-stepper-step> @@ -166,7 +173,7 @@ Table Schema </v-stepper-step> <v-stepper-content step="4"> - <TableSchema :back="true" :error="error" :columns="tableCreate.columns" @close="schemaClose" /> + <TableSchema :back="true" :error="error" :loading="loading" :columns="tableCreate.columns" @close="schemaClose" /> </v-stepper-content> <v-stepper-step :complete="step > 5" @@ -189,7 +196,8 @@ import TableSchema from '@/components/TableSchema' import { notEmpty, isNonNegativeInteger, isResearcher } from '@/utils' import ContainerService from '@/api/container.service' import TableService from '@/api/table.service' -import { determineDataTypes } from '@/api/analyse' +import MiddlewareService from '@/api/middleware.service' +import AnalyseService from '@/api/analyse.service' export default { name: 'TableFromCSV', @@ -312,47 +320,44 @@ export default { submit () { this.$refs.form.validate() }, - async upload () { + upload () { this.loading = true - const data = new FormData() - data.append('file', this.fileModel) - try { - const res = await this.$axios.post('/server-middleware/upload', data, this.fileConfig) - console.debug('file upload', res.data) - this.file = res.data - } catch (err) { - console.error('Failed to upload .csv data', err) - this.$toast.error('Could not upload data') - } - this.loading = false + return new Promise((resolve, reject) => { + MiddlewareService.upload(this.fileModel) + .then((file) => { + this.file = file + resolve(file) + }) + .catch((error) => { + reject(error) + }) + .finally(() => { + this.loading = false + }) + }) }, - async analyse () { + analyse () { this.loading = true - try { - const res = await determineDataTypes(this.token, `/tmp/${this.file.filename}`) - const { columns } = res.data - console.log('data analyse result', columns) - this.tableCreate.columns = Object.entries(columns) - .map(([key, val]) => { - return { - name: key, - type: val, - null_allowed: true, - primary_key: false, - enum_values: [] - } - }) - this.tableImport.location = `/tmp/${this.file.filename}` - this.step = 4 - this.loading = false - console.debug('upload csv', res.data) - return - } catch (err) { - console.error('Failed to upload .csv data', err) - console.debug('failed to upload .csv data, does the .csv contain a header line?') - this.$toast.error('Could not upload data') - } - this.loading = false + AnalyseService.determineDataTypes(`/tmp/${this.file.filename}`) + .then((analysis) => { + const { columns } = analysis + this.tableCreate.columns = Object.entries(columns) + .map(([key, val]) => { + return { + name: key, + type: val, + null_allowed: true, + primary_key: false, + enum_values: [] + } + }) + this.tableImport.location = `/tmp/${this.file.filename}` + this.step = 4 + this.loading = false + }) + .finally(() => { + this.loading = false + }) }, listTables () { this.loading = true @@ -371,6 +376,7 @@ export default { return } this.validStep4 = true + this.step = 5 this.createTable() }, setOthers (column) { @@ -423,4 +429,13 @@ export default { </script> <style scoped> +#back-btn { + min-width: auto; + padding: 0 0 0 12px; + background: none !important; + box-shadow: none; +} +#back-btn::before { + opacity: 0; +} </style> diff --git a/fda-ui/pages/login.vue b/fda-ui/pages/login.vue index 66351469bcc28af905037d164153f1b2d6b5e24c..d7d26e64a919a0c98d63db1a0973502f903eca92 100644 --- a/fda-ui/pages/login.vue +++ b/fda-ui/pages/login.vue @@ -108,7 +108,7 @@ export default { UserService.findOne(userId) .then((user) => { this.$store.commit('SET_USER', user) - this.$vuetify.theme.dark = UserMapper.getThemeDark(this.user) + this.$vuetify.theme.dark = user.attributes.theme_dark this.$router.push('/container') }) }) diff --git a/fda-ui/pages/search/index.vue b/fda-ui/pages/search/index.vue index a466a0f9229d395bfd03be5e671e250eec482079..d13f084ad5eaee140ee3a3503b7eb9c66aa46757 100644 --- a/fda-ui/pages/search/index.vue +++ b/fda-ui/pages/search/index.vue @@ -7,6 +7,7 @@ <v-card v-for="(result, idx) in results" :key="idx" + :to="link(result)" flat tile> <v-divider class="mx-4" /> diff --git a/fda-ui/pages/user/developer.vue b/fda-ui/pages/user/developer.vue deleted file mode 100644 index 7199d562b745dc63ef6aff2f6d926a2170eca0c7..0000000000000000000000000000000000000000 --- a/fda-ui/pages/user/developer.vue +++ /dev/null @@ -1,234 +0,0 @@ -<template> - <div> - <UserToolbar /> - <v-tabs-items v-model="tab"> - <v-tab-item> - <v-card flat tile> - <v-card-title>Personal Access Tokens</v-card-title> - <v-card-text> - <v-list-item v-for="(item, i) in tokens" :key="i" three-line> - <v-list-item-content> - <v-list-item-title :class="tokenClass(item)">sha256:{{ item.token_hash }}</v-list-item-title> - <v-list-item-subtitle v-if="!item.token" :class="tokenClass(item)"> - Last used: <span v-if="item.last_used">{{ format(item.last_used) }}</span><span v-if="!item.last_used">Never</span> — valid until: {{ format(item.expires) }} - </v-list-item-subtitle> - <v-list-item-subtitle v-if="item.token"> - <v-text-field - v-model="item.token" - :append-outer-icon="item.copied ? 'mdi-check' : 'mdi-content-copy'" - readonly - hint="Copy this token, it will not be visible again!" - persistent-hint - type="text" - @click:append-outer="copy(item)" /> - </v-list-item-subtitle> - <v-list-item-subtitle v-if="!item.token"> - <a @click="revokeToken(item.id)">Revoke Token</a> - </v-list-item-subtitle> - </v-list-item-content> - </v-list-item> - <v-btn - v-if="isResearcher || isDeveloper" - :disabled="tokens.length >= tokenMax" - class="mt-4" - color="secondary" - small - @click="mintToken"> - Create Token - </v-btn> - </v-card-text> - <v-divider v-if="isDeveloper" /> - <v-card-title v-if="isDeveloper">User Roles</v-card-title> - <v-card-subtitle v-if="isDeveloper">Modify user roles</v-card-subtitle> - <v-data-table - v-if="isDeveloper" - :headers="headers" - :items="users" - :loading="loadingUsers" - :items-per-page="10"> - <template v-slot:item.username="{ item }"> - {{ item.username }} - </template> - <template v-slot:item.roles="{ item }"> - <div v-for="(role, idx) in item.roles" :key="idx"> - {{ formatRole(role) }} - </div> - </template> - <template v-slot:item.action="{ item }"> - <v-btn - v-if="item.username !== user.username" - :disabled="isDeveloper1(item)" - x-small - @click="modifyRoles(item)"> - Modify - </v-btn> - <span v-if="item.username === user.username">(you)</span> - </template> - </v-data-table> - </v-card> - </v-tab-item> - </v-tabs-items> - <v-dialog - v-model="editRoleDialog" - persistent - max-width="640"> - <EditRoles :user="selectedUser" @close-dialog="closeDialog" /> - </v-dialog> - </div> -</template> - -<script> -import { formatTimestamp, isResearcher, isDeveloper } from '@/utils' -import UserToolbar from '@/components/UserToolbar' -import EditRoles from '@/components/dialogs/EditRoles' - -export default { - components: { - EditRoles, - UserToolbar - }, - data () { - return { - tab: 0, - error: false, - tokens: [], - loading: false, - loadingUsers: false, - users: [], - editRoleDialog: false, - selectedUser: {}, - roles: [ - { text: 'Researcher', value: 'researcher', code: 'ROLE_RESEARCHER' }, - { text: 'Data Steward', value: 'data_steward', code: 'ROLE_DATA_STEWARD' }, - { text: 'Developer', value: 'developer', code: 'ROLE_DEVELOPER' } - ] - } - }, - computed: { - token () { - return this.$store.state.token - }, - user () { - return this.$store.state.user - }, - headers () { - return [ - { text: 'Username', value: 'username', sortable: false }, - { text: 'Role', value: 'roles', sortable: false }, - { text: 'Action', value: 'action', sortable: false } - ] - }, - isDeveloper () { - return isDeveloper(this.user) - }, - isResearcher () { - return isResearcher(this.user) - }, - config () { - if (this.token === null) { - return {} - } - return { - headers: { Authorization: `Bearer ${this.token}` } - } - }, - tokenMax () { - return this.$config.tokenMax - } - }, - mounted () { - this.loadTokens() - this.loadUsers() - }, - methods: { - submit () { - }, - copy (item) { - item.copied = true - navigator.clipboard.writeText(item.token) - }, - format (timestamp) { - return formatTimestamp(timestamp) - }, - tokenClass (token) { - return token.last_used ? '' : 'token-not_used' - }, - isDeveloper1 (user) { - return isDeveloper(user) - }, - closeDialog (event) { - if (event.success) { - this.loadUsers() - } - this.editRoleDialog = false - }, - modifyRoles (item) { - this.selectedUser = item - this.editRoleDialog = true - }, - async loadTokens () { - this.loading = true - try { - const res = await this.$axios.get('/api/user/token', this.config) - this.tokens = res.data.filter(t => !t.deleted) - console.debug('tokens', this.tokens) - } catch (err) { - this.$toast.error('Could not load tokens') - } - this.loading = false - }, - async mintToken () { - this.loading = true - try { - const res = await this.$axios.post('/api/user/token', {}, this.config) - const token = res.data - token.copied = false - console.debug('token', token) - this.tokens.push(token) - } catch (err) { - if (err.response.status === 417) { - this.$toast.error('Already exceeded the maximum allowed number of tokens!') - } else { - this.$toast.error('Could not create token') - } - } - this.loading = false - }, - formatRole (role) { - if (role === null) { - return null - } - const arr = this.roles.filter(r => r.code === role) - return arr.length > 0 ? arr[0].text : null - }, - async 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) { - const { message } = error.response - this.$toast.error('Failed to load users: ' + message) - console.error('Failed to load users', error) - } - this.loadingUsers = false - }, - async revokeToken (id) { - this.loading = true - try { - await this.$axios.delete(`/api/user/token/${id}`, this.config) - await this.loadTokens() - } catch (err) { - this.$toast.error('Could not delete token') - } - this.loading = false - } - } -} -</script> -<style> -.token-not_used { - opacity: 0.4; -} -</style> diff --git a/fda-ui/plugins/axios.js b/fda-ui/plugins/axios.js index 5de5da3b202469b256c7ab811d438e21f170df42..bdfd8d1aca860e614a2d7b5fa5504104451a990b 100644 --- a/fda-ui/plugins/axios.js +++ b/fda-ui/plugins/axios.js @@ -3,6 +3,7 @@ import store from '@/store' import api from '@/api' import AuthenticationService from '@/api/authentication.service' import jwtDecode from 'jwt-decode' +import VueAxios from 'vue-axios' api.interceptors.request.use((config) => { const token = store().state.token @@ -30,4 +31,4 @@ api.interceptors.request.use((config) => { return config }) -Vue.use(api) +Vue.use(VueAxios, api) diff --git a/fda-ui/yarn.lock b/fda-ui/yarn.lock index bff2f60529e64d54be07f685b751033d20e69b24..2a71a6d3aefb0114bc97aa58dc855a878efd730b 100644 --- a/fda-ui/yarn.lock +++ b/fda-ui/yarn.lock @@ -12235,6 +12235,11 @@ vm-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== +vue-axios@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/vue-axios/-/vue-axios-3.5.2.tgz#28637524cca550a9e97197e85a41930ec63604d5" + integrity sha512-GP+dct7UlAWkl1qoP3ppw0z6jcSua5/IrMpjB5O8bh089iIiJ+hdxPYH2NPEpajlYgkW5EVMP95ttXWdas1O0g== + vue-chartjs@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/vue-chartjs/-/vue-chartjs-4.1.1.tgz#b1ffc2845e09d14cb5255305b11bd3e8df8058ab"