(ns auto-ap.handler (:require [amazonica.core :refer [defcredential]] [auto-ap.client-routes :as client-routes] [auto-ap.datomic :refer [conn pull-many]] [auto-ap.graphql.utils :refer [extract-client-ids limited-clients]] [auto-ap.logging :as alog] [auto-ap.routes.auth :as auth] [auto-ap.routes.exports :as exports] [auto-ap.routes.ezcater :as ezcater] [auto-ap.routes.graphql :as graphql] [auto-ap.routes.health :as health] [auto-ap.routes.invoices :as invoices] [auto-ap.routes.queries :as queries] [auto-ap.routes.yodlee2 :as yodlee2] [auto-ap.session-version :as session-version] [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.core :as ssr] [auto-ap.ssr.utils :refer [entity-id main-transformer]] [bidi.bidi :as bidi] [bidi.ring :refer [->ResourcesMaybe make-handler]] [buddy.auth.backends.session :refer [session-backend]] [buddy.auth.backends.token :refer [jws-backend]] [buddy.auth.middleware :refer [wrap-authentication wrap-authorization]] [cemerick.url :as url] [cheshire.core :as cheshire] [clj-time.coerce :as coerce] [clj-time.core :as time] [clojure.data.json :as json] [clojure.set :as set] [clojure.string :as str] [com.brunobonacci.mulog :as mu] [config.core :refer [env]] [datomic.api :as dc] [hiccup2.core :as hiccup] [malli.core :as mc] [ring.middleware.edn :refer [wrap-edn-params]] [ring.middleware.multipart-params :as mp] [ring.middleware.params :refer [wrap-params]] [ring.middleware.session :refer [wrap-session]] [ring.middleware.session.cookie :refer [cookie-store]] [ring.util.response :as response])) (when (:aws-access-key-id env) (defcredential (:aws-access-key-id env) (:aws-secret-access-key env) (:aws-region env))) (defn deep-merge [v & vs] (letfn [(rec-merge [v1 v2] (if (and (map? v1) (map? v2)) (merge-with deep-merge v1 v2) v2))] (when (some identity vs) (reduce #(rec-merge %1 %2) v vs)))) (def all-routes ["/" (-> (into [] (deep-merge ssr-routes/routes (second client-routes/routes) graphql/routes ezcater/routes health/routes queries/routes2 yodlee2/routes auth/routes invoices/routes exports/routes2)) (conj ["" (->ResourcesMaybe {:prefix "public/"})]) (conj [true :not-found])) ;; always go for not found as last resort, have to switch to vec in order for that to work ]) (defn not-found [_] {:status 404 :headers {} :body ""}) (defn render-index [_] (response/resource-response "index.html" {:root "public"})) (def match->handler-lookup (-> {:not-found not-found} (merge ssr/key->handler) (merge graphql/match->handler) (merge ezcater/match->handler) (merge health/match->handler) (merge queries/match->handler) (merge yodlee2/match->handler) (merge auth/match->handler) (merge invoices/match->handler) (merge exports/match->handler) (merge (into {} (map (fn [k] [k render-index]) client-routes/all-matches))))) (def match->handler (fn [route] (or (get match->handler-lookup route) route))) (def route-handler (make-handler all-routes match->handler)) (defn wrap-guess-route [handler] (fn [{:keys [uri request-method headers] :as request}] (let [matched-route (:handler (bidi.bidi/match-route all-routes uri :request-method request-method)) matched-hx-current-url-route (some->> (get headers "hx-current-url") url/url :path (bidi/match-route ssr-routes/only-routes) :handler)] (handler (assoc request :matched-route matched-route :matched-current-page-route matched-hx-current-url-route))))) (defn test-match-route [method uri] (bidi.bidi/match-route all-routes uri :request-method method)) (def auth-backend (jws-backend {:secret (:jwt-secret env) :options {:alg :hs512}})) (defn wrap-logging [handler] (fn [request] (mu/with-context (cond-> {:uri (:uri request) :route (:handler (bidi.bidi/match-route all-routes (:uri request) :request-method (:request-method request))) :client-selection (:client-selection request) :source "request" :query (:uri request) :request-method (:request-method request) :user (dissoc (:identity request) :gz-clients) :user-role (:user/role (:identity request)) :user-name (:user/name (:identity request))} (not= "/api/graphql" (:uri request)) (assoc :query-params (:query-params request))) (mu/trace ::http-request-trace {:pairs [] :capture (fn [r] {:status (:status r)})} (when-not (str/includes? (:uri request) "health-check") (alog/info ::http-request-starting)) (try (let [response (handler request)] response) (catch Exception e (alog/error ::request-error :status 500 :exception e) (throw e))))))) (defn wrap-idle-session-timeout [handler] (fn [request] (let [session (:session request {:version session-version/current-session-version}) end-time (coerce/to-date-time (::idle-timeout session))] (if (and end-time (time/before? end-time (time/now))) (if (get (:headers request) "hx-request") {:session nil :status 200 :headers {"hx-redirect" "/login"}} {:session nil :status 302 :headers {"Location" "/login"}}) (when-let [response (handler request)] (let [session (:session response session)] (if (nil? session) response (let [end-time (time/plus (time/now) (time/days 14))] (assoc response :session (assoc session ::idle-timeout (coerce/to-date end-time))))))))))) (defn wrap-hx-current-url-params [handler] (fn [request] (let [query-params (some-> (get-in request [:headers "hx-current-url"]) (url/url) :query) request (assoc request :hx-query-params query-params)] (handler request)))) (def client-selection-schema (mc/schema [:orn [:global [:enum :all :mine]] [:group-name [:map [:group :string]]] [:specific [:map [:selected [:vector entity-id]]]]])) (defn wrap-hydrate-clients [handler] (fn [request] (let [x-clients (-> request :client-selection) identity (or (-> request :identity) (-> request :session :identity)) ideal-ids (set (cond (or (= :all x-clients) (nil? x-clients)) (->> (dc/q '[:find ?c :where [?c :client/code]] (dc/db conn)) (map first)) (= :mine x-clients) (map :db/id (:user/clients identity)) (:group x-clients) (->> (dc/q '[:find ?c :in $ ?g :where [?c :client/groups ?g]] (dc/db conn) (str/upper-case (or (:group x-clients) "INVALID"))) (map first) set) (seq (:selected x-clients)) (->> x-clients :selected (filter #(not (nil? %))) set))) limited-clients (some->> (limited-clients identity) (map :db/id) set) client-ids (if (= "admin" (:user/role identity)) ideal-ids (set/intersection ideal-ids (or limited-clients #{}))) clients (some->> client-ids seq (pull-many (dc/db conn) '[:db/id :client/name :client/code :client/locations :client/matches :client/feature-flags {:client/bank-accounts [:db/id {:bank-account/type [:db/ident]} :bank-account/number :bank-account/name :bank-account/code]}]))] (mu/with-context {:clients (take 10 (map :client/code clients))} (handler (assoc request :clients clients :client (when (= 1 (count clients)) (first clients)))))))) (defn wrap-store-client-in-session [handler] (fn [{:keys [headers identity] :as request}] (let [client-selection (try (mc/decode client-selection-schema (some-> (get headers "x-clients") not-empty json/read-str) main-transformer) (catch Exception e (alog/warn ::cant-access :error e :identity identity :x-clients (pr-str (get headers "x-clients"))) nil)) new-request (if client-selection (assoc-in request [:client-selection] client-selection) (assoc-in request [:client-selection] (get-in request [:session :client-selection] :all)))] (cond-> (handler new-request) client-selection (update :session (fn [new-session] (-> (:session request) (into new-session) (assoc :client-selection client-selection)))))))) (defn wrap-gunzip-jwt [handler] (fn [{:keys [session] :as request}] (let [request (if-let [gz-clients (some-> request :identity :gz-clients)] (try (assoc-in request [:identity :user/clients] (auth/gunzip gz-clients)) (catch Exception e (alog/error :cant-gunzip-clients :error e) request)) request)] (handler request)))) #_(defn wrap-pprint-session [handler] (fn [request] (clojure.pprint/pprint (:session request)) (handler request))) (defn wrap-error [handler] (fn error-handling-request [request] (try (handler request) (catch Throwable e (if (= :notification (:type (ex-data e))) {:status 200 :headers {"hx-trigger" (cheshire/generate-string {"notification" (str (hiccup/html [:div (.getMessage e)]))}) "hx-reswap" "none"}} {:status 500 :body (pr-str e)}))))) (defn wrap-trim-clients [handler] (fn [request] (let [valid-clients (extract-client-ids (:clients request) (:client request) (:client-id (:parsed-query-params request)) (when (:client-code (:parsed-query-params request)) [:client/code (:client-code (:parsed-query-params request))])) trimmed-clients (->> valid-clients (take 20) set)] (handler (assoc request :valid-client-ids valid-clients :valid-trimmed-client-ids trimmed-clients :first-client-id (first valid-clients) :clients-trimmed? (not= (count trimmed-clients) (count valid-clients))))))) #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (defonce app (-> route-handler (wrap-hx-current-url-params) (wrap-guess-route) (wrap-logging) (wrap-trim-clients) (wrap-hydrate-clients) (wrap-store-client-in-session) (wrap-gunzip-jwt) (wrap-authorization auth-backend) (wrap-authentication auth-backend (session-backend {:authfn (fn [auth] (dissoc auth :exp))})) #_(wrap-pprint-session) (session-version/wrap-session-version) (wrap-idle-session-timeout) (wrap-session {:store (cookie-store {:key (byte-array [42, 52, -31, 101, -126, -33, -118, -69, -82, -59, -15, -69, -38, 103, -102, -1])})}) #_(wrap-reload) (wrap-params) (mp/wrap-multipart-params) (wrap-edn-params) (wrap-error)))