diff --git a/src/clj/auto_ap/db/users.clj b/src/clj/auto_ap/db/users.clj index 73b7be70..ac124c02 100644 --- a/src/clj/auto_ap/db/users.clj +++ b/src/clj/auto_ap/db/users.clj @@ -1,23 +1,44 @@ (ns auto-ap.db.users - (:require [auto-ap.db.utils :refer [get-conn]] + (:require [auto-ap.db.utils :refer [get-conn query db->clj clj->db]] [clojure.edn :as edn] - [clojure.java.jdbc :as j])) + [clojure.java.jdbc :as j] + [honeysql.core :as sql] + [honeysql.helpers :as helpers])) + +(defn data->fields [x] + (-> x + (merge (:data x)) + (dissoc :data))) + +(defn fields->data [x] + (-> x + (assoc-in [:data :role] (:role x)) + (assoc-in [:data :name] (:name x)) + (assoc-in [:data :companies] (:companies x)) + (dissoc :role) + (dissoc :name) + (dissoc :companies))) + +(def base-query (sql/build :select :* + :from :users)) + +(defn get-all [] + (map data->fields (query base-query))) (defn find-or-insert! [row] - (let [user (first (j/find-by-keys (get-conn) - :users - {:provider_id (:provider_id row) - :provider (:provider row)})) - ] + (let [user (-> base-query + (helpers/merge-where [:and [:= :provider-id (:provider-id row)] + [:= :provider (:provider row)]]) + query + first + data->fields)] (if user - (merge user (edn/read-string (:data user "{}"))) - (do - (j/insert! (get-conn) - :users - {:provider_id (:provider_id row) - :provider (:provider row) - :data "{}"}) - {:provider_id (:provider_id row) - :provider (:provider row) - :data "{}"}) - ))) + user + (-> (j/insert! (get-conn) + :users + (-> row + fields->data + clj->db)) + first + db->clj + data->fields)))) diff --git a/src/clj/auto_ap/graphql.clj b/src/clj/auto_ap/graphql.clj index b045be49..a871cafc 100644 --- a/src/clj/auto_ap/graphql.clj +++ b/src/clj/auto_ap/graphql.clj @@ -6,8 +6,10 @@ [com.walmartlabs.lacinia.executor :as executor] [com.walmartlabs.lacinia.resolve :as resolve] [auto-ap.db.invoices :as invoices] + [auto-ap.utils :refer [by]] [auto-ap.db.vendors :as vendors] [auto-ap.db.companies :as companies] + [auto-ap.db.users :as users] [auto-ap.db.checks :as checks] [auto-ap.routes.checks :as rchecks] [auto-ap.db.reminders :as reminders] @@ -63,6 +65,12 @@ :check {:type :check :resolve :get-check-by-id}}} + :user + {:fields {:id {:type 'Int} + :name {:type 'String} + :role {:type 'String} + :companies {:type '(list :company)}}} + :invoice {:fields {:id {:type 'Int} :total {:type 'String} @@ -117,7 +125,9 @@ :company {:type '(list :company) :resolve :get-company} :vendor {:type '(list :vendor) - :resolve :get-vendor}} + :resolve :get-vendor} + :user {:type '(list :user) + :resolve :get-user}} :input-objects { @@ -132,12 +142,7 @@ :company_id {:type 'Int}} :resolve :mutation/print-checks}}}) -(defn by [x kf] - (reduce - (fn [m x] - (assoc m (kf x) x)) - {} - x)) + (defn snake->kebab [s] (str/replace s #"_" "-")) @@ -178,8 +183,8 @@ (defn get-invoice-page [context args value] (let [extra-context (cond-> {} - (executor/selects-field? context :invoice/vendor) (assoc :vendor-cache (by (vendors/get-all) :id )) - (executor/selects-field? context :invoice/company) (assoc :company-cache (by (companies/get-all) :id ))) + (executor/selects-field? context :invoice/vendor) (assoc :vendor-cache (by :id (vendors/get-all))) + (executor/selects-field? context :invoice/company) (assoc :company-cache (by :id (companies/get-all)))) invoices (map ->graphql @@ -195,7 +200,7 @@ (defn get-reminder-page [context args value] (let [extra-context (cond-> {} - (executor/selects-field? context :reminder/vendor) (assoc :vendor-cache (by (vendors/get-all) :id ))) + (executor/selects-field? context :reminder/vendor) (assoc :vendor-cache (by :id (vendors/get-all)))) reminders (map ->graphql @@ -232,6 +237,22 @@ (->graphql (companies/get-all))) +(defn join-companies [users] + (let [companies (by :id (companies/get-all))] + (map + (fn [u] + (update u :companies #(map companies %))) + users))) + +(defn get-user [context args value] + (let [users (users/get-all) + users (cond-> users + (executor/selects-field? context :user/companies) + (join-companies) + )] + (println users) + (->graphql users))) + (defn get-vendor [context args value] (->graphql (vendors/get-all))) @@ -253,6 +274,7 @@ :get-invoices-checks get-invoices-checks :get-check-by-id get-check-by-id :get-company get-company + :get-user get-user :mutation/print-checks print-checks :get-vendor get-vendor}) schema/compile)) diff --git a/src/clj/auto_ap/routes/auth.clj b/src/clj/auto_ap/routes/auth.clj index 178f4c2b..ae673347 100644 --- a/src/clj/auto_ap/routes/auth.clj +++ b/src/clj/auto_ap/routes/auth.clj @@ -28,13 +28,16 @@ :body (doto println)) user (users/find-or-insert! {:provider "google" - :provider_id (:id profile)})] + :provider-id (:id profile) + :role "none" + :name (:name profile)})] (if (and token user) {:status 301 :headers {"Location" (str "/?jwt=" (jwt/sign {:user "test" :exp (time/plus (time/now) (time/days 7)) :companies (:companies user) + :role (:role user) :name (:name profile)} (:jwt-secret env) {:alg :hs512}))}} diff --git a/src/cljs/auto_ap/core.cljs b/src/cljs/auto_ap/core.cljs index 84c649c9..d4498989 100644 --- a/src/cljs/auto_ap/core.cljs +++ b/src/cljs/auto_ap/core.cljs @@ -21,14 +21,14 @@ (defn ^:export init [] (dev-setup) - (pushy/start! p/history) + (if-let [jwt (.get (js/URLSearchParams. (.-search (.-location js/window))) "jwt")] (do (.setItem js/localStorage "jwt" jwt) (re-frame/dispatch-sync [::events/initialize-db jwt])) (do (re-frame/dispatch-sync [::events/initialize-db (.getItem js/localStorage "jwt")]))) - + (pushy/start! p/history) (mount-root)) diff --git a/src/cljs/auto_ap/events.cljs b/src/cljs/auto_ap/events.cljs index feda9fe7..63e7fd5e 100644 --- a/src/cljs/auto_ap/events.cljs +++ b/src/cljs/auto_ap/events.cljs @@ -5,17 +5,31 @@ [auto-ap.routes :as routes] [auto-ap.effects :as effects] [venia.core :as v] - [bidi.bidi :as bidi])) + [bidi.bidi :as bidi] + [goog.crypt.base64 :as b64] + [clojure.string :as str])) + +(defn jwt->data [token] + (js->clj (.parse js/JSON (b64/decodeString (second (str/split token #"\." )))))) (re-frame/reg-event-fx ::initialize-db (fn [{:keys [db]} [_ token]] (let [handler (:handler (bidi/match-route routes/routes (.. js/window -location -pathname)))] - (if (and (not= :login handler) (not token)) + (cond + (and (not= :login handler) (not token)) {:redirect "/login" :db (assoc db/default-db :active-page :login :user token)} + + (and token (= "none" (get (jwt->data token) "role") )) + {:redirect "/needs-activation" + :db (assoc db/default-db + :active-page :needs-activation + :user token)} + + :else {:db (assoc db/default-db :active-page handler :user token) diff --git a/src/cljs/auto_ap/history.cljs b/src/cljs/auto_ap/history.cljs index cc1a5315..601741c1 100644 --- a/src/cljs/auto_ap/history.cljs +++ b/src/cljs/auto_ap/history.cljs @@ -5,10 +5,11 @@ [re-frame.core :as re-frame])) (defn- parse-url [url] + (println url) (bidi/match-route routes/routes url)) (defn- dispatch-route [matched-route] - (println matched-route) + (println "Matched route" matched-route) (re-frame/dispatch [:auto-ap.events/set-active-page (:handler matched-route)])) diff --git a/src/cljs/auto_ap/routes.cljs b/src/cljs/auto_ap/routes.cljs index 7dca3a47..647676ba 100644 --- a/src/cljs/auto_ap/routes.cljs +++ b/src/cljs/auto_ap/routes.cljs @@ -3,9 +3,11 @@ (def routes ["/" {"" :index "login/" :login + "needs-activation/" :needs-activation "check/" :check "admin/" {"" :admin "companies" :admin-companies + "users" :admin-users "reminders" :admin-reminders "vendors" :admin-vendors "excel-import" :admin-excel-import} diff --git a/src/cljs/auto_ap/views/main.cljs b/src/cljs/auto_ap/views/main.cljs index ab6f4513..fa4ac106 100644 --- a/src/cljs/auto_ap/views/main.cljs +++ b/src/cljs/auto_ap/views/main.cljs @@ -12,6 +12,7 @@ (defn page->layout [page] ({:login :blank :check :blank + :needs-activation :blank :index :left-panel :invoices :left-panel :import-invoices :left-panel @@ -19,10 +20,11 @@ :paid-invoices :left-panel :admin :admin-left-panel :admin-companies :admin-left-panel + :admin-users :admin-left-panel :admin-excel-import :admin-left-panel :admin-vendors :admin-left-panel :admin-reminders :admin-left-panel - :new-invoice :blank} page)) + :new-invoice :blank} page :blank)) (defn login-dropdown [] (let [user (re-frame/subscribe [::subs/user]) diff --git a/src/cljs/auto_ap/views/pages.cljs b/src/cljs/auto_ap/views/pages.cljs index 629d8923..b6b1af18 100644 --- a/src/cljs/auto_ap/views/pages.cljs +++ b/src/cljs/auto_ap/views/pages.cljs @@ -7,8 +7,10 @@ [auto-ap.views.pages.login :refer [login-page]] [auto-ap.views.pages.index :refer [index-page]] [auto-ap.views.pages.admin :refer [admin-page]] + [auto-ap.views.pages.needs-activation :refer [needs-activation-page]] [auto-ap.views.pages.check :refer [check-page]] [auto-ap.views.pages.admin.companies :refer [admin-companies-page]] + [auto-ap.views.pages.admin.users :refer [admin-users-page]] [auto-ap.views.pages.admin.vendors :refer [admin-vendors-page]] [auto-ap.views.pages.admin.reminders :refer [admin-reminders-page]] [auto-ap.views.pages.unpaid-invoices :refer [unpaid-invoices-page]] @@ -35,6 +37,9 @@ (defmethod active-page :admin [] [admin-page]) +(defmethod active-page :needs-activation [] + [needs-activation-page]) + (defmethod active-page :check [] [check-page]) @@ -50,6 +55,9 @@ (defmethod active-page :admin-excel-import [] [admin-excel-import-page]) +(defmethod active-page :admin-users [] + [admin-users-page]) + (defmethod active-page :unpaid-invoices [] [unpaid-invoices-page]) diff --git a/src/cljs/auto_ap/views/pages/admin/users.cljs b/src/cljs/auto_ap/views/pages/admin/users.cljs new file mode 100644 index 00000000..640774af --- /dev/null +++ b/src/cljs/auto_ap/views/pages/admin/users.cljs @@ -0,0 +1,155 @@ +(ns auto-ap.views.pages.admin.users + (:require-macros [cljs.core.async.macros :refer [go]]) + (:require [re-frame.core :as re-frame] + [reagent.core :as reagent] + [auto-ap.subs :as subs] + [auto-ap.events.admin.companies :as events] + [auto-ap.entities.companies :as entity] + [auto-ap.views.components.address :refer [address-field]] + [auto-ap.views.utils :refer [login-url dispatch-value-change bind-field horizontal-field dispatch-event]] + [auto-ap.views.components.modal :refer [modal]] + [auto-ap.utils :refer [by]] + [cljs.reader :as edn] + [auto-ap.routes :as routes] + [bidi.bidi :as bidi])) + +(re-frame/reg-sub + ::users + (fn [db] + (-> db (::users [])))) + +(re-frame/reg-sub + ::editing + (fn [db] + (-> db ::editing))) + +(re-frame/reg-event-fx + ::users-mounted + (fn [{:keys [db]} _] + {:graphql {:token (:user db) + :query-obj {:venia/queries [[:user + [:name + :id + :role + [:companies [:id :name]]]]]} + :on-success [::received]}})) + +(re-frame/reg-event-db + ::change + (fn [db [_ path value]] + (assoc-in db (concat [::editing] path) + value))) + +(re-frame/reg-event-db + ::received + (fn [db [_ d]] + (assoc-in db [::users] (:user d)))) + +(re-frame/reg-event-db + ::edit + (fn [db [_ d]] + (if-let [user (get (by :id (::users db)) d)] + (assoc-in db [::editing :user] user) + (dissoc db ::editing)))) + +(re-frame/reg-event-db + ::add-company + (fn [db [_ d]] + (let [company (get @(re-frame/subscribe [::subs/companies-by-id]) + (js/parseInt (get-in db [::editing :adding-company])))] + (update-in db [::editing :user :companies] conj company)))) + +(re-frame/reg-event-db + ::remove-company + (fn [db [_ d]] + (println "remove compnay " d) + (update-in db [::editing :user :companies] #(filter (fn [c] (not= (:id c) d)) %)))) + +(defn users-table [] + (let [users (re-frame/subscribe [::users])] + [:table {:class "table", :style {:width "100%"}} + [:thead + [:tr + [:th "User"] + [:th "Role"] + [:th "Companies"]]] + [:tbody (for [{:keys [id name role companies] :as c} @users] + ^{:key (str name "-" id )} + [:tr {:on-click (fn [] (re-frame/dispatch [::edit id])) + :style {"cursor" "pointer"}} + [:td name] + [:td role] + [:td]])]])) +(def admin-users-page + (with-meta + (fn [] + [:div + (let [companies (re-frame/subscribe [::users]) + editing @(re-frame/subscribe [::editing])] + (println editing) + + [:div + [:h1.title "Users"] + [users-table] + + (when editing + [modal {:title (str "Edit " (:name (:user editing))) + :foot [:a.button.is-primary {:on-click (fn [] (re-frame/dispatch [::events/save]))} + [:span "Save"] + (when (:saving? editing) + [:span.icon + [:i.fa.fa-spin.fa-spinner]])] + :hide-event [::edit nil]} + [horizontal-field + [:label.label "Name"] + [:div.control + [bind-field + [:input.input {:type "text" + :field [:user :name] + :spec ::entity/name + :event ::change + :subscription editing}]]]] + + [horizontal-field + [:label.label "Role"] + [:div.control + [bind-field + [:select.select {:type "select" + :field [:user :role] + :spec ::entity/name + :event ::change + :subscription editing} + [:option {:value "none"} "None"] + [:option {:value "user"} "User"] + [:option {:value "admin"} "Admin"]]]]] + + + (when (= "user" (:role (:user editing))) + [horizontal-field + [:label.label "Companies"] + [:div.control + + [:div.field.has-addons + [:p.control + [:div.select + [bind-field + [:select {:type "select" + :field [:adding-company] + :event ::change + :subscription editing} + [:option] + (let [used-companies (set (map :id (:companies (:user editing))))] + (for [{:keys [id name]} @(re-frame/subscribe [::subs/companies]) + :when (not (used-companies id))] + ^{:key id} [:option {:value id} name]))]]]] + [:p.control + [:button.button.is-primary {:on-click (dispatch-event [::add-company])} "Add"]]] + + [:ul + (for [{:keys [id name]} (:companies (:user editing))] + ^{:key id} [:li name [:a.icon {:on-click (dispatch-event [::remove-company id])} [:i.fa.fa-times ]]])]]]) + + (when (:saving? editing) [:div.is-overlay {:style {"backgroundColor" "rgba(150,150,150, 0.5)"}}])])])]) + {:component-will-mount #(re-frame/dispatch-sync [::users-mounted {}]) })) + + diff --git a/src/cljs/auto_ap/views/pages/needs_activation.cljs b/src/cljs/auto_ap/views/pages/needs_activation.cljs new file mode 100644 index 00000000..3faa9ae5 --- /dev/null +++ b/src/cljs/auto_ap/views/pages/needs_activation.cljs @@ -0,0 +1,17 @@ +(ns auto-ap.views.pages.needs-activation + (:require-macros [cljs.core.async.macros :refer [go]]) + (:require [re-frame.core :as re-frame] + [reagent.core :as reagent] + [auto-ap.subs :as subs] + [auto-ap.events :as events] + [auto-ap.views.utils :refer [login-url]] + [cljs.reader :as edn] + [auto-ap.routes :as routes] + [bidi.bidi :as bidi] + [goog.string :as gstring])) + + + +(defn needs-activation-page [] + [:div + [:h2 "Sorry, your user is not activated yet. Please have Ben Skinner enable your account."]]) diff --git a/src/cljs/auto_ap/views/utils.cljs b/src/cljs/auto_ap/views/utils.cljs index 8bb4f965..c9b53eec 100644 --- a/src/cljs/auto_ap/views/utils.cljs +++ b/src/cljs/auto_ap/views/utils.cljs @@ -42,6 +42,8 @@ event (if (keyword? event) [event] event) keys (assoc keys :on-change (dispatch-value-change (conj event field)) + + :value (get-in subscription field) :class (str class (when (and spec (not (s/valid? spec (get-in subscription field)))) " is-danger")))