From e15697267a2217e8dbe4730d50566f2566f89540 Mon Sep 17 00:00:00 2001 From: Martin Weise <martin.weise@tuwien.ac.at> Date: Thu, 18 May 2023 17:49:01 +0200 Subject: [PATCH] WIP --- dbrepo-authentication-service/Dockerfile | 3 +- .../dbrepo-realm.json | 103 ++++++-- .../generate-keystore.sh | 2 + dbrepo-authentication-service/server.keystore | Bin 0 -> 2776 bytes .../maintenance/BannerMessageBriefDto.java | 33 +++ .../maintenance/BannerMessageCreateDto.java | 44 ++++ .../api/maintenance/BannerMessageDto.java | 47 ++++ .../api/maintenance/BannerMessageTypeDto.java | 28 +++ .../maintenance/BannerMessageUpdateDto.java | 44 ++++ .../entities/maintenance/BannerMessage.java | 43 ++++ .../maintenance/BannerMessageType.java | 13 + dbrepo-metadata-db/setup-schema.sql | 12 + dbrepo-metadata-service/pom.xml | 6 + .../at/tuwien/config/WebSecurityConfig.java | 1 - .../at/tuwien/config/WebSecurityConfig.java | 1 - dbrepo-ui/api/metadata.service.js | 104 ++++++++ dbrepo-ui/components/DBToolbar.vue | 5 +- dbrepo-ui/components/DatabaseList.vue | 5 +- dbrepo-ui/components/QueryList.vue | 28 +-- dbrepo-ui/components/TableToolbar.vue | 8 +- dbrepo-ui/components/UserToolbar.vue | 26 ++ .../dialogs/EditMaintenanceMessage.vue | 228 ++++++++++++++++++ dbrepo-ui/layouts/default.vue | 47 ++-- dbrepo-ui/nuxt.config.js | 3 +- .../_database_id/query/_query_id/index.vue | 6 +- .../_database_id/table/_table_id/data.vue | 32 +-- .../_database_id/table/_table_id/info.vue | 13 - dbrepo-ui/pages/user/authentication.vue | 1 - dbrepo-ui/pages/user/developer.vue | 133 ++++++++++ dbrepo-ui/store/index.js | 16 +- dbrepo-ui/utils/index.js | 22 +- .../tuwien/endpoint/MaintenanceEndpoint.java | 161 +++++++++++++ .../at/tuwien/config/WebSecurityConfig.java | 1 + .../BannerMessageNotFoundException.java | 20 ++ .../at/tuwien/mapper/BannerMessageMapper.java | 24 ++ .../jpa/BannerMessageRepository.java | 14 ++ .../tuwien/service/BannerMessageService.java | 22 ++ .../impl/BannerMessageServiceImpl.java | 78 ++++++ dbrepo.conf | 9 + 39 files changed, 1277 insertions(+), 109 deletions(-) create mode 100644 dbrepo-authentication-service/generate-keystore.sh create mode 100644 dbrepo-authentication-service/server.keystore create mode 100644 dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageBriefDto.java create mode 100644 dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageCreateDto.java create mode 100644 dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageDto.java create mode 100644 dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageTypeDto.java create mode 100644 dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageUpdateDto.java create mode 100644 dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/maintenance/BannerMessage.java create mode 100644 dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/maintenance/BannerMessageType.java create mode 100644 dbrepo-ui/api/metadata.service.js create mode 100644 dbrepo-ui/components/dialogs/EditMaintenanceMessage.vue create mode 100644 dbrepo-ui/pages/user/developer.vue create mode 100644 dbrepo-user-service/rest-service/src/main/java/at/tuwien/endpoint/MaintenanceEndpoint.java create mode 100644 dbrepo-user-service/services/src/main/java/at/tuwien/exception/BannerMessageNotFoundException.java create mode 100644 dbrepo-user-service/services/src/main/java/at/tuwien/mapper/BannerMessageMapper.java create mode 100644 dbrepo-user-service/services/src/main/java/at/tuwien/repository/jpa/BannerMessageRepository.java create mode 100644 dbrepo-user-service/services/src/main/java/at/tuwien/service/BannerMessageService.java create mode 100644 dbrepo-user-service/services/src/main/java/at/tuwien/service/impl/BannerMessageServiceImpl.java diff --git a/dbrepo-authentication-service/Dockerfile b/dbrepo-authentication-service/Dockerfile index d5aae51744..a44ea5df15 100644 --- a/dbrepo-authentication-service/Dockerfile +++ b/dbrepo-authentication-service/Dockerfile @@ -5,13 +5,14 @@ MAINTAINER Martin Weise <martin.weise@tuwien.ac.at> # Enable health and metrics support ENV KC_HEALTH_ENABLED=true ENV KC_METRICS_ENABLED=true -ENV KC_FEATURES=update-email # Configure a database vendor ENV KC_DB=mariadb WORKDIR /opt/keycloak +COPY ./server.keystore ./conf/server.keystore + RUN /opt/keycloak/bin/kc.sh build ###### SECOND STAGE ###### diff --git a/dbrepo-authentication-service/dbrepo-realm.json b/dbrepo-authentication-service/dbrepo-realm.json index e2fc0ac590..040ad7b261 100644 --- a/dbrepo-authentication-service/dbrepo-realm.json +++ b/dbrepo-authentication-service/dbrepo-realm.json @@ -55,6 +55,17 @@ "clientRole" : false, "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", "attributes" : { } + }, { + "id" : "9bb4a8dc-28e0-4645-b62f-cc94425f0cb0", + "name" : "default-maintenance-handling", + "description" : "${default-maintenance-handling}", + "composite" : true, + "composites" : { + "realm" : [ "create-maintenance-message", "find-maintenance-message", "update-maintenance-message", "delete-maintenance-message", "list-maintenance-messages" ] + }, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } }, { "id" : "5136d7a3-e3f0-4585-bacd-15cb8a56095c", "name" : "escalated-container-handling", @@ -198,6 +209,14 @@ "clientRole" : false, "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", "attributes" : { } + }, { + "id" : "f4116230-8642-4bb7-bbc8-db9c5c07b558", + "name" : "create-maintenance-message", + "description" : "${create-maintenance-message}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } }, { "id" : "973f0999-cc70-4b28-9f43-979c470bea8e", "name" : "default-data-steward-roles", @@ -249,6 +268,14 @@ "clientRole" : false, "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", "attributes" : { } + }, { + "id" : "272a79a7-e282-4261-8f7d-5d5d1364243a", + "name" : "update-maintenance-message", + "description" : "${update-maintenance-message}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } }, { "id" : "64c16bfb-2015-48ad-a23f-637ff24419cb", "name" : "default-query-handling", @@ -268,6 +295,14 @@ "clientRole" : false, "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", "attributes" : { } + }, { + "id" : "c047d521-cec3-4444-86c4-aef098489b7b", + "name" : "delete-maintenance-message", + "description" : "${delete-maintenance-message}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } }, { "id" : "e14ab76b-1c24-484d-ae2d-478b8457edea", "name" : "list-licenses", @@ -435,7 +470,7 @@ "description" : "${default-developer-roles}", "composite" : true, "composites" : { - "realm" : [ "escalated-query-handling", "default-table-handling", "escalated-database-handling", "default-container-handling", "default-query-handling", "default-user-handling", "default-database-handling", "escalated-container-handling", "escalated-table-handling", "default-identifier-handling" ] + "realm" : [ "escalated-query-handling", "default-table-handling", "escalated-database-handling", "default-container-handling", "default-query-handling", "default-user-handling", "default-database-handling", "default-maintenance-handling", "escalated-container-handling", "escalated-table-handling", "default-identifier-handling" ] }, "clientRole" : false, "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", @@ -504,6 +539,14 @@ "clientRole" : false, "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", "attributes" : { } + }, { + "id" : "d6e38368-b40f-423b-82e4-e8aa595237c9", + "name" : "find-maintenance-message", + "description" : "${find-maintenance-message}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } }, { "id" : "fd1cc463-3e67-49d9-81b8-2cd90c1daa9c", "name" : "check-database-access", @@ -539,6 +582,14 @@ "clientRole" : false, "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", "attributes" : { } + }, { + "id" : "09f7bdb0-296f-46c8-a3a3-8f9254fb17e4", + "name" : "list-maintenance-messages", + "description" : "${list-maintenance-messages}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } }, { "id" : "fe3bc45c-61c2-4ece-bcaf-d410dc7de501", "name" : "update-database-access", @@ -909,7 +960,7 @@ "otpPolicyLookAheadWindow" : 1, "otpPolicyPeriod" : 30, "otpPolicyCodeReusable" : false, - "otpSupportedApplications" : [ "totpAppFreeOTPName", "totpAppMicrosoftAuthenticatorName", "totpAppGoogleName" ], + "otpSupportedApplications" : [ "totpAppMicrosoftAuthenticatorName", "totpAppGoogleName", "totpAppFreeOTPName" ], "webAuthnPolicyRpEntityName" : "keycloak", "webAuthnPolicySignatureAlgorithms" : [ "ES256" ], "webAuthnPolicyRpId" : "", @@ -1932,7 +1983,7 @@ "subType" : "authenticated", "subComponents" : { }, "config" : { - "allowed-protocol-mapper-types" : [ "oidc-usermodel-attribute-mapper", "oidc-full-name-mapper", "oidc-usermodel-property-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-user-property-mapper", "saml-role-list-mapper", "oidc-address-mapper", "saml-user-attribute-mapper" ] + "allowed-protocol-mapper-types" : [ "saml-user-attribute-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper", "oidc-address-mapper", "oidc-usermodel-property-mapper", "saml-role-list-mapper", "oidc-full-name-mapper", "oidc-sha256-pairwise-sub-mapper" ] } }, { "id" : "3ab11d74-5e76-408a-b85a-26bf8950f979", @@ -1941,7 +1992,7 @@ "subType" : "anonymous", "subComponents" : { }, "config" : { - "allowed-protocol-mapper-types" : [ "saml-user-property-mapper", "oidc-usermodel-attribute-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper", "oidc-address-mapper", "oidc-usermodel-property-mapper", "saml-user-attribute-mapper", "oidc-full-name-mapper" ] + "allowed-protocol-mapper-types" : [ "oidc-usermodel-attribute-mapper", "oidc-usermodel-property-mapper", "saml-user-property-mapper", "oidc-full-name-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-address-mapper", "saml-user-attribute-mapper", "saml-role-list-mapper" ] } } ], "org.keycloak.keys.KeyProvider" : [ { @@ -1993,7 +2044,7 @@ "internationalizationEnabled" : false, "supportedLocales" : [ ], "authenticationFlows" : [ { - "id" : "7e7d6810-5b6c-4ec6-865c-5f0b62ec56d7", + "id" : "a50c392e-a870-484c-b7c4-7c85e886ee46", "alias" : "Account verification options", "description" : "Method with which to verity the existing account", "providerId" : "basic-flow", @@ -2015,7 +2066,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "6d972ab3-0618-4971-b44a-0fc0d11c7280", + "id" : "3f384243-bf93-4535-8811-b23526f12963", "alias" : "Authentication Options", "description" : "Authentication options.", "providerId" : "basic-flow", @@ -2044,7 +2095,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "821a14e0-ef26-4b07-b716-fa34393eda56", + "id" : "478c7d4f-86f2-42eb-91db-4bc03e20e416", "alias" : "Browser - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -2066,7 +2117,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "e70eadbd-4c39-4cfd-86ac-e50acc753b1b", + "id" : "cca43646-a74e-4439-b029-3c0a11cd3693", "alias" : "Direct Grant - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -2088,7 +2139,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "4e35af97-acf4-4ca8-bc81-0477c1adfb6d", + "id" : "815189fe-ef5f-4865-be03-1e4b5dd9eafe", "alias" : "First broker login - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -2110,7 +2161,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "2e0bd063-274a-4aab-a5f0-038a0bca5b98", + "id" : "b72709d3-246d-4057-a82f-1a6e721bdd0c", "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", @@ -2132,7 +2183,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "6a20fab2-44bb-4451-b29a-6fb7e14a52ce", + "id" : "191c4793-7fdd-449a-8218-70d047d882b7", "alias" : "Reset - Conditional OTP", "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", "providerId" : "basic-flow", @@ -2154,7 +2205,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "159d7398-74a7-4f60-a3fd-eb2df46f5ce7", + "id" : "afd9a108-753a-4012-a65f-69d149d8313a", "alias" : "User creation or linking", "description" : "Flow for the existing/non-existing user alternatives", "providerId" : "basic-flow", @@ -2177,7 +2228,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "85a66c55-4665-4ba0-bec9-7254eb8e5895", + "id" : "95c1347f-8248-4506-8ac7-77da64331658", "alias" : "Verify Existing Account by Re-authentication", "description" : "Reauthentication of existing account", "providerId" : "basic-flow", @@ -2199,7 +2250,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "c002e6da-2397-4fae-8d48-1eec3719ca15", + "id" : "00aa962b-69e7-4032-99d6-33d2e15689bf", "alias" : "browser", "description" : "browser based authentication", "providerId" : "basic-flow", @@ -2235,7 +2286,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "a03631cf-2fea-4a12-a35c-8137023503bd", + "id" : "b7efe33a-b00a-4830-b362-739101e1a437", "alias" : "clients", "description" : "Base authentication for clients", "providerId" : "client-flow", @@ -2271,7 +2322,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "a89940e4-bf4d-4a04-8fdf-dcf775336b20", + "id" : "39a89fb2-74b0-44bb-877c-a2f75044f5e1", "alias" : "direct grant", "description" : "OpenID Connect Resource Owner Grant", "providerId" : "basic-flow", @@ -2300,7 +2351,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "2dc2582b-be6f-4d9a-b545-b2c0e79a3581", + "id" : "c0b9af60-9bb2-42e2-815d-c03144b4773c", "alias" : "docker auth", "description" : "Used by Docker clients to authenticate against the IDP", "providerId" : "basic-flow", @@ -2315,7 +2366,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "09e56692-226f-4384-85e0-e33463cdb226", + "id" : "50e548b3-60a1-4511-838a-894462d7437a", "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", @@ -2338,7 +2389,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "1439c900-92e0-4230-a1a7-ae82c3b8ddc9", + "id" : "260eb8b4-8889-41e4-b3d4-ce8adf15474c", "alias" : "forms", "description" : "Username, password, otp and other auth forms.", "providerId" : "basic-flow", @@ -2360,7 +2411,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "4cc3bb1b-e85d-447e-b50e-1afbe107bafe", + "id" : "d7db5234-08e2-44ed-8708-5f4e906fe8a5", "alias" : "http challenge", "description" : "An authentication flow based on challenge-response HTTP Authentication Schemes", "providerId" : "basic-flow", @@ -2382,7 +2433,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "04c49d80-30e4-4a37-b1c7-4d18c1b6a7f1", + "id" : "73ada652-053b-42f3-be1f-af907cdd3151", "alias" : "registration", "description" : "registration flow", "providerId" : "basic-flow", @@ -2398,7 +2449,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "85abb75a-0774-4b2d-8a71-2a92b0cfb639", + "id" : "ef0d04cc-a30d-4cdc-8e62-26075e256f41", "alias" : "registration form", "description" : "registration form", "providerId" : "form-flow", @@ -2434,7 +2485,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "948f68c1-015b-4349-a56f-6ee177d558ce", + "id" : "c6db550e-4542-4474-8e3d-955fb50b1a05", "alias" : "reset credentials", "description" : "Reset credentials for a user if they forgot their password or something", "providerId" : "basic-flow", @@ -2470,7 +2521,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "6046d416-4a88-4af6-b440-9fbc87fba478", + "id" : "9ba81baf-f271-44df-b712-1a58af78c27c", "alias" : "saml ecp", "description" : "SAML ECP Profile Authentication Flow", "providerId" : "basic-flow", @@ -2486,13 +2537,13 @@ } ] } ], "authenticatorConfig" : [ { - "id" : "3c91aefc-127f-4722-8375-72e8434d6266", + "id" : "d70d482f-3f61-4514-918f-9634c6dc7200", "alias" : "create unique user config", "config" : { "require.password.update.after.registration" : "false" } }, { - "id" : "1041c583-4682-44a8-b61b-9712bd4987c4", + "id" : "b5e8a2b3-a407-42f4-bc5d-c82c2c77c8d1", "alias" : "review profile config", "config" : { "update.profile.on.first.login" : "missing" diff --git a/dbrepo-authentication-service/generate-keystore.sh b/dbrepo-authentication-service/generate-keystore.sh new file mode 100644 index 0000000000..8b68c44a1f --- /dev/null +++ b/dbrepo-authentication-service/generate-keystore.sh @@ -0,0 +1,2 @@ +#!/bin/bash +keytool -genkey -alias server -keyalg RSA -keypass password -storepass password -keystore server.keystore \ No newline at end of file diff --git a/dbrepo-authentication-service/server.keystore b/dbrepo-authentication-service/server.keystore new file mode 100644 index 0000000000000000000000000000000000000000..9dcd5051210b5bd2f945a8325610684c8e0029a8 GIT binary patch literal 2776 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 XGs=$kMvoZ+C%<97mSy`DR73#)a~}c> literal 0 HcmV?d00001 diff --git a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageBriefDto.java b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageBriefDto.java new file mode 100644 index 0000000000..a11c70f621 --- /dev/null +++ b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageBriefDto.java @@ -0,0 +1,33 @@ +package at.tuwien.api.maintenance; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.*; +import lombok.extern.jackson.Jacksonized; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Jacksonized +@ToString +public class BannerMessageBriefDto { + + @NotNull + private BannerMessageTypeDto type; + + @NotBlank + @Schema(example = "Maintenance starts on 8am on Monday") + private String message; + + @Schema(example = "https://example.com") + private String link; + + @JsonProperty("link_text") + @Schema(example = "More") + private String linkText; + +} diff --git a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageCreateDto.java b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageCreateDto.java new file mode 100644 index 0000000000..8bfe43b2b1 --- /dev/null +++ b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageCreateDto.java @@ -0,0 +1,44 @@ +package at.tuwien.api.maintenance; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.*; +import lombok.extern.jackson.Jacksonized; + +import java.time.Instant; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Jacksonized +@ToString +public class BannerMessageCreateDto { + + @NotNull + private BannerMessageTypeDto type; + + @NotBlank + @Schema(example = "Maintenance starts on 8am on Monday") + private String message; + + @Schema(example = "https://example.com") + private String link; + + @JsonProperty("link_text") + @Schema(example = "More") + private String linkText; + + @JsonProperty("display_start") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "UTC") + private Instant displayStart; + + @JsonProperty("display_end") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "UTC") + private Instant displayEnd; + +} diff --git a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageDto.java b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageDto.java new file mode 100644 index 0000000000..1b822b9ed4 --- /dev/null +++ b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageDto.java @@ -0,0 +1,47 @@ +package at.tuwien.api.maintenance; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.*; +import lombok.extern.jackson.Jacksonized; + +import java.time.Instant; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Jacksonized +@ToString +public class BannerMessageDto { + + @NotNull + private Long id; + + @NotNull + private BannerMessageTypeDto type; + + @NotBlank + @Schema(example = "Maintenance starts on 8am on Monday") + private String message; + + @Schema(example = "https://example.com") + private String link; + + @JsonProperty("link_text") + @Schema(example = "More") + private String linkText; + + @JsonProperty("display_start") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC") + private Instant displayStart; + + @JsonProperty("display_end") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC") + private Instant displayEnd; + +} diff --git a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageTypeDto.java b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageTypeDto.java new file mode 100644 index 0000000000..8a867f5ea4 --- /dev/null +++ b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageTypeDto.java @@ -0,0 +1,28 @@ +package at.tuwien.api.maintenance; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; + +@Getter +public enum BannerMessageTypeDto { + + @JsonProperty("error") + ERROR("error"), + + @JsonProperty("warning") + WARNING("warning"), + + @JsonProperty("info") + INFO("info"); + + private String name; + + BannerMessageTypeDto(String name) { + this.name = name; + } + + @Override + public String toString() { + return this.name; + } +} diff --git a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageUpdateDto.java b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageUpdateDto.java new file mode 100644 index 0000000000..26a2ac68ad --- /dev/null +++ b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/maintenance/BannerMessageUpdateDto.java @@ -0,0 +1,44 @@ +package at.tuwien.api.maintenance; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.*; +import lombok.extern.jackson.Jacksonized; + +import java.time.Instant; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Jacksonized +@ToString +public class BannerMessageUpdateDto { + + @NotNull + private BannerMessageTypeDto type; + + @NotBlank + @Schema(example = "Maintenance starts on 8am on Monday") + private String message; + + @Schema(example = "https://example.com") + private String link; + + @JsonProperty("link_text") + @Schema(example = "More") + private String linkText; + + @JsonProperty("display_start") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "UTC") + private Instant displayStart; + + @JsonProperty("display_end") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "UTC") + private Instant displayEnd; + +} diff --git a/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/maintenance/BannerMessage.java b/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/maintenance/BannerMessage.java new file mode 100644 index 0000000000..f3cf82d499 --- /dev/null +++ b/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/maintenance/BannerMessage.java @@ -0,0 +1,43 @@ +package at.tuwien.entities.maintenance; + +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.GenericGenerator; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.Instant; + +@Data +@Entity +@Builder +@ToString +@AllArgsConstructor +@NoArgsConstructor +@EntityListeners(AuditingEntityListener.class) +@Table(name = "mdb_banner_messages") +@NamedQueries({ + @NamedQuery(name = "BannerMessage.findByActive", query = "select m from BannerMessage m where (m.displayStart = null and m.displayEnd = null) or (m.displayStart = null and m.displayEnd >= NOW()) or (m.displayStart <= NOW() and m.displayEnd >= NOW()) or (m.displayStart <= NOW() and m.displayEnd = null)") +}) +public class BannerMessage { + + @Id + @EqualsAndHashCode.Include + @GeneratedValue(generator = "messages-sequence") + @GenericGenerator(name = "messages-sequence", strategy = "increment") + @Column(updatable = false, nullable = false) + private Long id; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, columnDefinition = "enum('ERROR','WARNING','INFO')") + private BannerMessageType type; + + @Column(nullable = false) + private String message; + + @Column(columnDefinition = "TIMESTAMP") + private Instant displayStart; + + @Column(columnDefinition = "TIMESTAMP") + private Instant displayEnd; + +} diff --git a/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/maintenance/BannerMessageType.java b/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/maintenance/BannerMessageType.java new file mode 100644 index 0000000000..8d17965f48 --- /dev/null +++ b/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/maintenance/BannerMessageType.java @@ -0,0 +1,13 @@ + +package at.tuwien.entities.maintenance; + +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public enum BannerMessageType { + WARNING, + ERROR, + INFO; +} \ No newline at end of file diff --git a/dbrepo-metadata-db/setup-schema.sql b/dbrepo-metadata-db/setup-schema.sql index eb7809d140..24798819e9 100644 --- a/dbrepo-metadata-db/setup-schema.sql +++ b/dbrepo-metadata-db/setup-schema.sql @@ -332,6 +332,18 @@ CREATE TABLE IF NOT EXISTS `fda`.`mdb_view` FOREIGN KEY (vdbid) REFERENCES mdb_databases (id) ) WITH SYSTEM VERSIONING; +CREATE TABLE IF NOT EXISTS `fda`.`mdb_banner_messages` +( + id bigint NOT NULL AUTO_INCREMENT, + type ENUM ('ERROR', 'WARNING', 'INFO') NOT NULL default 'INFO', + message TEXT NOT NULL, + link TEXT NULL, + link_text VARCHAR(255) NULL, + display_start timestamp NULL, + display_end timestamp NULL, + PRIMARY KEY (id) +) WITH SYSTEM VERSIONING; + CREATE TABLE IF NOT EXISTS `fda`.`mdb_view_columns` ( id BIGINT NOT NULL AUTO_INCREMENT, diff --git a/dbrepo-metadata-service/pom.xml b/dbrepo-metadata-service/pom.xml index 6c3514de1e..8dfab218ee 100644 --- a/dbrepo-metadata-service/pom.xml +++ b/dbrepo-metadata-service/pom.xml @@ -114,6 +114,12 @@ <version>${project.version}</version> <scope>compile</scope> </dependency> + <!-- Authentication --> + <dependency> + <groupId>com.auth0</groupId> + <artifactId>java-jwt</artifactId> + <version>${jwt.version}</version> + </dependency> <!-- IDE --> <dependency> <groupId>org.projectlombok</groupId> diff --git a/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java b/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java index 13d92092fc..7bec3fbf94 100644 --- a/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java +++ b/dbrepo-metadata-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java @@ -2,7 +2,6 @@ package at.tuwien.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; diff --git a/dbrepo-query-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java b/dbrepo-query-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java index 582fe83630..1c36be92a4 100644 --- a/dbrepo-query-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java +++ b/dbrepo-query-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java @@ -5,7 +5,6 @@ import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; import io.swagger.v3.oas.annotations.security.SecurityScheme; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; diff --git a/dbrepo-ui/api/metadata.service.js b/dbrepo-ui/api/metadata.service.js new file mode 100644 index 0000000000..9c02ea9247 --- /dev/null +++ b/dbrepo-ui/api/metadata.service.js @@ -0,0 +1,104 @@ +import Vue from 'vue' +import api from '@/api' + +class MetadataService { + findAllMessages () { + return new Promise((resolve, reject) => { + api.get('/api/maintenance/message', { headers: { Accept: 'application/json' } }) + .then((response) => { + const messages = response.data + console.debug('response messages', messages) + resolve(messages) + }) + .catch((error) => { + const { code, message } = error + console.error('Failed to load messages', error) + Vue.$toast.error(`[${code}] Failed to load messages: ${message}`) + reject(error) + }) + }) + } + + createMessage (data) { + return new Promise((resolve, reject) => { + api.post('/api/maintenance/message', data, { headers: { Accept: 'application/json' } }) + .then((response) => { + const messages = response.data + console.debug('response message', messages) + resolve(messages) + }) + .catch((error) => { + const { code, message } = error + console.error('Failed to create message', error) + Vue.$toast.error(`[${code}] Failed to create message: ${message}`) + reject(error) + }) + }) + } + + findMessage (id) { + return new Promise((resolve, reject) => { + api.get(`/api/maintenance/message/${id}`, { headers: { Accept: 'application/json' } }) + .then((response) => { + const messages = response.data + console.debug('response message', messages) + resolve(messages) + }) + .catch((error) => { + const { code, message } = error + console.error('Failed to find message', error) + Vue.$toast.error(`[${code}] Failed to find message: ${message}`) + reject(error) + }) + }) + } + + updateMessage (id, data) { + return new Promise((resolve, reject) => { + api.put(`/api/maintenance/message/${id}`, data, { headers: { Accept: 'application/json' } }) + .then((response) => { + const messages = response.data + console.debug('response message', messages) + resolve(messages) + }) + .catch((error) => { + const { code, message } = error + console.error('Failed to update message', error) + Vue.$toast.error(`[${code}] Failed to update message: ${message}`) + reject(error) + }) + }) + } + + deleteMessage (id) { + return new Promise((resolve, reject) => { + api.delete(`/api/maintenance/message/${id}`, { headers: { Accept: 'application/json' } }) + .then(() => resolve()) + .catch((error) => { + const { code, message } = error + console.error('Failed to delete message', error) + Vue.$toast.error(`[${code}] Failed to delete message: ${message}`) + reject(error) + }) + }) + } + + findActiveMessages () { + return new Promise((resolve, reject) => { + api.get('/api/maintenance/message/active', { headers: { Accept: 'application/json' } }) + .then((response) => { + const messages = response.data + console.debug('response messages', messages) + resolve(messages) + }) + .catch((error) => { + const { code, message } = error + console.error('Failed to load active messages', error) + Vue.$toast.error(`[${code}] Failed to load active messages: ${message}`) + reject(error) + }) + }) + } +} + +export default new MetadataService() diff --git a/dbrepo-ui/components/DBToolbar.vue b/dbrepo-ui/components/DBToolbar.vue index b8590bc855..c899ab03eb 100644 --- a/dbrepo-ui/components/DBToolbar.vue +++ b/dbrepo-ui/components/DBToolbar.vue @@ -55,7 +55,7 @@ <v-tab :to="`/container/${$route.params.container_id}/database/${$route.params.database_id}/table`"> {{ $t('databases.toolbar.tables', { name: 'vue-i18n' }) }} </v-tab> - <v-tab v-if="hasReadAccess" :to="`/container/${$route.params.container_id}/database/${$route.params.database_id}/query`"> + <v-tab v-if="canViewQueries" :to="`/container/${$route.params.container_id}/database/${$route.params.database_id}/query`"> {{ $t('databases.toolbar.subsets', { name: 'vue-i18n' }) }} </v-tab> <v-tab :to="`/container/${$route.params.container_id}/database/${$route.params.database_id}/view`"> @@ -121,6 +121,9 @@ export default { } return this.roles.includes('execute-query') }, + canViewQueries () { + return this.database.is_public || this.hasReadAccess + }, canCreateView () { if (!this.user || !this.isOwner) { return false diff --git a/dbrepo-ui/components/DatabaseList.vue b/dbrepo-ui/components/DatabaseList.vue index fc044ee2ff..bd1c650460 100644 --- a/dbrepo-ui/components/DatabaseList.vue +++ b/dbrepo-ui/components/DatabaseList.vue @@ -1,13 +1,16 @@ <template> <div> <v-progress-linear v-if="loadingContainers || loadingDatabases" :indeterminate="!error" /> + <v-card v-if="!$vuetify.theme.dark && containers.list > 0" flat tile> + <v-divider class="mx-4" /> + </v-card> <v-card v-for="(container, idx) in containers" :key="idx" :to="link(container)" flat tile> - <v-divider v-if="!$vuetify.theme.dark" class="mx-4" /> + <v-divider v-if="idx !== 0" 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/dbrepo-ui/components/QueryList.vue b/dbrepo-ui/components/QueryList.vue index 1bead29f29..4485e34945 100644 --- a/dbrepo-ui/components/QueryList.vue +++ b/dbrepo-ui/components/QueryList.vue @@ -1,11 +1,16 @@ <template> <div> - <v-progress-linear v-if="loadingIdentifiers || loadingQueries || error" :color="loadingColor" :value="loadProgress" /> - <v-card v-if="!(loadingIdentifiers || loadingQueries) && queries && queries.length === 0" flat> + <v-progress-linear v-if="loadingIdentifiers || loadingQueries || error" :color="loadingColor" :indeterminate="!error" /> + <v-card v-if="!error && !(loadingIdentifiers || loadingQueries) && queries && queries.length === 0" flat tile> <v-card-text> (no subsets) </v-card-text> </v-card> + <v-card v-if="error" flat tile> + <v-card-text> + Failed to load queries: database is not reachable + </v-card-text> + </v-card> <v-tabs-items> <div v-if="!loadingQueries && !error"> <div v-for="(item,i) in queries" :key="i"> @@ -77,7 +82,6 @@ export default { return { loadingQueries: false, loadingIdentifiers: false, - loadProgress: 0, error: false, queries: [], identifiers: [] @@ -129,7 +133,6 @@ export default { mounted () { this.loadQueries() this.loadIdentifiers() - this.simulateProgress() }, methods: { loadIdentifiers () { @@ -148,6 +151,9 @@ export default { .then((queries) => { this.queries = queries }) + .catch(() => { + this.error = true + }) .finally(() => { this.loadingQueries = false }) @@ -181,20 +187,6 @@ export default { return 'primary--text' } return null - }, - simulateProgress () { - if (this.loadProgress !== 0) { - return - } - const timeout = 30 * 1000 /* ms */ - const ticks = 100 /* ms */ - let i = 0 - setInterval(() => { - if (i++ >= timeout && !this.error) { - return - } - this.loadProgress = ((i * 100) / timeout) * 100 - }, ticks) } } } diff --git a/dbrepo-ui/components/TableToolbar.vue b/dbrepo-ui/components/TableToolbar.vue index b3f22595c9..69351b3121 100644 --- a/dbrepo-ui/components/TableToolbar.vue +++ b/dbrepo-ui/components/TableToolbar.vue @@ -108,12 +108,18 @@ export default { return UserUtils.hasWriteAccess(this.access) && this.roles.includes('insert-table-data') }, canEditTuple () { + if (this.selection === null || this.selection.length !== 1) { + return false + } if (!this.roles || !this.isDataTab) { return false } return UserUtils.hasWriteAccess(this.access) && this.roles.includes('insert-table-data') }, canDeleteTuple () { + if (this.selection === null || this.selection.length < 1) { + return false + } if (!this.roles || !this.isDataTab) { return false } @@ -199,7 +205,7 @@ export default { }) TableService.deleteTuple(this.$route.params.container_id, this.$route.params.database_id, this.$route.params.table_id, { keys: constraints }) .then(() => { - this.$toast.success(`Deleted ${this.selection.length} rows(s)`) + this.$toast.success(`Deleted ${this.selection.length} row${this.selection.length !== 1 ? 's' : ''}`) this.$emit('modified', { success: true, action: 'delete' }) }) } diff --git a/dbrepo-ui/components/UserToolbar.vue b/dbrepo-ui/components/UserToolbar.vue index 462b438496..878a480859 100644 --- a/dbrepo-ui/components/UserToolbar.vue +++ b/dbrepo-ui/components/UserToolbar.vue @@ -12,6 +12,9 @@ <v-tab to="/user/authentication"> Authentication </v-tab> + <v-tab v-if="canHandleMessages" to="/user/developer"> + Developer + </v-tab> </v-tabs> </div> </template> @@ -23,6 +26,29 @@ export default { return { tab: null } + }, + computed: { + user () { + return this.$store.state.user + }, + roles () { + return this.$store.state.roles + }, + canCreateMessage () { + if (!this.roles) { + return false + } + return this.roles.includes('create-maintenance-message') + }, + canModifyMessage () { + if (!this.roles) { + return false + } + return this.roles.includes('modify-maintenance-message') + }, + canHandleMessages () { + return this.canCreateMessage || this.canModifyMessage + } } } </script> diff --git a/dbrepo-ui/components/dialogs/EditMaintenanceMessage.vue b/dbrepo-ui/components/dialogs/EditMaintenanceMessage.vue new file mode 100644 index 0000000000..4215123895 --- /dev/null +++ b/dbrepo-ui/components/dialogs/EditMaintenanceMessage.vue @@ -0,0 +1,228 @@ +<template> + <div> + <v-form ref="form" v-model="valid" autocomplete="off" @submit.prevent="submit"> + <v-card> + <v-progress-linear v-if="loading" :color="loadingColor" :indeterminate="!error" /> + <v-card-title v-text="title" /> + <v-card-text> + <v-row dense> + <v-col> + <v-select + v-model="localMessage.type" + :items="types" + item-text="name" + item-value="value" + :rules="[v => !!v || $t('Required')]" + required + label="Type *" /> + </v-col> + </v-row> + <v-row dense> + <v-col> + <v-text-field + v-model="localMessage.message" + :rules="[v => !!v || $t('Required')]" + required + label="Message *" /> + </v-col> + </v-row> + <v-row dense> + <v-col cols="6"> + <v-text-field + v-model="localMessage.display_start" + clearable + label="Start timestamp" /> + </v-col> + <v-col cols="6"> + <v-text-field + v-model="localMessage.display_end" + clearable + label="End timestamp" /> + </v-col> + </v-row> + <v-row dense> + <v-col> + <v-text-field + v-model="localMessage.link" + clearable + label="Link" /> + </v-col> + </v-row> + <v-row dense> + <v-col> + <v-text-field + v-model="localMessage.link_text" + clearable + label="Link Text" /> + </v-col> + </v-row> + </v-card-text> + <v-card-actions> + <v-btn + v-if="isModification" + class="ml-2" + color="error" + @click="deleteMessage"> + Delete + </v-btn> + <v-spacer /> + <v-btn + class="mb-2" + @click="cancel"> + Cancel + </v-btn> + <v-btn + id="database" + class="mb-2 ml-3 mr-2" + :disabled="!valid || loading" + :color="buttonColor" + type="submit" + :loading="loading" + @click="submitButton"> + {{ buttonText }} + </v-btn> + </v-card-actions> + </v-card> + </v-form> + </div> +</template> + +<script> +import MetadataService from '@/api/metadata.service' + +export default { + props: { + id: { + type: Number, + default () { + return null + } + } + }, + data () { + return { + valid: false, + loading: false, + error: false, + types: [ + { name: 'Error', value: 'error' }, + { name: 'Warning', value: 'warning' }, + { name: 'Info', value: 'info' } + ], + localMessage: { + type: null, + message: null, + display_start: null, + display_end: null, + link: null, + link_text: null + }, + modify: { + username: null, + type: null + } + } + }, + computed: { + loadingColor () { + return this.error ? 'red lighten-2' : 'primary' + }, + database () { + return this.$store.state.database + }, + title () { + return (!this.isModification ? 'Create' : 'Modify') + ' maintenance message' + }, + buttonColor () { + if (this.modify.type && this.modify.type === 'revoke') { + return 'error' + } + return 'secondary' + }, + isModification () { + return this.id !== null + }, + buttonText () { + return (this.isModification ? 'Modify' : 'Create') + ' message' + } + }, + watch: { + id () { + this.init() + } + }, + mounted () { + this.init() + }, + methods: { + submit () { + this.$refs.form.validate() + }, + cancel () { + this.$emit('close-dialog', { success: false }) + }, + init () { + if (!this.id) { + this.localMessage = { + type: null, + message: null, + display_start: null, + display_end: null, + link: null, + link_text: null + } + } else { + this.loadMessage(this.id) + } + }, + loadMessage (id) { + MetadataService.findMessage(id) + .then((message) => { + this.localMessage = message + }) + }, + submitButton () { + if (this.isModification) { + this.updateMessage() + } else { + this.createMessage() + } + }, + createMessage () { + this.loading = true + MetadataService.createMessage(this.localMessage) + .then(() => { + this.$emit('close-dialog', { success: true }) + this.$emit('reload-messages', { success: true }) + }) + .finally(() => { + this.loading = false + }) + }, + updateMessage () { + this.loading = true + const payload = Object.assign({}, this.localMessage) + delete payload.id + MetadataService.updateMessage(this.localMessage.id, payload) + .then(() => { + this.$emit('close-dialog', { success: true }) + this.$emit('reload-messages', { success: true }) + }) + .finally(() => { + this.loading = false + }) + }, + deleteMessage () { + this.loading = true + MetadataService.deleteMessage(this.localMessage.id) + .then(() => { + this.$emit('close-dialog', { success: true }) + this.$emit('reload-messages', { success: true }) + }) + .finally(() => { + this.loading = false + }) + } + } +} +</script> diff --git a/dbrepo-ui/layouts/default.vue b/dbrepo-ui/layouts/default.vue index 8f9358cb23..061bfc620d 100644 --- a/dbrepo-ui/layouts/default.vue +++ b/dbrepo-ui/layouts/default.vue @@ -39,6 +39,19 @@ </v-list-item-content> </v-list-item> </v-list> + <div id="messages"> + <v-alert + v-for="(message, idx) in messages" + :key="idx" + class="banner" + border="left" + tile + :type="message.type"> + {{ message.message }} + <span v-if="message.link">‐</span> + <a v-if="message.link" :href="message.link">{{ message.link_text ? message.link_text : message.link }}</a> + </v-alert> + </div> </v-navigation-drawer> <v-form ref="form" @submit.prevent="submit"> <v-app-bar fixed app> @@ -70,8 +83,8 @@ {{ $t('layout.signup', { name: 'vue-i18n' }) }} </v-btn> </div> - <div> - <v-btn v-if="user" to="/user" plain> + <div v-if="user"> + <v-btn to="/user" plain> {{ user.username }} </v-btn> <v-menu bottom offset-y left> @@ -105,19 +118,6 @@ <nuxt /> </v-container> </v-main> - <v-footer - v-if="sandbox" - padless> - <v-card - flat - tile - width="100%" - class="banner text-center"> - <v-card-text class="black--text"> - This is a <strong>TEST</strong> environment, do not use production/confidential data! — <a href="//github.com/fair-data-austria/dbrepo/issues/new" class="black--text">Report a bug</a> - </v-card-text> - </v-card> - </v-footer> </v-app> </template> @@ -160,6 +160,9 @@ export default { locale () { return this.$store.state.locale }, + messages () { + return this.$store.state.messages + }, table () { return this.$store.state.table }, @@ -215,9 +218,7 @@ export default { } }, mounted () { - if (this.refreshToken) { - AuthenticationService.authenticateToken(this.refreshToken) - } + this.$store.dispatch('reloadMessages') if (this.locale) { this.$i18n.locale = this.locale } @@ -302,6 +303,16 @@ export default { } </script> <style> +#messages { + position: absolute; + left: 0; + right: 0; + bottom: 0; +} +.banner { + width: 100%; + margin: 8px 0 0 0; +} .search-result-title, .search-result-subtitle { overflow: hidden; diff --git a/dbrepo-ui/nuxt.config.js b/dbrepo-ui/nuxt.config.js index 9c06aff0c1..9db2e6dc80 100644 --- a/dbrepo-ui/nuxt.config.js +++ b/dbrepo-ui/nuxt.config.js @@ -110,11 +110,10 @@ export default { primary: colors.blue.darken2, accent: colors.amber.darken3, secondary: colors.blueGrey.base, - info: colors.amber.lighten1, + info: colors.blue.lighten2, code: colors.grey.lighten4, warning: colors.orange.lighten2, error: colors.red.base /* is used by forms */, - banner: colors.red.lighten2, success: colors.teal.base }, dark: { diff --git a/dbrepo-ui/pages/container/_container_id/database/_database_id/query/_query_id/index.vue b/dbrepo-ui/pages/container/_container_id/database/_database_id/query/_query_id/index.vue index dcbe339660..eddbcca9f4 100644 --- a/dbrepo-ui/pages/container/_container_id/database/_database_id/query/_query_id/index.vue +++ b/dbrepo-ui/pages/container/_container_id/database/_database_id/query/_query_id/index.vue @@ -25,7 +25,7 @@ </DownloadButton> </v-toolbar-title> </v-toolbar> - <v-card v-if="query && query.identifier" flat tile> + <v-card v-if="query.identifier" flat tile> <v-card-title>Identifier</v-card-title> <v-card-text> <v-list dense> @@ -38,7 +38,7 @@ Persistent Identifier </v-list-item-title> <v-list-item-content> - <Banner :identifier="query.identifier" /> + <Banner v-if="canPersistQuery" :identifier="query.identifier" /> </v-list-item-content> <v-list-item-title class="mt-2"> Title @@ -306,7 +306,7 @@ export default { return this.query.result_hash }, canPersistQuery () { - if (!this.query || this.query.is_persisted) { + if (this.loadingQuery || !this.query || this.query.is_persisted) { return false } return UserUtils.hasReadAccess(this.access) diff --git a/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/data.vue b/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/data.vue index dfd27fc763..925403b70d 100644 --- a/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/data.vue +++ b/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/data.vue @@ -23,8 +23,14 @@ </v-toolbar-title> </v-toolbar> <v-card tile> - <v-progress-linear v-if="loadingData > 0 || error" :value="loadProgress" :color="error ? 'error' : 'primary'" /> + <v-progress-linear v-if="loadingData > 0 || error" :indeterminate="!error" :color="loadingColor" /> + <v-card v-if="error" flat tile> + <v-card-text> + Failed to load table data: database is not reachable + </v-card-text> + </v-card> <v-data-table + v-if="!error" :headers="headers" :items="rows" :options.sync="options" @@ -53,7 +59,6 @@ export default { return { loading: true, loadingData: 0, - loadProgress: 0, editTupleDialog: false, total: -1, footerProps: { @@ -67,7 +72,8 @@ export default { version: null, lastReload: new Date(), tab: null, - error: false, // XXX: `error` is never changed + edit: false, + error: false, options: { page: 1, itemsPerPage: 10 @@ -85,7 +91,7 @@ export default { }, computed: { loadingColor () { - return this.error ? 'red lighten-2' : 'primary' + return this.error ? 'error' : 'primary' }, token () { return this.$store.state.token @@ -168,7 +174,6 @@ export default { }, mounted () { this.reload() - this.simulateProgress() this.loadProperties() }, methods: { @@ -277,6 +282,9 @@ export default { return row }) }) + .catch(() => { + this.error = true + }) .finally(() => { this.loadingData-- }) @@ -290,20 +298,6 @@ export default { .finally(() => { this.loadingData-- }) - }, - simulateProgress () { - if (this.loadProgress !== 0) { - return - } - const timeout = 30 * 1000 /* ms */ - const ticks = 100 /* ms */ - let i = 0 - setInterval(() => { - if (i++ >= timeout && !this.error) { - return - } - this.loadProgress = ((i * 100) / timeout) * 100 - }, ticks) } } } diff --git a/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/info.vue b/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/info.vue index 171c0e219e..13fd456916 100644 --- a/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/info.vue +++ b/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/info.vue @@ -83,10 +83,6 @@ </v-list-item-title> <v-list-item-content v-if="canWriteQueues" class="amqp-consumer"> <span v-text="`${consumersUp}/${consumersTotal}`" /> - <v-badge - class="ml-1" - :color="consumersState.color" - :content="consumersState.text" /> </v-list-item-content> </v-list-item-content> </v-list-item> @@ -154,15 +150,6 @@ export default { access () { return this.$store.state.access }, - consumersState () { - if (this.consumersTotal === 0 || this.consumersTotal - this.consumersUp > 0 || this.loadingConsumers) { - return { color: 'warning', text: 'pending' } - } - if (this.consumersTotal === 0) { - return { color: 'error', text: 'down' } - } - return { color: 'success', text: 'up' } - }, consumersTotal () { return this.consumers.length }, diff --git a/dbrepo-ui/pages/user/authentication.vue b/dbrepo-ui/pages/user/authentication.vue index 92af241111..877ae8ce16 100644 --- a/dbrepo-ui/pages/user/authentication.vue +++ b/dbrepo-ui/pages/user/authentication.vue @@ -40,7 +40,6 @@ </v-btn> </v-col> </v-row> - <pre>{{ $refs.form3 }}</pre> </v-form> </v-card-text> </v-card> diff --git a/dbrepo-ui/pages/user/developer.vue b/dbrepo-ui/pages/user/developer.vue new file mode 100644 index 0000000000..7825e56243 --- /dev/null +++ b/dbrepo-ui/pages/user/developer.vue @@ -0,0 +1,133 @@ +<template> + <div v-if="canHandleMessages"> + <UserToolbar /> + <v-tabs-items v-model="tab"> + <v-tab-item> + <v-card flat tile> + <v-card-title>Maintenance Messages</v-card-title> + <v-data-table + :headers="headers" + :items="messages" + :loading="loadingMessages" + :items-per-page="10"> + <template v-slot:item.action="{ item }"> + <v-btn + x-small + @click="modifyMessage(item)"> + Modify + </v-btn> + </template> + </v-data-table> + <v-card-text> + <v-btn + small + color="secondary" + :disabled="!canCreateMessage" + @click="createMessage"> + Create Message + </v-btn> + </v-card-text> + </v-card> + </v-tab-item> + </v-tabs-items> + <v-dialog + v-model="dialog" + persistent + max-width="640"> + <EditMaintenanceMessage :id="messageId" @close-dialog="closeDialog" /> + </v-dialog> + </div> +</template> + +<script> +import UserToolbar from '@/components/UserToolbar' +import MetadataService from '@/api/metadata.service' +import EditMaintenanceMessage from '@/components/dialogs/EditMaintenanceMessage' +import { isActiveMessage } from '@/utils' + +export default { + components: { + UserToolbar, + EditMaintenanceMessage + }, + data () { + return { + tab: 0, + headers: [ + { text: 'Active', value: 'active' }, + { text: 'Type', value: 'type' }, + { text: 'Message', value: 'message' }, + { text: 'Action', value: 'action' } + ], + messages: [], + loadingMessages: false, + dialog: false, + messageId: null + } + }, + computed: { + token () { + return this.$store.state.token + }, + user () { + return this.$store.state.user + }, + roles () { + return this.$store.state.roles + }, + canCreateMessage () { + if (!this.roles) { + return false + } + return this.roles.includes('create-maintenance-message') + }, + canModifyMessage () { + if (!this.roles) { + return false + } + return this.roles.includes('modify-maintenance-message') + }, + canHandleMessages () { + return this.canCreateMessage || this.canModifyMessage + } + }, + mounted () { + this.loadMessages() + }, + methods: { + submit () { + }, + modifyMessage (message) { + this.messageId = message.id + this.dialog = true + }, + createMessage () { + this.messageId = null + this.dialog = true + }, + loadMessages () { + MetadataService.findAllMessages() + .then((messages) => { + this.messages = messages.map((message) => { + message.active = isActiveMessage(message) ? '● true' : 'false' + message.action = 'hello' + return message + }) + }) + .catch(() => { + this.loadingMessages = false + }) + .finally(() => { + this.loadingMessages = false + }) + }, + closeDialog (event) { + this.dialog = false + if (event.success) { + this.loadMessages() + this.$store.dispatch('reloadMessages') + } + } + } +} +</script> diff --git a/dbrepo-ui/store/index.js b/dbrepo-ui/store/index.js index e42e49e0d0..690814ed85 100644 --- a/dbrepo-ui/store/index.js +++ b/dbrepo-ui/store/index.js @@ -3,6 +3,7 @@ import Vuex, { Store } from 'vuex' import UserService from '@/api/user.service' import DatabaseService from '@/api/database.service' import TableService from '@/api/table.service' +import MetadataService from '@/api/metadata.service' Vue.use(Vuex) @@ -16,7 +17,8 @@ const store = new Store({ database: null, table: null, access: null, - locale: null + locale: null, + messages: [] }, getters: { getToken: state => state.token, @@ -26,7 +28,8 @@ const store = new Store({ getDatabase: state => state.database, getTable: state => state.table, getAccess: state => state.access, - getLocale: state => state.locale + getLocale: state => state.locale, + getMessages: state => state.messages }, mutations: { SET_TOKEN (state, token) { @@ -52,6 +55,9 @@ const store = new Store({ }, SET_LOCALE (state, locale) { state.locale = locale + }, + SET_MESSAGES (state, messages) { + state.messages = messages } }, actions: { @@ -78,6 +84,12 @@ const store = new Store({ .then((table) => { commit('SET_TABLE', table) }) + }, + reloadMessages ({ state, commit }) { + MetadataService.findActiveMessages() + .then((messages) => { + commit('SET_MESSAGES', messages) + }) } } }) diff --git a/dbrepo-ui/utils/index.js b/dbrepo-ui/utils/index.js index 01ed03767c..0afe772e10 100644 --- a/dbrepo-ui/utils/index.js +++ b/dbrepo-ui/utils/index.js @@ -99,6 +99,25 @@ function isOrcid (orcid) { return orcid.charAt(orcid.length - 1) === checksum } +function isActiveMessage (message) { + if (!message) { + return false + } + if (message.display_start === null || message.display_end === null) { + return true + } + if (message.display_start === null || new Date(message.display_end) >= new Date()) { + return true + } + if (new Date(message.display_start) <= new Date() || new Date(message.display_end) >= new Date()) { + return true + } + if (new Date(message.display_start) <= new Date() || message.display_end === null) { + return true + } + return false +} + module.exports = { notEmpty, formatTimestamp, @@ -109,5 +128,6 @@ module.exports = { formatYearUTC, formatMonthUTC, formatDayUTC, - isOrcid + isOrcid, + isActiveMessage } diff --git a/dbrepo-user-service/rest-service/src/main/java/at/tuwien/endpoint/MaintenanceEndpoint.java b/dbrepo-user-service/rest-service/src/main/java/at/tuwien/endpoint/MaintenanceEndpoint.java new file mode 100644 index 0000000000..8882f83fc9 --- /dev/null +++ b/dbrepo-user-service/rest-service/src/main/java/at/tuwien/endpoint/MaintenanceEndpoint.java @@ -0,0 +1,161 @@ +package at.tuwien.endpoint; + +import at.tuwien.api.maintenance.BannerMessageBriefDto; +import at.tuwien.api.maintenance.BannerMessageCreateDto; +import at.tuwien.api.maintenance.BannerMessageDto; +import at.tuwien.api.maintenance.BannerMessageUpdateDto; +import at.tuwien.api.user.UserBriefDto; +import at.tuwien.exception.BannerMessageNotFoundException; +import at.tuwien.mapper.BannerMessageMapper; +import at.tuwien.service.BannerMessageService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Log4j2 +@CrossOrigin(origins = "*") +@RestController +@RequestMapping("/api/maintenance") +public class MaintenanceEndpoint { + + private final BannerMessageMapper bannerMessageMapper; + private final BannerMessageService bannerMessageService; + + @Autowired + public MaintenanceEndpoint(BannerMessageMapper bannerMessageMapper, BannerMessageService bannerMessageService) { + this.bannerMessageMapper = bannerMessageMapper; + this.bannerMessageService = bannerMessageService; + } + + @GetMapping("/message") + @Operation(summary = "Find maintenance messages") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "List messages", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = BannerMessageDto[].class))}), + }) + public ResponseEntity<List<BannerMessageDto>> list() { + log.debug("endpoint list active maintenance messages"); + final List<BannerMessageDto> dtos = bannerMessageService.findAll() + .stream() + .map(bannerMessageMapper::bannerMessageToBannerMessageDto) + .toList(); + log.trace("list maintenance messages results in dtos {}", dtos); + return ResponseEntity.ok(dtos); + } + + @GetMapping("/message/{id}") + @Operation(summary = "Find one maintenance message") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "Get messages", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = BannerMessageDto.class))}), + }) + public ResponseEntity<BannerMessageDto> find(@NotNull @PathVariable("id") Long messageId) + throws BannerMessageNotFoundException { + log.debug("endpoint find one maintenance messages"); + final BannerMessageDto dto = bannerMessageMapper.bannerMessageToBannerMessageDto(bannerMessageService.find(messageId)); + log.trace("find one maintenance message results in dto {}", dto); + return ResponseEntity.ok(dto); + } + + @GetMapping("/message/active") + @Operation(summary = "Find active maintenance messages") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", + description = "List messages", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = BannerMessageBriefDto[].class))}), + }) + public ResponseEntity<List<BannerMessageBriefDto>> active() { + log.debug("endpoint list active maintenance messages"); + final List<BannerMessageBriefDto> dtos = bannerMessageService.getActive() + .stream() + .map(bannerMessageMapper::bannerMessageToBannerMessageBriefDto) + .toList(); + log.trace("list active maintenance messages results in dtos {}", dtos); + return ResponseEntity.ok(dtos); + } + + @PostMapping("/message") + @Operation(summary = "Create maintenance message") + @PreAuthorize("hasAuthority('create-maintenance-message')") + @ApiResponses(value = { + @ApiResponse(responseCode = "201", + description = "Created message", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = BannerMessageBriefDto.class))}), + }) + public ResponseEntity<BannerMessageBriefDto> create(@Valid @RequestBody BannerMessageCreateDto data) { + log.debug("endpoint create maintenance message, data={}", data); + final BannerMessageBriefDto dto = bannerMessageMapper.bannerMessageToBannerMessageBriefDto(bannerMessageService.create(data)); + log.trace("create maintenance message results in dto {}", dto); + return ResponseEntity.status(HttpStatus.CREATED) + .body(dto); + } + + @PutMapping("/message/{id}") + @Operation(summary = "Update maintenance message") + @PreAuthorize("hasAuthority('update-maintenance-message')") + @ApiResponses(value = { + @ApiResponse(responseCode = "202", + description = "Updated message", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = BannerMessageBriefDto.class))}), + @ApiResponse(responseCode = "404", + description = "Could not find message", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = BannerMessageNotFoundException.class))}), + }) + public ResponseEntity<BannerMessageBriefDto> update(@NotNull @PathVariable("id") Long messageId, + @Valid @RequestBody BannerMessageUpdateDto data) + throws BannerMessageNotFoundException { + log.debug("endpoint update maintenance message, messageId={}, data={}", messageId, data); + final BannerMessageBriefDto dto = bannerMessageMapper.bannerMessageToBannerMessageBriefDto(bannerMessageService.update(messageId, data)); + log.trace("update maintenance message results in dto {}", dto); + return ResponseEntity.status(HttpStatus.ACCEPTED) + .body(dto); + } + + @DeleteMapping("/message/{id}") + @Operation(summary = "Delete maintenance message") + @PreAuthorize("hasAuthority('delete-maintenance-message')") + @ApiResponses(value = { + @ApiResponse(responseCode = "202", + description = "Deleted message", + content = {@Content(mediaType = "application/json")}), + @ApiResponse(responseCode = "404", + description = "Could not find message", + content = {@Content( + mediaType = "application/json", + schema = @Schema(implementation = BannerMessageNotFoundException.class))}), + }) + public ResponseEntity<BannerMessageBriefDto> delete(@NotNull @PathVariable("id") Long messageId) + throws BannerMessageNotFoundException { + log.debug("endpoint delete maintenance message, messageId={}", messageId); + bannerMessageService.delete(messageId); + return ResponseEntity.status(HttpStatus.ACCEPTED) + .build(); + } + +} diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java b/dbrepo-user-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java index 3f1cf7c1ce..fe44809a44 100644 --- a/dbrepo-user-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java +++ b/dbrepo-user-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java @@ -43,6 +43,7 @@ public class WebSecurityConfig { final OrRequestMatcher publicEndpoints = new OrRequestMatcher( new AntPathRequestMatcher("/api/user/**", "GET"), new AntPathRequestMatcher("/api/user/**", "POST"), + new AntPathRequestMatcher("/api/maintenance/**", "GET"), new AntPathRequestMatcher("/v3/api-docs.yaml"), new AntPathRequestMatcher("/v3/api-docs/**"), new AntPathRequestMatcher("/swagger-ui/**"), diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/BannerMessageNotFoundException.java b/dbrepo-user-service/services/src/main/java/at/tuwien/exception/BannerMessageNotFoundException.java new file mode 100644 index 0000000000..e4587b1453 --- /dev/null +++ b/dbrepo-user-service/services/src/main/java/at/tuwien/exception/BannerMessageNotFoundException.java @@ -0,0 +1,20 @@ +package at.tuwien.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.NOT_FOUND) +public class BannerMessageNotFoundException extends Exception { + + public BannerMessageNotFoundException(String msg) { + super(msg); + } + + public BannerMessageNotFoundException(String msg, Throwable thr) { + super(msg, thr); + } + + public BannerMessageNotFoundException(Throwable thr) { + super(thr); + } +} diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/mapper/BannerMessageMapper.java b/dbrepo-user-service/services/src/main/java/at/tuwien/mapper/BannerMessageMapper.java new file mode 100644 index 0000000000..8fd2b7dc51 --- /dev/null +++ b/dbrepo-user-service/services/src/main/java/at/tuwien/mapper/BannerMessageMapper.java @@ -0,0 +1,24 @@ +package at.tuwien.mapper; + +import at.tuwien.api.maintenance.BannerMessageBriefDto; +import at.tuwien.api.maintenance.BannerMessageCreateDto; +import at.tuwien.api.maintenance.BannerMessageDto; +import at.tuwien.api.maintenance.BannerMessageTypeDto; +import at.tuwien.entities.maintenance.BannerMessage; +import at.tuwien.entities.maintenance.BannerMessageType; +import org.mapstruct.Mapper; + +@Mapper(componentModel = "spring") +public interface BannerMessageMapper { + + org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(BannerMessageMapper.class); + + BannerMessageDto bannerMessageToBannerMessageDto(BannerMessage data); + + BannerMessageBriefDto bannerMessageToBannerMessageBriefDto(BannerMessage data); + + BannerMessage bannerMessageCreateDtoToBannerMessage(BannerMessageCreateDto data); + + BannerMessageType bannerMessageTypeDtoToBannerMessageType(BannerMessageTypeDto data); + +} diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/repository/jpa/BannerMessageRepository.java b/dbrepo-user-service/services/src/main/java/at/tuwien/repository/jpa/BannerMessageRepository.java new file mode 100644 index 0000000000..bcdbf9f832 --- /dev/null +++ b/dbrepo-user-service/services/src/main/java/at/tuwien/repository/jpa/BannerMessageRepository.java @@ -0,0 +1,14 @@ +package at.tuwien.repository.jpa; + +import at.tuwien.entities.maintenance.BannerMessage; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface BannerMessageRepository extends JpaRepository<BannerMessage, Long> { + + List<BannerMessage> findByActive(); + +} diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/service/BannerMessageService.java b/dbrepo-user-service/services/src/main/java/at/tuwien/service/BannerMessageService.java new file mode 100644 index 0000000000..1753779848 --- /dev/null +++ b/dbrepo-user-service/services/src/main/java/at/tuwien/service/BannerMessageService.java @@ -0,0 +1,22 @@ +package at.tuwien.service; + +import at.tuwien.api.maintenance.BannerMessageCreateDto; +import at.tuwien.api.maintenance.BannerMessageUpdateDto; +import at.tuwien.entities.maintenance.BannerMessage; +import at.tuwien.exception.BannerMessageNotFoundException; + +import java.util.List; + +public interface BannerMessageService { + List<BannerMessage> findAll(); + + List<BannerMessage> getActive(); + + BannerMessage find(Long id) throws BannerMessageNotFoundException; + + BannerMessage create(BannerMessageCreateDto data); + + BannerMessage update(Long id, BannerMessageUpdateDto data) throws BannerMessageNotFoundException; + + void delete(Long id) throws BannerMessageNotFoundException; +} diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/service/impl/BannerMessageServiceImpl.java b/dbrepo-user-service/services/src/main/java/at/tuwien/service/impl/BannerMessageServiceImpl.java new file mode 100644 index 0000000000..5c38de25fc --- /dev/null +++ b/dbrepo-user-service/services/src/main/java/at/tuwien/service/impl/BannerMessageServiceImpl.java @@ -0,0 +1,78 @@ +package at.tuwien.service.impl; + +import at.tuwien.api.maintenance.BannerMessageCreateDto; +import at.tuwien.api.maintenance.BannerMessageUpdateDto; +import at.tuwien.entities.maintenance.BannerMessage; +import at.tuwien.exception.BannerMessageNotFoundException; +import at.tuwien.mapper.BannerMessageMapper; +import at.tuwien.repository.jpa.BannerMessageRepository; +import at.tuwien.service.BannerMessageService; +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 BannerMessageServiceImpl implements BannerMessageService { + + private final BannerMessageMapper bannerMessageMapper; + private final BannerMessageRepository bannerMessageRepository; + + @Autowired + public BannerMessageServiceImpl(BannerMessageMapper bannerMessageMapper, + BannerMessageRepository bannerMessageRepository) { + this.bannerMessageMapper = bannerMessageMapper; + this.bannerMessageRepository = bannerMessageRepository; + } + + @Override + public List<BannerMessage> findAll() { + return bannerMessageRepository.findAll(); + } + + @Override + public List<BannerMessage> getActive() { + return bannerMessageRepository.findByActive(); + } + + @Override + public BannerMessage find(Long id) throws BannerMessageNotFoundException { + final Optional<BannerMessage> optional = bannerMessageRepository.findById(id); + if (optional.isEmpty()) { + log.error("Failed to find banner message with id {}", id); + throw new BannerMessageNotFoundException("Failed to find banner message with id " + id); + } + return optional.get(); + } + + @Override + public BannerMessage create(BannerMessageCreateDto data) { + final BannerMessage entity = bannerMessageMapper.bannerMessageCreateDtoToBannerMessage(data); + final BannerMessage message = bannerMessageRepository.save(entity); + log.info("Created banner message with id {}", message.getId()); + return message; + } + + @Override + public BannerMessage update(Long id, BannerMessageUpdateDto data) throws BannerMessageNotFoundException { + final BannerMessage entity = find(id); + entity.setMessage(data.getMessage()); + entity.setDisplayEnd(data.getDisplayEnd()); + entity.setDisplayStart(data.getDisplayStart()); + entity.setType(bannerMessageMapper.bannerMessageTypeDtoToBannerMessageType(data.getType())); + final BannerMessage message = bannerMessageRepository.save(entity); + log.info("Updated banner message with id {}", message.getId()); + return message; + } + + @Override + public void delete(Long id) throws BannerMessageNotFoundException { + find(id); + bannerMessageRepository.deleteById(id); + log.info("Deleted banner message with id {}", id); + } + +} diff --git a/dbrepo.conf b/dbrepo.conf index e972bfcd58..4220e14b68 100644 --- a/dbrepo.conf +++ b/dbrepo.conf @@ -105,6 +105,15 @@ server { proxy_read_timeout 90; } + location /api/maintenance { + 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://user; + proxy_read_timeout 90; + } + location /api/identifier { proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; -- GitLab