diff --git a/src/clj/auto_ap/graphql/plaid.clj b/src/clj/auto_ap/graphql/plaid.clj index 4ec1ef83..763ebe07 100644 --- a/src/clj/auto_ap/graphql/plaid.clj +++ b/src/clj/auto_ap/graphql/plaid.clj @@ -2,25 +2,34 @@ (:require [auto-ap.datomic :refer [add-sorter-fields apply-pagination apply-sort-3 conn merge-query]] - [auto-ap.graphql.utils :refer [assert-admin assert-present <-graphql ->graphql]] + [auto-ap.graphql.utils + :refer [->graphql + <-graphql + assert-admin + assert-can-see-client + assert-present + limited-clients]] [auto-ap.plaid.core :as p] [clj-time.coerce :as coerce] [clj-time.core :as time] + [clojure.tools.logging :as log] [com.walmartlabs.lacinia.util :refer [attach-resolvers]] [datomic.api :as d])) (defn plaid-link-token [context value args] - (assert-admin (:id context)) + (when-not (:client_id value) + (throw (ex-info "Client ID is required" {:validation-error "Client ID is required"}))) + (assert-can-see-client (:id context) (:client_id value)) (let [client-code (:client/code (d/pull (d/db conn) [:client/code] (:client_id value)))] {:token (p/get-link-token client-code)})) (defn link-plaid [context value args] - (assert-admin (:id context)) (when-not (:client_code value) (throw (ex-info "Client not provided" {:validation-error "Client not provided."}))) (when-not (:public_token value) (throw (ex-info "Public token not provided" {:validation-error "public token not provided"}))) - + (log/info (:id context) (:db/id (d/pull (d/db conn) [:db/id] [:client/code (:client_code value)]))) + (assert-can-see-client (:id context) (:db/id (d/pull (d/db conn) [:db/id] [:client/code (:client_code value)]))) (let [access-token (:access_token (p/exchange-public-token (:public_token value) (:client_code value))) account-result (p/get-accounts access-token ) item {:plaid-item/client [:client/code (:client_code value)] @@ -40,7 +49,8 @@ :plaid-item/_accounts "plaid-item"} balance (assoc :plaid-account/balance balance))))) (into [item]))) - {:message (str "Plaid linked successfully. Access Token: " access-token)})) + (log/info "Access token was " access-token) + {:message (str "Plaid linked successfully.")})) (def default-read '[:db/id @@ -54,7 +64,6 @@ :plaid-account/name]}]) (defn raw-graphql-ids [db args] - (println args) (let [query (cond-> {:query {:find [] :in ['$] :where []} @@ -63,6 +72,11 @@ (:sort args) (add-sorter-fields {"external-id" ['[?e :plaid-item/external-id ?sort-external-id]]} args) + (limited-clients (:id args)) + (merge-query {:query {:in ['[?xx ...]] + :where ['[?e :plaid-item/client ?xx]]} + :args [ (set (map :db/id (limited-clients (:id args))))]}) + (:client-id args) (merge-query {:query {:in '[?client-id] :where ['[?e :plaid-item/client ?client-id]]} @@ -93,7 +107,7 @@ (defn get-plaid-item-page [context args value] - (assert-admin (:id context)) + (let [args (assoc args :id (:id context)) [plaid-items cnt] (get-graphql (<-graphql (assoc args :id (:id context))))] {:plaid_items (->> plaid-items diff --git a/src/cljc/auto_ap/client_routes.cljc b/src/cljc/auto_ap/client_routes.cljc index f3344447..5af3beab 100644 --- a/src/cljc/auto_ap/client_routes.cljc +++ b/src/cljc/auto_ap/client_routes.cljc @@ -17,8 +17,7 @@ "reminders" :admin-reminders "vendors" :admin-vendors "excel-import" :admin-excel-import - "yodlee2" :admin-yodlee2 - "plaid" :admin-plaid} + "yodlee2" :admin-yodlee2} "invoices/" {"" :invoices "import" :import-invoices "unpaid" :unpaid-invoices @@ -33,6 +32,7 @@ "requires-feedback" :requires-feedback-transactions "excluded" :excluded-transactions} "reports/" {"" :reports} + "plaid" :plaid "ledger/" {"" :ledger "profit-and-loss" :profit-and-loss "balance-sheet" :balance-sheet diff --git a/src/cljs/auto_ap/events.cljs b/src/cljs/auto_ap/events.cljs index cc8c19bc..44cd4ae1 100644 --- a/src/cljs/auto_ap/events.cljs +++ b/src/cljs/auto_ap/events.cljs @@ -4,11 +4,12 @@ [auto-ap.routes :as routes] [auto-ap.subs :as subs] [auto-ap.utils :refer [by]] - [auto-ap.views.utils :refer [with-user]] + [auto-ap.views.utils :refer [with-user parse-jwt]] [bidi.bidi :as bidi] [clojure.string :as str] [goog.crypt.base64 :as b64] - [re-frame.core :as re-frame])) + [re-frame.core :as re-frame] + [goog.crypt.base64 :as base64])) (defn jwt->data [token] (js->clj (.parse js/JSON (b64/decodeString (second (str/split token #"\." )))))) @@ -142,12 +143,20 @@ (re-frame/reg-event-fx ::set-active-route (fn [{:keys [db]} [_ handler params route-params]] + (println (:user/role (parse-jwt (:user db)))) (cond (and (not= :login handler) (not (:user db))) {:redirect (bidi/path-for routes/routes :login) :db (assoc db :active-route :login :active-page :login :page-failure nil)} + + (and (not= "admin" (:user/role (parse-jwt (:user db)))) + (str/includes? (name handler) "admin")) + {:redirect (bidi/path-for routes/routes :index) + :db (assoc db :active-route :index + :active-page :index + :page-failure nil)} :else {:db (-> db (assoc :active-route handler diff --git a/src/cljs/auto_ap/subs.cljs b/src/cljs/auto_ap/subs.cljs index 4adc529e..a8900709 100644 --- a/src/cljs/auto_ap/subs.cljs +++ b/src/cljs/auto_ap/subs.cljs @@ -2,6 +2,7 @@ (ns auto-ap.subs (:require [re-frame.core :as re-frame] [auto-ap.utils :refer [by]] + [auto-ap.views.utils :refer [parse-jwt]] [clojure.string :as str] [goog.crypt.base64 :as base64] [minisearch :as ms])) @@ -141,8 +142,7 @@ (re-frame/reg-sub ::user (fn [db] - (when (:user db) - (js->clj (.parse js/JSON (base64/decodeString (second (str/split (:user db) #"\.")))) :keywordize-keys true)))) + (parse-jwt (:user db)))) (re-frame/reg-sub ::active-route diff --git a/src/cljs/auto_ap/views/components/admin/side_bar.cljs b/src/cljs/auto_ap/views/components/admin/side_bar.cljs index 4afbdfa3..a53f490e 100644 --- a/src/cljs/auto_ap/views/components/admin/side_bar.cljs +++ b/src/cljs/auto_ap/views/components/admin/side_bar.cljs @@ -56,10 +56,7 @@ [:span {:class "icon icon-saving-bank-1" :style {:font-size "25px"}}] [:span {:class "name"} "Yodlee 2 Link"]]] - [:li.menu-item - [:a {:href (bidi/path-for routes/routes :admin-plaid), :class (str "item" (active-when ap = :admin-plaid))} - [:span {:class "icon icon-saving-bank-1" :style {:font-size "25px"}}] - [:span {:class "name"} "Plaid Link"]]] + [:ul ]] [:p.menu-label "History"] diff --git a/src/cljs/auto_ap/views/main.cljs b/src/cljs/auto_ap/views/main.cljs index a31762ff..c86355d5 100644 --- a/src/cljs/auto_ap/views/main.cljs +++ b/src/cljs/auto_ap/views/main.cljs @@ -28,7 +28,7 @@ [auto-ap.views.pages.admin.users :refer [admin-users-page]] [auto-ap.views.pages.admin.import-batches :refer [import-batches-page]] [auto-ap.views.pages.admin.yodlee2 :as yodlee2] - [auto-ap.views.pages.admin.plaid :as plaid])) + [auto-ap.views.pages.company.plaid :as plaid])) (defmulti page (fn [active-page] active-page)) (defmethod page :unpaid-invoices [_] @@ -112,8 +112,8 @@ (defmethod page :admin-yodlee2 [_] (yodlee2/admin-yodle-provider-accounts-page)) -(defmethod page :admin-plaid [_] - (plaid/admin-plaid-page)) +(defmethod page :plaid [_] + (plaid/plaid-page)) (defmethod page :admin-accounts [_] (admin-accounts-page)) diff --git a/src/cljs/auto_ap/views/pages/admin/clients/form.cljs b/src/cljs/auto_ap/views/pages/admin/clients/form.cljs index 0411c398..d26e0e2f 100644 --- a/src/cljs/auto_ap/views/pages/admin/clients/form.cljs +++ b/src/cljs/auto_ap/views/pages/admin/clients/form.cljs @@ -9,7 +9,7 @@ [auto-ap.views.components.address :refer [address2-field]] [react-signature-canvas] [auto-ap.views.components.typeahead :refer [typeahead-v3]] - [auto-ap.views.components.level :refer [left-stack]] + [auto-ap.views.components.level :refer [left-stack] :as level] [auto-ap.views.components :as com] [auto-ap.views.components.typeahead.vendor :refer [search-backed-typeahead]] diff --git a/src/cljs/auto_ap/views/pages/admin/plaid.cljs b/src/cljs/auto_ap/views/pages/admin/plaid.cljs index 33dce7ce..e709a90e 100644 --- a/src/cljs/auto_ap/views/pages/admin/plaid.cljs +++ b/src/cljs/auto_ap/views/pages/admin/plaid.cljs @@ -1,9 +1,9 @@ -(ns auto-ap.views.pages.admin.plaid +(ns auto-ap.views.pages.company.plaid (:require [auto-ap.effects.forward :as forward] [auto-ap.status :as status] [auto-ap.subs :as subs] - [auto-ap.views.components.admin.side-bar :refer [admin-side-bar]] + [auto-ap.views.pages.company.side-bar :refer [company-side-bar]] [auto-ap.views.components.grid :as grid] [auto-ap.views.components.layouts :refer [side-bar-layout]] [auto-ap.views.pages.admin.plaid.table :as table] @@ -146,19 +146,17 @@ (defn plaid-link-token-button [] (let [status @(re-frame/subscribe [::status/single ::get-link-token]) - client-code (:code @(re-frame/subscribe [::subs/client]))] + client @(re-frame/subscribe [::subs/client])] [:button.button.is-primary {:disabled (status/disabled-for status) :class (status/class-for status) - :on-click (dispatch-event [::get-link-token client-code])} - "Authenticate with Plaid (" client-code ")"])) + :on-click (dispatch-event [::get-link-token (:code client)])} + "Authenticate with Plaid (" (:name client) ")"])) (defn link-flow [] [:div (let [link-token @(re-frame/subscribe [::link-token]) client-code (:code @(re-frame/subscribe [::subs/client]))] (cond - - (and link-token client-code) [:div "Authentication successful!" @@ -185,12 +183,12 @@ ])) -(defn admin-plaid-page [] +(defn plaid-page [] (reagent/create-class {:component-will-unmount #(re-frame/dispatch [::unmounted]) :component-did-mount #(re-frame/dispatch [::mounted]) :reagent-render (fn [] - [side-bar-layout {:side-bar [admin-side-bar {}] + [side-bar-layout {:side-bar [company-side-bar {}] :main [admin-plaid-item-content]}])})) diff --git a/src/cljs/auto_ap/views/pages/admin/plaid/table.cljs b/src/cljs/auto_ap/views/pages/admin/plaid/table.cljs index 2dcfcee6..bad6a6f2 100644 --- a/src/cljs/auto_ap/views/pages/admin/plaid/table.cljs +++ b/src/cljs/auto_ap/views/pages/admin/plaid/table.cljs @@ -79,6 +79,7 @@ (defn table [{:keys [status data-page]}] (let [{:keys [data]} @(re-frame/subscribe [::data-page/page data-page]) params @(re-frame/subscribe [::params]) + is-admin? @(re-frame/subscribe [::subs/is-admin?]) statuses @(re-frame/subscribe [::status/multi ::refresh])] [grid/grid {:data-page data-page :column-count 5} @@ -105,5 +106,6 @@ [:li (:name a) [:div.tag (->$ (:balance a))]])]] [grid/cell {} [:div.buttons - [buttons/fa-icon {:event [::delete-requested (:id c)] - :icon "fa-times"}]]]])]]])) + (when is-admin? + [buttons/fa-icon {:event [::delete-requested (:id c)] + :icon "fa-times"}])]]])]]])) diff --git a/src/cljs/auto_ap/views/pages/company/plaid.cljs b/src/cljs/auto_ap/views/pages/company/plaid.cljs new file mode 100644 index 00000000..fc6468be --- /dev/null +++ b/src/cljs/auto_ap/views/pages/company/plaid.cljs @@ -0,0 +1,194 @@ +(ns auto-ap.views.pages.admin.plaid + (:require + [auto-ap.effects.forward :as forward] + [auto-ap.status :as status] + [auto-ap.subs :as subs] + [auto-ap.views.pages.company.side-bar :refer [company-side-bar]] + [auto-ap.views.components.grid :as grid] + [auto-ap.views.components.layouts :refer [side-bar-layout]] + [auto-ap.views.pages.admin.plaid.table :as table] + [auto-ap.views.utils :refer [dispatch-event with-user]] + [re-frame.core :as re-frame] + [react-plaid-link :refer [usePlaidLink]] + [reagent.core :as reagent] + [auto-ap.views.pages.data-page :as data-page] + [clojure.set :as set] + [vimsical.re-frame.fx.track :as track])) + +(re-frame/reg-sub + ::link-token + (fn [db] + (-> db ::link-token))) + +(re-frame/reg-sub + ::message + (fn [db] + (-> db ::message))) + +(re-frame/reg-sub + ::params + :<- [::table/params] + (fn [table-params] + table-params)) + +(re-frame/reg-sub + ::plaid-items + (fn [db] + (::plaid-items db))) + +(re-frame/reg-event-fx + ::params-change + (fn [_ [_ params]] + {:set-uri-params params})) + + + +(re-frame/reg-event-fx + ::data-requested + (fn [{:keys [db]} [_ params]] + (println "PRAAMS" params) + {:graphql {:token (:user db) + :owns-state {:single ::page} + :query-obj {:venia/queries [{:query/data [:plaid-item-page {:client-id (:id @(re-frame/subscribe [::subs/client])) + :sort (:sort params) + :start (:start params 0)} + [[:plaid-items [:id :last-updated :status + [:client [:id]] + [:accounts [:id :name :number :balance]]]] + :count + :start + :end + :total]] + :query/alias :result}] + } + :on-success (fn [result] + [::data-page/received ::page + (set/rename-keys (:result result) + {:plaid-items :data})])}})) + +(re-frame/reg-event-fx + ::mounted + (fn [{:keys [db]} _] + {:dispatch [::data-requested] + ::forward/register {:id ::plaid-item-deleted + :events #{::table/plaid-item-deleted} + :event-fn (fn [[_ query-result]] + [::data-requested])} + ::track/register {:id ::params + :subscription [::data-page/params ::page] + :event-fn (fn [params] + [::data-requested params])} + :db (dissoc db + ::link-token + ::message)})) + +(re-frame/reg-event-fx + ::unmounted + (fn [{:keys [db]} _] + {::forward/dispose {:id ::plaid-item-deleted}})) + + +(re-frame/reg-event-fx + ::get-link-token + [with-user] + (fn [{:keys [db]} [_ client]] + {:graphql {:token (:user db) + :owns-state {:single ::get-link-token} + :query-obj {:venia/queries [[:plaid-link-token {:client-id (:id @(re-frame/subscribe [::subs/client]))} + [:token]]]} + :on-success [::authenticated]}})) + +(re-frame/reg-event-fx + ::plaid-linked + (fn [{:keys [db]} [_ m]] + {:db (assoc db ::message (:message (:link-plaid m))) + :dispatch [::data-requested]})) + +(re-frame/reg-event-fx + ::exchange-token + [with-user] + (fn [{:keys [db]} [_ client public-token]] + {:graphql {:token (:user db) + :owns-state {:single ::get-link-token} + :query-obj + {:venia/operation {:operation/type :mutation + :operation/name "LinkPlaid"} + :venia/queries [{:query/data + [:link-plaid + {:client-code client + :public-token public-token} + [:message]]}]} + :on-success [::plaid-linked]}})) + +(re-frame/reg-event-db + ::authenticated + (fn [db [_ link-token]] + (-> db + (assoc-in [::link-token] (:token (:plaid-link-token link-token)))))) + +(re-frame/reg-event-db + ::received + (fn [db [_ d]] + (assoc-in db [::plaid-items] (:plaid-item-page d)))) + + +(defn plaid-item-table [] + [table/table {:data-page ::page + :status @(re-frame/subscribe [::status/single ::page])}]) + +(defn link-button [{:keys [link-token client-code]}] + (let [plaid (usePlaidLink #js {:token link-token + :onSuccess (fn [x] + (re-frame/dispatch [::exchange-token client-code x]))})] + [:div + [:button.button.is-primary {:on-click (.-open plaid)} + [:span [:span.icon [:i.fa.fa-external-link]] " Go to plaid"]]])) + +(defn plaid-link-token-button [] + (let [status @(re-frame/subscribe [::status/single ::get-link-token]) + client-code (:code @(re-frame/subscribe [::subs/client]))] + [:button.button.is-primary {:disabled (status/disabled-for status) + :class (status/class-for status) + :on-click (dispatch-event [::get-link-token client-code])} + "Authenticate with Plaid (" client-code ")"])) + +(defn link-flow [] + [:div + (let [link-token @(re-frame/subscribe [::link-token]) + client-code (:code @(re-frame/subscribe [::subs/client]))] + (cond + (and link-token client-code) + [:div + "Authentication successful!" + [:f> link-button {:link-token link-token + :client-code client-code}]] + + client-code + [plaid-link-token-button] + + + :else + nil))]) + + +(defn admin-plaid-item-content [] + (let [message @(re-frame/subscribe [::message])] + [:div + [:h1.title "Plaid Accounts"] + (when message + [:div.notification.is-info.is-light + message]) + [plaid-item-table] + [link-flow] + ])) + + +(defn admin-plaid-page [] + (reagent/create-class + {:component-will-unmount #(re-frame/dispatch [::unmounted]) + :component-did-mount #(re-frame/dispatch [::mounted]) + :reagent-render (fn [] + [side-bar-layout {:side-bar [company-side-bar {}] + :main [admin-plaid-item-content]}])})) + + diff --git a/src/cljs/auto_ap/views/pages/company/side_bar.cljs b/src/cljs/auto_ap/views/pages/company/side_bar.cljs index 2f628883..ff04cafe 100644 --- a/src/cljs/auto_ap/views/pages/company/side_bar.cljs +++ b/src/cljs/auto_ap/views/pages/company/side_bar.cljs @@ -14,4 +14,8 @@ [:a.item {:href (bidi/path-for routes/routes :reports) :class [(active-when ap = :reports)]} [:span {:class "icon icon-receipt" :style {:font-size "25px"}}] - [:span {:class "name"} "Reports"]]]]])) + [:span {:class "name"} "Reports"]]]] + [:li.menu-item + [:a {:href (bidi/path-for routes/routes :plaid), :class (str "item" (active-when ap = :plaid))} + [:span {:class "icon icon-saving-bank-1" :style {:font-size "25px"}}] + [:span {:class "name"} "Plaid Link"]]]])) diff --git a/src/cljs/auto_ap/views/utils.cljs b/src/cljs/auto_ap/views/utils.cljs index 0e0d15c9..1814ce76 100644 --- a/src/cljs/auto_ap/views/utils.cljs +++ b/src/cljs/auto_ap/views/utils.cljs @@ -614,3 +614,11 @@ :else x)) + + +(defn parse-jwt [jwt] + (when-let [json (some-> jwt + (str/split #"\.") + second + base64/decodeString)] + (js->clj (.parse js/JSON json) :keywordize-keys true)))