(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.datomic.clients :as d-clients] [auto-ap.graphql.utils :refer [assert-can-see-client 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.ssr-routes :as ssr-routes] [auto-ap.ssr.core :as ssr] [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] [clj-time.coerce :as coerce] [clj-time.core :as time] [clojure.string :as str] [clojure.edn :as edn] [com.brunobonacci.mulog :as mu] [config.core :refer [env]] [datomic.api :as dc] [ring.middleware.edn :refer [wrap-edn-params]] [ring.middleware.multipart-params :as mp] [ring.middleware.params :refer [wrap-params]] [ring.middleware.reload :refer [wrap-reload]] [ring.middleware.session :refer [wrap-session]] [ring.middleware.session.cookie :refer [cookie-store]] [ring.util.response :as response] [unilog.context :as lc] [clojure.set :as set])) (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] :as request} ] (let [matched-route (:handler (bidi.bidi/match-route all-routes uri :request-method request-method))] (handler (assoc request :matched-route matched-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 {:uri (:uri request) :query (:uri request) :request-method (:request-method request) :user (:identity request) :user-role (:user/role (:identity request)) :user-name (:user/name (:identity request)) :query-params (:query-params request)} (mu/trace ::http-request-trace [] (lc/with-context {:uri (:uri request) :source "request" :user-role (:user/role (:identity request)) :user-name (:user/name (:identity request)) :query-params (:query-params request)} (when-not (str/includes? (:uri request) "health-check") (alog/info ::http-request-starting)) (try (let [response (handler request)] (alog/info ::http-request-done :status-code (:status response)) response) (catch Exception e (alog/error ::request-error :exception e) (throw e)))))))) (defn wrap-idle-session-timeout [handler ] (fn [request] (let [session (:session request {}) 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)))) (defn wrap-hydrate-clients [handler] (fn [request] (let [x-clients (-> request :session :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)) (seq x-clients) (->> x-clients (map (fn [c] (if (string? c) (try (Long/parseLong c) (catch Exception e nil)) c))) (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) d-clients/full-read))] (lc/with-context {:clients (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 [x-clients (edn/read-string (get headers "x-clients")) x-clients (try (if-let [client-id (and x-clients (sequential? x-clients) (first x-clients))] (do (assert-can-see-client identity (cond-> client-id (string? client-id) (Long/parseLong))) [(if (string? client-id) (Long/parseLong client-id) client-id)]) x-clients) (catch Exception e (alog/warn ::cant-access :error e :identity identity :x-clients (pr-str x-clients)) :all)) new-request (if x-clients (assoc-in request [:session :client-selection] x-clients) request)] (cond-> (handler new-request) x-clients (update :session (fn [new-session] (-> (:session request) (into new-session) (assoc :client-selection x-clients)))))))) (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))) #_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]} (def app (-> route-handler (wrap-hx-current-url-params) (wrap-guess-route) (wrap-hydrate-clients) (wrap-store-client-in-session) (wrap-logging) (wrap-gunzip-jwt) (wrap-authorization auth-backend) (wrap-authentication auth-backend (session-backend {:authfn (fn [auth] (dissoc auth :exp))})) #_(wrap-pprint-session) (wrap-idle-session-timeout) (wrap-session {:store (cookie-store {:key (byte-array [42, 52, -31, 105, -126, -33, -118, -69, -82, -59, -15, -69, -38, 103, -102, -1])} )}) (wrap-reload) (wrap-params) (mp/wrap-multipart-params) (wrap-edn-params) ))