diff --git a/etp-backend/deps.edn b/etp-backend/deps.edn index bb91f0ad2..62cdaa8eb 100644 --- a/etp-backend/deps.edn +++ b/etp-backend/deps.edn @@ -16,12 +16,14 @@ javax.servlet/servlet-api {:mvn/version "2.5"} org.clojure/tools.logging {:mvn/version "1.2.4"} prismatic/schema {:mvn/version "1.4.1"} - metosin/reitit-ring {:mvn/version "0.6.0"} - metosin/reitit-swagger {:mvn/version "0.6.0"} - metosin/reitit-swagger-ui {:mvn/version "0.6.0"} - metosin/reitit-middleware {:mvn/version "0.6.0"} - metosin/reitit-dev {:mvn/version "0.6.0"} - metosin/reitit-schema {:mvn/version "0.6.0"} + metosin/reitit-ring {:mvn/version "0.7.0-alpha7"} + metosin/reitit-swagger {:mvn/version "0.7.0-alpha7"} + metosin/reitit-swagger-ui {:mvn/version "0.7.0-alpha7"} + metosin/ring-swagger-ui {:mvn/version "5.9.0"} + metosin/reitit-middleware {:mvn/version "0.7.0-alpha7"} + metosin/reitit-dev {:mvn/version "0.7.0-alpha7"} + metosin/reitit-schema {:mvn/version "0.7.0-alpha7"} + fi.metosin/reitit-openapi {:mvn/version "0.7.0-alpha7"} metosin/muuntaja {:mvn/version "0.6.8"} metosin/jsonista {:mvn/version "0.3.8"} metosin/schema-tools {:mvn/version "0.13.1"} diff --git a/etp-backend/src/main/clj/solita/etp/api/palveluvayla.clj b/etp-backend/src/main/clj/solita/etp/api/palveluvayla.clj new file mode 100644 index 000000000..09fde8e83 --- /dev/null +++ b/etp-backend/src/main/clj/solita/etp/api/palveluvayla.clj @@ -0,0 +1,47 @@ +(ns solita.etp.api.palveluvayla + (:require [schema.core :as s] + [solita.etp.schema.common :as schema.common] + [solita.etp.schema.energiatodistus :as schema.energiatodistus])) + +(def accept-language-header {(s/optional-key :accept-language) schema.common/AcceptLanguage}) + +(def routes ["/energiatodistukset" + ["/pdf/:id" + ["" {:get {:summary "Hae PDF-muotoinen energiatodistus tunnuksen id:llä" + :parameters {:path {:id schema.common/Key} + :header accept-language-header} + :responses {200 {:body nil} + 404 {:body s/Str}} + :handler (constantly {:status 200}) + :openapi {:responses {200 {:description "PDF-muotoinen energiatodistus" + :content {:application/pdf {:schema {:type "string" + :format "binary"}}}}}}}}]] + ["/json" + ["/any" + ["" {:get {:summary "Hae energiatodistuksia json-muodossa. Palauttaa jaetut kentät sekä 2013, että 2018 lain mukaisista energiatodistuksista" + :parameters {:query {:rakennustunnus schema.common/Rakennustunnus}} + :responses {200 {:body [schema.energiatodistus/EnergiatodistusForAnyLaatija]}} + :handler (constantly {:status 200})}}] + ["/:id" {:get {:summary "Hae yksittäinen energiatodistus todistuksen tunnuksen perusteella. Vastaus sisältää vain kentät jotka ovat yhteisiä 2013 ja 2018 versioille." + :parameters {:path {:id schema.common/Key}} + :responses {200 {:body [schema.energiatodistus/EnergiatodistusForAnyLaatija]} + 404 {:body s/Str}} + :handler (constantly {:status 200})}}]] + ["/2013" + ["" {:get {:summary "Hae energiatodistuksia, jotka on laadittu vuoden 2013 säännösten mukaan" + :responses {200 {:body [schema.energiatodistus/Energiatodistus2013]}} + :handler (constantly {:status 200})}}] + ["/:id" {:get {:summary "Hae yksittäinen vuoden 2013 säännösten mukainen energiatodistus todistuksen tunnuksen perusteella" + :parameters {:path {:id schema.common/Key}} + :responses {200 {:body schema.energiatodistus/Energiatodistus2013} + 404 {:body s/Str}} + :handler (constantly {:status 200})}}]] + ["/2018" + ["" {:get {:summary "Hae energiatodistuksia, jotka on laadittu vuoden 2018 säännösten mukaan" + :responses {200 {:body [schema.energiatodistus/Energiatodistus2018]}} + :handler (constantly {:status 200})}}] + ["/:id" {:get {:summary "Hae yksittäinen vuoden 2018 säännösten mukainen energiatodistus todistuksen tunnuksen perusteella" + :parameters {:path {:id schema.common/Key}} + :responses {200 {:body schema.energiatodistus/Energiatodistus2018} + 404 {:body s/Str}} + :handler (constantly {:status 200})}}]]]]) diff --git a/etp-backend/src/main/clj/solita/etp/handler.clj b/etp-backend/src/main/clj/solita/etp/handler.clj index 555421fd7..c49566d33 100644 --- a/etp-backend/src/main/clj/solita/etp/handler.clj +++ b/etp-backend/src/main/clj/solita/etp/handler.clj @@ -1,46 +1,61 @@ (ns solita.etp.handler (:require [clojure.string :as str] [clojure.walk :as w] - [ring.middleware.cookies :as cookies] - [reitit.ring :as ring] - [reitit.swagger :as swagger] - [reitit.swagger-ui :as swagger-ui] + [muuntaja.core :as m] [reitit.coercion.schema] + [reitit.dev.pretty :as pretty] + [reitit.openapi :as openapi] + [reitit.ring :as ring] [reitit.ring.coercion :as coercion] - [reitit.ring.middleware.parameters :as parameters] - [reitit.ring.middleware.muuntaja :as muuntaja] [reitit.ring.middleware.multipart :as multipart] + [reitit.ring.middleware.muuntaja :as muuntaja] + [reitit.ring.middleware.parameters :as parameters] [reitit.spec :as rs] - [reitit.dev.pretty :as pretty] - [muuntaja.core :as m] + [schema.coerce] + [reitit.swagger-ui :as swagger-ui] + [ring.middleware.cookies :as cookies] + [schema.core] [schema.core :as s] + [solita.common.cf-signed-url :as signed-url] [solita.etp.api.aineisto :as aineisto-api] + [solita.etp.api.energiatodistus :as energiatodistus-api] + [solita.etp.api.geo :as geo-api] [solita.etp.api.kayttaja :as kayttaja-api] - [solita.etp.api.yritys :as yritys-api] [solita.etp.api.laatija :as laatija-api] - [solita.etp.api.geo :as geo-api] - [solita.etp.api.energiatodistus :as energiatodistus-api] - [solita.etp.api.valvonta :as valvonta-api] - [solita.etp.api.valvonta-oikeellisuus :as valvonta-oikeellisuus-api] - [solita.etp.api.valvonta-kaytto :as valvonta-kaytto-api] [solita.etp.api.laskutus :as laskutus-api] - [solita.etp.api.viesti :as viesti-api] + [solita.etp.api.palveluvayla :as palveluvayla] [solita.etp.api.sivu :as sivu-api] [solita.etp.api.statistics :as statistics-api] + [solita.etp.api.valvonta :as valvonta-api] + [solita.etp.api.valvonta-kaytto :as valvonta-kaytto-api] + [solita.etp.api.valvonta-oikeellisuus :as valvonta-oikeellisuus-api] + [solita.etp.api.viesti :as viesti-api] + [solita.etp.api.yritys :as yritys-api] [solita.etp.config :as config] - [solita.etp.security :as security] - [solita.etp.jwt :as jwt] - [solita.etp.header-middleware :as header-middleware] [solita.etp.exception :as exception] - [solita.common.cf-signed-url :as signed-url]) - (:import [java.net URLEncoder] - [java.nio.charset StandardCharsets])) + [solita.etp.header-middleware :as header-middleware] + [solita.etp.jwt :as jwt] + [solita.etp.schema.common :as schema.common] + [solita.etp.security :as security]) + (:import (java.net URLEncoder) + (java.nio.charset StandardCharsets))) (defn tag [tag routes] (w/prewalk - #(if (and (map? %) (contains? % :summary)) - (assoc % :tags #{tag}) %) - routes)) + #(if (and (map? %) (contains? % :summary)) + (assoc % :tags #{tag}) %) + routes)) + +(defn openapi-id + "Adds an openapi and swagger id to all given routes. The id is used by openapi and swagger generators to include the routes in the generated documentation." + [id routes] + (w/prewalk + #(if (and (map? %) ((some-fn :get :post :patch :delete :options :head) %)) + (-> % + (assoc :swagger {:id #{id}}) + (assoc :openapi {:id #{id}})) + %) + routes)) (defn- req->jwt [request] (try @@ -52,41 +67,41 @@ (if-let [id-token (:custom:id_token data)] (if (:custom:VIRTU_localID data) (str config/keycloak-virtu-logout-url - "?id_token_hint=" id-token - "&post_logout_redirect_uri=" (URLEncoder/encode config/cognito-logout-url StandardCharsets/UTF_8)) + "?id_token_hint=" id-token + "&post_logout_redirect_uri=" (URLEncoder/encode config/cognito-logout-url StandardCharsets/UTF_8)) (str config/keycloak-suomifi-logout-url - "?id_token_hint=" id-token - "&post_logout_redirect_uri=" (URLEncoder/encode config/cognito-logout-url StandardCharsets/UTF_8))) + "?id_token_hint=" id-token + "&post_logout_redirect_uri=" (URLEncoder/encode config/cognito-logout-url StandardCharsets/UTF_8))) (str config/index-url "/uloskirjauduttu")))) -(def empty-cookie {:value "" - :path "/" - :max-age 0 +(def empty-cookie {:value "" + :path "/" + :max-age 0 :http-only true - :secure true}) + :secure true}) (def system-routes - [["/swagger.json" - {:get {:no-doc true - :swagger {:info {:title "Energiatodistuspalvelu API" + [["/openapi.json" + {:get {:no-doc true + :swagger {:info {:title "Energiatodistuspalvelu API" :description ""}} - :handler (swagger/create-swagger-handler)}}] + :handler (openapi/create-openapi-handler)}}] ["/health" {:get {:summary "Health check" - :tags #{"System"} + :tags #{"System"} :handler (constantly {:status 200})}}] ["/login" - {:get {:summary "Callback used to redirect user back to where they were" - :tags #{"System"} + {:get {:summary "Callback used to redirect user back to where they were" + :tags #{"System"} :parameters {:query {:redirect s/Str}} - :handler (fn [{:keys [parameters]}] - (let [redirect (-> parameters :query :redirect)] - {:status 302 - :headers {"Location" (if (str/starts-with? + :handler (fn [{:keys [parameters]}] + (let [redirect (-> parameters :query :redirect)] + {:status 302 + :headers {"Location" (if (str/starts-with? + redirect + config/index-url) redirect - config/index-url) - redirect - config/index-url)}}))}}] + config/index-url)}}))}}] ["/logout" {:get {:summary "Callback used to redirect user to cognito logout" :tags #{"System"} @@ -99,10 +114,10 @@ ;; TODO Temporary endpoint for seeing headers added by load balancer ["/headers" {:get {:summary "Endpoint for seeing request headers" - :tags #{"System"} + :tags #{"System"} :handler (fn [{:keys [headers]}] {:status 200 - :body headers})}}]]) + :body headers})}}]]) (def routes ["/api" {:middleware [[header-middleware/wrap-default-cache] @@ -145,8 +160,8 @@ [security/wrap-whoami-from-signed config/public-index-url {:key-pair-id config/url-signing-key-id - :public-key (signed-url/pem-string->public-key - config/url-signing-public-key)}] + :public-key (signed-url/pem-string->public-key + config/url-signing-public-key)}] ;; Otherwise, assume that the reverse ;; proxies on front of the backend ;; service have verified the signature. @@ -156,33 +171,48 @@ (concat (tag "Aineisto API" aineisto-api/signed-routes))] ["/internal" (concat (tag "Laskutus API" laskutus-api/routes) - (tag "Laatija Internal API" laatija-api/internal-routes))]]) + (tag "Laatija Internal API" laatija-api/internal-routes))] + ["/palveluvayla" ["/openapi.json" {:get {:no-doc true + :openapi {:info {:title "Energiatodistuspalvelu API" :description "Hae energiatodistuksia pdf tai json muodoissa"} + :id "Palveluväylä"} + :handler (openapi/create-openapi-handler)}}] + (openapi-id "Palveluväylä" palveluvayla/routes)]]) + +(def default-string-coercion-options-with-project-specific-ones + "Add more schemas that support coercion to default configuration in addition to those supported by schema coercion out of the box" + (assoc-in reitit.coercion.schema/default-options [:matchers :string :default] (some-fn (some-fn {schema.common/AcceptLanguage (schema.coerce/safe schema.common/parse-accept-language)}) + (get-in reitit.coercion.schema/default-options [:matchers :string :default])))) (def route-opts {;; Uncomment line below to see diffs of requests in middleware chain ;;:reitit.middleware/transform dev/print-request-diffs :exception pretty/exception - :validate rs/validate - :data {:coercion reitit.coercion.schema/coercion - :muuntaja m/instance - :middleware [swagger/swagger-feature - parameters/parameters-middleware - muuntaja/format-negotiate-middleware - muuntaja/format-response-middleware - exception/exception-middleware - muuntaja/format-request-middleware - coercion/coerce-response-middleware - coercion/coerce-request-middleware - multipart/multipart-middleware]}}) + :validate rs/validate + :data {:coercion (reitit.coercion.schema/create default-string-coercion-options-with-project-specific-ones) + :muuntaja m/instance + :middleware [openapi/openapi-feature + parameters/parameters-middleware + muuntaja/format-negotiate-middleware + muuntaja/format-response-middleware + exception/exception-middleware + muuntaja/format-request-middleware + coercion/coerce-response-middleware + coercion/coerce-request-middleware + multipart/multipart-middleware]}}) (def router (ring/router routes route-opts)) (def handler (ring/ring-handler router (ring/routes - (swagger-ui/create-swagger-ui-handler - {:path "/api/documentation" - :url "/api/swagger.json" - :config {:validationUrl nil} - :operationsSorter "alpha"}) - (ring/create-default-handler)))) + (swagger-ui/create-swagger-ui-handler + {:path "/api/documentation" + :url "/api/openapi.json" + :config {:validationUrl nil} + :operationsSorter "alpha"}) + (swagger-ui/create-swagger-ui-handler + {:path "/api/palveluvayla/openapi" + :url "/api/palveluvayla/openapi.json" + :config {:validationUrl nil} + :operationsSorter "alpha"}) + (ring/create-default-handler)))) diff --git a/etp-backend/src/main/clj/solita/etp/schema/common.clj b/etp-backend/src/main/clj/solita/etp/schema/common.clj index 8b279e96c..5ff224163 100644 --- a/etp-backend/src/main/clj/solita/etp/schema/common.clj +++ b/etp-backend/src/main/clj/solita/etp/schema/common.clj @@ -11,9 +11,9 @@ (def Id {:id Key}) (def IdAndWarnings (assoc Id :warnings [{:property schema/Str - :value schema/Num - :min schema/Num - :max schema/Num}])) + :value schema/Num + :min schema/Num + :max schema/Num}])) (defn StringBase [max] (schema/constrained schema/Str #(<= 1 (count %) max) (str "[1, " max "]"))) @@ -56,7 +56,7 @@ (def Luokittelu (merge Id {:label-fi schema/Str :label-sv schema/Str - :valid schema/Bool})) + :valid schema/Bool})) (def Date java.time.LocalDate) (def Instant java.time.Instant) @@ -71,10 +71,10 @@ (defn valid-henkilotunnus? [s] (try - (let [date-part (subs s 0 6) - century-sign (nth s 6) + (let [date-part (subs s 0 6) + century-sign (nth s 6) individual-number (subs s 7 10) - checksum (last s)] + checksum (last s)] (and (= 11 (count s)) (contains? #{\+ \- \A} century-sign) (= checksum (henkilotunnus-checksum (str date-part individual-number))))) @@ -100,11 +100,11 @@ "y-tunnus")) (def ConstraintError - { :type schema/Keyword - :constraint schema/Keyword}) + {:type schema/Keyword + :constraint schema/Keyword}) (def GeneralError - {:type schema/Keyword + {:type schema/Keyword :message schema/Str}) (defn valid-ovt-tunnus? [s] @@ -117,7 +117,7 @@ "ovt-tunnus")) (def iban-char-map (zipmap (map char (range (int \a) (inc (int \z)))) - (range 10 36) )) + (range 10 36))) (defn valid-iban? [s] (try @@ -169,7 +169,7 @@ (defn valid-rakennustunnus? [s] (try (let [number-part (subs s 0 9) - checksum (last s)] + checksum (last s)] (and (= 10 (count s)) (= checksum (henkilotunnus-checksum number-part)))) (catch StringIndexOutOfBoundsException _ false))) @@ -197,3 +197,24 @@ schema)) (def Language (schema/enum "fi" "sv")) + +(schema/defschema WeightedLocale + "A single locale with weight for Accept-Language header" + [(schema/one (schema/constrained schema/Str #(re-matches #"(?i)([*]|[a-z]{2,3})" %)) "lang") (schema/one (schema/constrained schema/Num #(and (>= % 0) (<= % 1))) "weight")]) +(schema/defschema AcceptLanguage + "Define a schema for Accept-Language header. The header can contain multiple locales with preferred weights, for example Accept-Language: fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5" + [WeightedLocale]) + +(defn parse-lang + "Parse a language from language tag i.e. drop subtags such as region separated by -" + [s] + (-> s (str/split #"-") first)) + +(defn parse-locale [s] + (let [parts (str/split s #";q=")] + (case (count parts) + 1 [(parse-lang s) 1.0] + 2 [(-> parts first parse-lang) (Double/parseDouble (second parts))]))) + +(defn parse-accept-language [s] + (map parse-locale (str/split s #",")))