mirror of
https://github.com/status-im/open-bounty.git
synced 2025-02-02 12:44:40 +00:00
New UX, webhook securing, atomic repo toggling etc.
Secure Github webhooks (Fixes #18) * Use a unique random secret for webhooks * Validate payload signature when receiving webhook Make enabling + disabling a repo more robust * store repository state instead of enabled flag in DB * atomic toggle UI button (Fixes #17) New UX for managing repos * group repos by owner * look and feel according to UI spec General improvements * only request user's repos via Github API once per session * fix issue with cljs code figwheel reload * simplify app-db structure
This commit is contained in:
parent
6e646280c2
commit
d35b794ca4
2
env/dev/cljs/commiteth/dev.cljs
vendored
2
env/dev/cljs/commiteth/dev.cljs
vendored
@ -7,7 +7,7 @@
|
||||
|
||||
(figwheel/watch-and-reload
|
||||
:websocket-url "ws://localhost:3449/figwheel-ws"
|
||||
:on-jsload core/mount-components)
|
||||
:on-jsload core/on-js-load)
|
||||
|
||||
(devtools/install!)
|
||||
|
||||
|
@ -37,8 +37,9 @@
|
||||
[tentacles "0.5.1"]
|
||||
[re-frisk "0.3.2"]
|
||||
[bk/ring-gzip "0.2.1"]
|
||||
;;
|
||||
]
|
||||
[crypto-random "1.2.0"]
|
||||
[crypto-equality "1.0.0"]
|
||||
[cheshire "5.7.0"]]
|
||||
|
||||
:min-lein-version "2.0.0"
|
||||
:source-paths ["src/clj" "src/cljc"]
|
||||
|
15
resources/migrations/20170215174652-repo-enable-state.up.sql
Normal file
15
resources/migrations/20170215174652-repo-enable-state.up.sql
Normal file
@ -0,0 +1,15 @@
|
||||
|
||||
CREATE TABLE "public"."repo_state" (
|
||||
"id" int,
|
||||
"description" character varying(32) NOT NULL,
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
INSERT INTO "public"."repo_state"("id", "description") VALUES(0, 'Not enabled');
|
||||
INSERT INTO "public"."repo_state"("id", "description") VALUES(1, 'Creating hook');
|
||||
INSERT INTO "public"."repo_state"("id", "description") VALUES(2, 'Enabled');
|
||||
INSERT INTO "public"."repo_state"("id", "description") VALUES(-1, 'Failed to create hook');
|
||||
|
||||
ALTER TABLE "public"."repositories" DROP COLUMN "enabled";
|
||||
ALTER TABLE "public"."repositories"
|
||||
ADD COLUMN "state" int NOT NULL DEFAULT 0;
|
@ -0,0 +1,2 @@
|
||||
ALTER TABLE "public"."repositories"
|
||||
ADD COLUMN "hook_secret" character varying(64);
|
@ -50,38 +50,58 @@ WHERE r.repo_id = :repo_id;
|
||||
|
||||
-- Repositories --------------------------------------------------------------------
|
||||
|
||||
-- :name toggle-repository! :<! :1
|
||||
-- :doc toggles 'enabled' flag of a given repository
|
||||
-- :name set-repo-state! :<! :1
|
||||
-- :doc sets repository to given state
|
||||
UPDATE repositories
|
||||
SET enabled = NOT enabled
|
||||
SET state = :state
|
||||
WHERE repo_id = :repo_id
|
||||
RETURNING repo_id, login, repo, enabled, hook_id;
|
||||
RETURNING repo_id, login, repo, state, hook_id;
|
||||
|
||||
|
||||
-- :name get-repo :? :1
|
||||
-- :doc retrieve a repository given login and repo-name
|
||||
SELECT *
|
||||
FROM repositories
|
||||
WHERE login = :login
|
||||
AND repo = :repo;
|
||||
|
||||
|
||||
-- :name create-repository! :<! :1
|
||||
-- :doc creates repository if not exists
|
||||
INSERT INTO repositories (repo_id, user_id, login, repo, enabled)
|
||||
INSERT INTO repositories (repo_id, user_id, login, repo, state)
|
||||
SELECT
|
||||
:repo_id,
|
||||
:user_id,
|
||||
:login,
|
||||
:repo,
|
||||
:enabled
|
||||
:state
|
||||
WHERE NOT exists(SELECT 1
|
||||
FROM repositories
|
||||
WHERE repo_id = :repo_id)
|
||||
RETURNING repo_id, user_id, login, repo, enabled;
|
||||
RETURNING repo_id, user_id, login, repo, state;
|
||||
|
||||
-- :name get-enabled-repositories :? :*
|
||||
-- :doc returns enabled repositories for a given login
|
||||
SELECT repo_id
|
||||
FROM repositories
|
||||
WHERE user_id = :user_id AND enabled = TRUE;
|
||||
WHERE user_id = :user_id
|
||||
AND state = 2;
|
||||
|
||||
-- :name update-hook-id :! :n
|
||||
-- :doc updates hook_id of a specified repository
|
||||
|
||||
-- :name update-repo-generic :! :n
|
||||
/* :require [clojure.string :as string]
|
||||
[hugsql.parameters :refer [identifier-param-quote]] */
|
||||
UPDATE repositories
|
||||
SET hook_id = :hook_id
|
||||
WHERE repo_id = :repo_id;
|
||||
SET
|
||||
/*~
|
||||
(string/join ","
|
||||
(for [[field _] (:updates params)]
|
||||
(str (identifier-param-quote (name field) options)
|
||||
" = :v:updates." (name field))))
|
||||
~*/
|
||||
where repo_id = :repo_id;
|
||||
|
||||
|
||||
|
||||
-- Issues --------------------------------------------------------------------------
|
||||
|
||||
|
@ -16,17 +16,14 @@
|
||||
(some #(= label-name (:name %)) labels)))
|
||||
|
||||
|
||||
(defn add-bounty-for-issue [repo-map issue]
|
||||
(defn add-bounty-for-issue [repo repo-id login issue]
|
||||
(log/debug "add-bounty-for-issue" issue)
|
||||
(let [{issue-id :id
|
||||
issue-number :number
|
||||
issue-title :title} issue
|
||||
{repo :repo
|
||||
repo-id :repo_id
|
||||
user :login} repo-map
|
||||
created-issue (issues/create repo-id issue-id issue-number issue-title)
|
||||
repo-owner (:address (users/get-repo-owner repo-id))]
|
||||
(log/info (format "Issue %s/%s/%s labeled as bounty" user repo issue-number))
|
||||
(log/info (format "Issue %s/%s/%s labeled as bounty" login repo issue-number))
|
||||
(if (= 1 created-issue)
|
||||
(let [transaction-hash (eth/deploy-contract repo-owner)]
|
||||
(log/info "Contract deployed, transaction-hash:" transaction-hash )
|
||||
@ -34,13 +31,11 @@
|
||||
(log/debug "Issue already exists in DB, ignoring"))))
|
||||
|
||||
|
||||
(defn add-bounties-for-existing-issues [repo-map]
|
||||
(let [{repo :repo
|
||||
user :login} repo-map
|
||||
issues (github/get-issues user repo)
|
||||
(defn add-bounties-for-existing-issues [repo repo-id login]
|
||||
(let [issues (github/get-issues login repo)
|
||||
bounty-issues (filter has-bounty-label? issues)]
|
||||
(log/debug bounty-issues)
|
||||
(log/debug "adding bounties for"
|
||||
(count bounty-issues) " existing issues")
|
||||
(doall
|
||||
(map (partial add-bounty-for-issue repo-map) bounty-issues))))
|
||||
(map (partial add-bounty-for-issue repo repo-id login) bounty-issues))))
|
||||
|
@ -1,21 +1,20 @@
|
||||
(ns commiteth.db.repositories
|
||||
(:require [commiteth.db.core :refer [*db*] :as db]
|
||||
[clojure.java.jdbc :as jdbc]
|
||||
[clojure.set :refer [rename-keys]]))
|
||||
|
||||
(defn toggle
|
||||
"Toggles specified repository"
|
||||
[repo-id]
|
||||
(jdbc/with-db-connection [con-db *db*]
|
||||
(db/toggle-repository! con-db {:repo_id repo-id})))
|
||||
[clojure.set :refer [rename-keys]]
|
||||
[clojure.string :as str]))
|
||||
|
||||
(defn create
|
||||
"Creates repository"
|
||||
"Creates repository or returns existing one."
|
||||
[repo]
|
||||
(jdbc/with-db-connection [con-db *db*]
|
||||
(or
|
||||
(db/create-repository! con-db (-> repo
|
||||
(rename-keys {:id :repo_id :name :repo})
|
||||
(merge {:enabled true})))))
|
||||
(rename-keys {:id :repo_id
|
||||
:name :repo})
|
||||
(merge {:state 0})))
|
||||
(db/get-repo {:repo (:name repo)
|
||||
:login (:login repo)}))))
|
||||
|
||||
(defn get-enabled
|
||||
"Lists enabled repositories ids for a given login"
|
||||
@ -25,8 +24,16 @@
|
||||
(db/get-enabled-repositories con-db {:user_id user-id}))
|
||||
(mapcat vals)))
|
||||
|
||||
(defn update-hook-id
|
||||
"Updates github webhook id for a given repository"
|
||||
[repo-id hook-id]
|
||||
(defn update-repo
|
||||
[repo-id updates]
|
||||
(jdbc/with-db-connection [con-db *db*]
|
||||
(db/update-hook-id con-db {:repo_id repo-id :hook_id hook-id})))
|
||||
(db/update-repo-generic con-db {:repo_id repo-id
|
||||
:updates updates})))
|
||||
|
||||
|
||||
(defn get-repo
|
||||
"Get a repo from DB given it's full name (owner/repo-name)"
|
||||
[full-name]
|
||||
(let [[login repo-name] (str/split full-name #"/")]
|
||||
(jdbc/with-db-connection [con-db *db*]
|
||||
(db/get-repo {:login login :repo repo-name}))))
|
||||
|
@ -87,7 +87,6 @@
|
||||
owner2 (format-param owner)
|
||||
data (str contract-code owner1 owner2)
|
||||
value (format "0x%x" 1)]
|
||||
(println data)
|
||||
(send-transaction (eth-account) nil value {:data data})))
|
||||
|
||||
(defn- format-call-params
|
||||
|
@ -41,7 +41,7 @@
|
||||
:redirect_uri (redirect-uri)
|
||||
:state state}}))
|
||||
|
||||
(defn- auth-params
|
||||
(defn auth-params
|
||||
[token]
|
||||
{:oauth-token token
|
||||
:client-id (client-id)
|
||||
@ -64,19 +64,22 @@
|
||||
:permissions
|
||||
:private])
|
||||
|
||||
(def login-field [:owner :login])
|
||||
|
||||
(defn list-repos
|
||||
"List all repos managed by the given user."
|
||||
(defn get-user-repos
|
||||
"List all repos managed by the given user. Returns map of repository
|
||||
maps grouped by owner. Owner is an organization name or the user's
|
||||
own login."
|
||||
[token]
|
||||
(let [all-repos-with-admin-access
|
||||
(->>
|
||||
(map #(merge
|
||||
{:login (get-in % login-field)}
|
||||
{:owner-login (get-in % [:owner :login])}
|
||||
{:owner-type (get-in % [:owner :type])}
|
||||
(select-keys % repo-fields))
|
||||
(repos/repos (merge (auth-params token) {:type "all"
|
||||
:all-pages true})))
|
||||
(filter #(not (:fork %)))
|
||||
(filter #(-> % :permissions :admin))))
|
||||
(filter #(-> % :permissions :admin)))]
|
||||
(group-by :owner-login all-repos-with-admin-access)))
|
||||
|
||||
(defn get-user
|
||||
[token]
|
||||
@ -91,12 +94,13 @@
|
||||
:email)))
|
||||
|
||||
(defn add-webhook
|
||||
[full-repo token]
|
||||
[full-repo token secret]
|
||||
(log/debug "adding webhook" full-repo token)
|
||||
; TODO: use secret key in config param
|
||||
(let [[user repo] (str/split full-repo #"/")]
|
||||
(repos/create-hook user repo "web"
|
||||
{:url (str (server-address) "/webhook")
|
||||
:secret secret
|
||||
:content_type "json"}
|
||||
(merge (auth-params token)
|
||||
{:events ["issues", "issue_comment", "pull_request"]
|
||||
|
@ -23,9 +23,7 @@
|
||||
(wrap-routes middleware/wrap-csrf)
|
||||
(wrap-routes middleware/wrap-formats))
|
||||
#'redirect-routes
|
||||
(-> #'webhook-routes
|
||||
(wrap-routes wrap-json-params)
|
||||
(wrap-routes wrap-keyword-params))
|
||||
#'webhook-routes
|
||||
#'service-routes
|
||||
#'qr-routes
|
||||
(route/not-found
|
||||
|
@ -11,7 +11,8 @@
|
||||
[commiteth.bounties :as bounties]
|
||||
[commiteth.github.core :as github]
|
||||
[clojure.tools.logging :as log]
|
||||
[commiteth.eth.core :as eth]))
|
||||
[commiteth.eth.core :as eth]
|
||||
[crypto.random :as random]))
|
||||
|
||||
(defn access-error [_ _]
|
||||
(unauthorized {:error "unauthorized"}))
|
||||
@ -28,6 +29,65 @@
|
||||
[_ binding acc]
|
||||
(update-in acc [:letks] into [binding `(:identity ~'+compojure-api-request+)]))
|
||||
|
||||
|
||||
(defn enable-repo [repo-id repo login token]
|
||||
(log/debug "enable-repo" repo-id repo)
|
||||
(let [hook-secret (random/base64 32)]
|
||||
(try
|
||||
(repositories/update-repo repo-id {:state 1
|
||||
:hook_secret hook-secret})
|
||||
(let [created-hook (github/add-webhook repo token hook-secret)]
|
||||
(log/debug "Created webhook:" created-hook)
|
||||
(github/create-label repo token)
|
||||
(repositories/update-repo repo-id {:state 2
|
||||
:hook_id (:id created-hook)})
|
||||
(bounties/add-bounties-for-existing-issues repo repo-id login))
|
||||
(catch Exception e
|
||||
(log/info "exception when creating webhook" (.getMessage e))
|
||||
(repositories/update-repo repo-id {:state -1})))))
|
||||
|
||||
|
||||
(defn disable-repo [repo-id repo hook-id token]
|
||||
(log/debug "disable-repo" repo-id repo)
|
||||
(do
|
||||
(github/remove-webhook repo hook-id token)
|
||||
(repositories/update-repo repo-id {:hook_secret ""
|
||||
:state 0
|
||||
:hook_id nil})))
|
||||
|
||||
|
||||
(defn handle-toggle-repo [user params]
|
||||
(log/debug "handle-toggle-repo" user params)
|
||||
(let [{token :token
|
||||
login :login
|
||||
user-id :id} user
|
||||
{repo-id :id
|
||||
repo :full_name} params
|
||||
db-item (repositories/create (merge params {:user_id user-id
|
||||
:login login}))
|
||||
is-enabled (= 2 (:state db-item))]
|
||||
(if is-enabled
|
||||
(disable-repo repo-id repo (:hook_id db-item) token)
|
||||
(enable-repo repo-id repo login token))
|
||||
(merge
|
||||
{:enabled (not is-enabled)}
|
||||
(select-keys params [:id :full_name]))))
|
||||
|
||||
(defn in? [coll elem]
|
||||
(some #(= elem %) coll))
|
||||
|
||||
(defn handle-get-user-repos [user]
|
||||
(log/debug "handle-get-user-repos")
|
||||
(let [github-repos (github/get-user-repos (:token user))
|
||||
enabled-repos (vec (repositories/get-enabled (:id user)))
|
||||
repo-enabled? (fn [repo] (in? enabled-repos (:id repo)))
|
||||
update-enabled (fn [repo] (assoc repo :enabled (repo-enabled? repo)))]
|
||||
(into {}
|
||||
(map (fn [[group repos]] {group
|
||||
(map update-enabled repos)})
|
||||
github-repos))))
|
||||
|
||||
|
||||
(defapi service-routes
|
||||
{:swagger {:ui "/swagger-ui"
|
||||
:spec "/swagger.json"
|
||||
@ -57,10 +117,7 @@
|
||||
(GET "/repositories" []
|
||||
:auth-rules authenticated?
|
||||
:current-user user
|
||||
(ok {:repositories (->> (github/list-repos (:token user))
|
||||
(map #(select-keys %
|
||||
[:id :html_url
|
||||
:name :full_name :description])))}))
|
||||
(ok {:repositories (handle-get-user-repos user)}))
|
||||
(GET "/enabled-repositories" []
|
||||
:auth-rules authenticated?
|
||||
:current-user user
|
||||
@ -86,20 +143,4 @@
|
||||
(POST "/repository/toggle" {:keys [params]}
|
||||
:auth-rules authenticated?
|
||||
:current-user user
|
||||
(ok (let [{repo-id :id
|
||||
repo :full_name} params
|
||||
{token :token
|
||||
login :login
|
||||
user-id :id} user
|
||||
result (or
|
||||
(repositories/create (merge params {:user_id user-id}))
|
||||
(repositories/toggle repo-id))]
|
||||
(if (:enabled result)
|
||||
(let [created-hook (github/add-webhook repo token)]
|
||||
(log/debug "Created webhook:" created-hook)
|
||||
(future
|
||||
(github/create-label repo token)
|
||||
(repositories/update-hook-id repo-id (:id created-hook))
|
||||
(bounties/add-bounties-for-existing-issues result)))
|
||||
(github/remove-webhook repo (:hook_id result) token))
|
||||
result))))))
|
||||
(ok (handle-toggle-repo user params))))))
|
||||
|
@ -1,15 +1,19 @@
|
||||
(ns commiteth.routes.webhooks
|
||||
(:require [compojure.core :refer [defroutes POST]]
|
||||
[commiteth.github.core :as github]
|
||||
[commiteth.db.pull-requests :as pull-requests]
|
||||
[commiteth.db.issues :as issues]
|
||||
[commiteth.db.users :as users]
|
||||
(:require [cheshire.core :as json]
|
||||
[clojure.string :as str :refer [join]]
|
||||
[clojure.tools.logging :as log]
|
||||
[commiteth.bounties :as bounties]
|
||||
[ring.util.http-response :refer [ok]]
|
||||
[clojure.string :refer [join]]
|
||||
[clojure.tools.logging :as log])
|
||||
(:import [java.lang Integer]))
|
||||
|
||||
[commiteth.db
|
||||
[issues :as issues]
|
||||
[pull-requests :as pull-requests]
|
||||
[repositories :as repos]
|
||||
[users :as users]]
|
||||
[commiteth.github.core :as github]
|
||||
[commiteth.util.digest :refer [hex-hmac-sha1]]
|
||||
[compojure.core :refer [defroutes POST]]
|
||||
[crypto.equality :as crypto]
|
||||
[ring.util.http-response :refer [ok]])
|
||||
(:import java.lang.Integer))
|
||||
|
||||
(defn find-issue-event
|
||||
[events type owner]
|
||||
@ -125,15 +129,28 @@
|
||||
(handle-issue-closed webhook-payload)))
|
||||
(ok (str webhook-payload)))
|
||||
|
||||
|
||||
(defn handle-pull-request
|
||||
[pull-request]
|
||||
(when (= "closed" (:action pull-request))
|
||||
(handle-pull-request-closed pull-request))
|
||||
(ok (str pull-request)))
|
||||
|
||||
|
||||
(defn validate-secret [webhook-payload raw-payload github-signature]
|
||||
(let [full-name (get-in webhook-payload [:repository :full_name])
|
||||
repo (repos/get-repo full-name)
|
||||
secret (:hook_secret repo)
|
||||
signature (str "sha1=" (hex-hmac-sha1 secret raw-payload))]
|
||||
(crypto/eq? signature github-signature)))
|
||||
|
||||
|
||||
(defroutes webhook-routes
|
||||
(POST "/webhook" {:keys [params headers]}
|
||||
(POST "/webhook" {:keys [headers body]}
|
||||
(let [raw-payload (slurp body)
|
||||
payload (json/parse-string raw-payload true)]
|
||||
(if (validate-secret payload raw-payload (get headers "x-hub-signature"))
|
||||
(case (get headers "x-github-event")
|
||||
"issues" (handle-issue params)
|
||||
"pull_request" (handle-pull-request params)
|
||||
(ok))))
|
||||
"issues" (handle-issue payload)
|
||||
"pull_request" (handle-pull-request payload)
|
||||
(ok))))))
|
||||
|
6
src/cljs/commiteth/bounties.cljs
Normal file
6
src/cljs/commiteth/bounties.cljs
Normal file
@ -0,0 +1,6 @@
|
||||
(ns commiteth.bounties
|
||||
(:require [re-frame.core :as rf]))
|
||||
|
||||
(defn bounties-page []
|
||||
(fn []
|
||||
[:div "Bounties view not implemented"]))
|
@ -10,6 +10,8 @@
|
||||
[commiteth.handlers]
|
||||
[commiteth.subscriptions]
|
||||
[commiteth.activity :refer [activity-page]]
|
||||
[commiteth.repos :refer [repos-page]]
|
||||
[commiteth.bounties :refer [bounties-page]]
|
||||
[commiteth.manage :refer [manage-page]]
|
||||
[commiteth.issues :refer [issues-page]]
|
||||
[commiteth.common :refer [input checkbox]]
|
||||
@ -19,7 +21,7 @@
|
||||
[re-frisk.core :refer [enable-re-frisk!]])
|
||||
(:import goog.History))
|
||||
|
||||
(defn error-pane
|
||||
#_(defn error-pane
|
||||
[]
|
||||
(let [error (rf/subscribe [:error])]
|
||||
(fn []
|
||||
@ -31,12 +33,12 @@
|
||||
:on-click #(rf/dispatch [:clear-error])}
|
||||
(str @error)]))))
|
||||
|
||||
(defn save-address
|
||||
#_(defn save-address
|
||||
[user-id address]
|
||||
(fn [_]
|
||||
(rf/dispatch [:save-user-address user-id address])))
|
||||
|
||||
(defn address-settings []
|
||||
#_(defn address-settings []
|
||||
(let [user (rf/subscribe [:user])
|
||||
user-id (:id @user)
|
||||
address (rf/subscribe [:get-in [:user :address]])]
|
||||
@ -92,8 +94,8 @@
|
||||
(fn []
|
||||
(let [tabs (apply conj [[:activity "Activity"]]
|
||||
(when @user
|
||||
[[:issues "Bounties"]
|
||||
[:manage "Repositories"]]))]
|
||||
[[:repos "Repositories"]
|
||||
[:bounties "Bounties"]]))]
|
||||
(into [:div.ui.attached.tabular.menu.tiny]
|
||||
(for [[page caption] tabs]
|
||||
(let [props {:class (str "ui item"
|
||||
@ -122,16 +124,17 @@
|
||||
|
||||
(def pages
|
||||
{:activity #'activity-page
|
||||
:issues #'issues-page
|
||||
:manage #'manage-page})
|
||||
:repos #'repos-page
|
||||
:bounties #'bounties-page})
|
||||
|
||||
(defn page []
|
||||
(fn []
|
||||
[:div.ui.pusher
|
||||
[page-header]
|
||||
[error-pane]
|
||||
[:div.ui.vertical.segment
|
||||
[(pages @(rf/subscribe [:page]))]]]))
|
||||
;; [error-pane]
|
||||
[:div.ui.vertical.segment.foo
|
||||
[:div.page-content
|
||||
[(pages @(rf/subscribe [:page]))]]]]))
|
||||
|
||||
(secretary/set-config! :prefix "#")
|
||||
|
||||
@ -140,7 +143,7 @@
|
||||
|
||||
(secretary/defroute "/manage" []
|
||||
(if js/user
|
||||
(rf/dispatch [:set-active-page :manage])
|
||||
(rf/dispatch [:set-active-page :repos])
|
||||
(secretary/dispatch! "/")))
|
||||
|
||||
(defn hook-browser-navigation! []
|
||||
@ -154,12 +157,18 @@
|
||||
(defn mount-components []
|
||||
(r/render [#'page] (.getElementById js/document "app")))
|
||||
|
||||
(defonce active-user (r/atom nil))
|
||||
|
||||
(defn load-user []
|
||||
(when-let [login js/user]
|
||||
(if-let [login js/user]
|
||||
(when-not (= login @active-user)
|
||||
(println "active user changed, loading user data")
|
||||
(reset! active-user login)
|
||||
(rf/dispatch [:set-active-user
|
||||
{:login login
|
||||
:id (js/parseInt js/userId)
|
||||
:token js/token}])))
|
||||
:token js/token}]))
|
||||
(reset! active-user nil)))
|
||||
|
||||
(defn load-issues []
|
||||
(rf/dispatch [:load-bounties]))
|
||||
@ -168,7 +177,13 @@
|
||||
(load-issues)
|
||||
(load-user))
|
||||
|
||||
(js/setInterval load-data 60000)
|
||||
(defonce timer-id (r/atom nil))
|
||||
|
||||
(defn on-js-load []
|
||||
(when-not (nil? @timer-id)
|
||||
(js/clearInterval @timer-id))
|
||||
(reset! timer-id (js/setInterval load-data 60000))
|
||||
(mount-components))
|
||||
|
||||
(defn init! []
|
||||
(rf/dispatch-sync [:initialize-db])
|
||||
@ -177,4 +192,4 @@
|
||||
(load-interceptors!)
|
||||
(hook-browser-navigation!)
|
||||
(load-data)
|
||||
(mount-components))
|
||||
(on-js-load))
|
||||
|
@ -1,11 +1,10 @@
|
||||
(ns commiteth.db)
|
||||
|
||||
(def default-db
|
||||
{:page :issues
|
||||
{:page :activity
|
||||
:user nil
|
||||
:repos-loading? false
|
||||
:repos []
|
||||
:enabled-repos {}
|
||||
:repos {}
|
||||
:all-bounties []
|
||||
:owner-bounties []
|
||||
:error nil
|
||||
|
@ -6,10 +6,12 @@
|
||||
|
||||
(reg-fx
|
||||
:http
|
||||
(fn [{:keys [method url on-success params]}]
|
||||
(fn [{:keys [method url on-success on-error finally params]}]
|
||||
(method url
|
||||
{:headers {"Accept" "application/transit+json"}
|
||||
:handler on-success
|
||||
:error-handler on-error
|
||||
:finally finally
|
||||
:params params})))
|
||||
|
||||
(reg-event-db
|
||||
@ -56,7 +58,6 @@
|
||||
{:db (assoc db :user user)
|
||||
:dispatch-n [[:load-user-profile]
|
||||
[:load-user-repos]
|
||||
[:load-enabled-repos]
|
||||
[:load-owner-bounties]]}))
|
||||
|
||||
(reg-event-fx
|
||||
@ -130,29 +131,45 @@
|
||||
:url "/api/user/repositories"
|
||||
:on-success #(dispatch [:set-user-repos (:repositories %)])}}))
|
||||
|
||||
(reg-event-db
|
||||
:set-enabled-repos
|
||||
(fn [db [_ repos]]
|
||||
(assoc db :enabled-repos (zipmap repos (repeat true)))))
|
||||
|
||||
(reg-event-fx
|
||||
:load-enabled-repos
|
||||
(fn [{:keys [db]} [_]]
|
||||
{:db db
|
||||
:http {:method GET
|
||||
:url "/api/user/enabled-repositories"
|
||||
:on-success #(dispatch [:set-enabled-repos %])}}))
|
||||
(defn update-repo-state [all-repos full-name data]
|
||||
(let [[owner repo-name] (js->clj (.split full-name "/"))]
|
||||
(println "update-repo-busy-state" owner repo-name)
|
||||
(update all-repos
|
||||
owner
|
||||
(fn [repos] (map (fn [repo] (if (= (:name repo) repo-name)
|
||||
(assoc repo
|
||||
:busy? (:busy? data)
|
||||
:enabled (:enabled data))
|
||||
repo))
|
||||
repos)))))
|
||||
|
||||
(reg-event-fx
|
||||
:toggle-repo
|
||||
(fn [{:keys [db]} [_ repo]]
|
||||
(println "toggle-repo" repo)
|
||||
{:db db
|
||||
(println repo)
|
||||
{:db (assoc db :repos (update-repo-state (:repos db) (:full_name repo) {:busy? true
|
||||
:enabled (:enabled repo)}))
|
||||
:http {:method POST
|
||||
:url "/api/user/repository/toggle"
|
||||
:on-success #(println %)
|
||||
:on-success #(dispatch [:repo-toggle-success %])
|
||||
;; TODO :on-error #(dispatch [:repo-toggle-error %])
|
||||
:finally #(println "finally" %)
|
||||
:params (select-keys repo [:id :login :full_name :name])}}))
|
||||
|
||||
|
||||
|
||||
|
||||
(reg-event-db
|
||||
:repo-toggle-success
|
||||
(fn [db [_ repo]]
|
||||
(println "repo-toggle-success" repo)
|
||||
(assoc db :repos (update-repo-state (:repos db)
|
||||
(:full_name repo)
|
||||
{:busy? false
|
||||
:enabled (:enabled repo)} ))))
|
||||
|
||||
|
||||
(reg-event-fx
|
||||
:save-user-address
|
||||
(fn [{:keys [db]} [_ user-id address]]
|
||||
|
@ -4,6 +4,10 @@
|
||||
[commiteth.issues :refer [issues-list-table issue-url]]
|
||||
[clojure.set :refer [rename-keys]]))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
(defn repository-row [repo]
|
||||
(let [{repo-id :id
|
||||
url :html_url
|
||||
|
60
src/cljs/commiteth/repos.cljs
Normal file
60
src/cljs/commiteth/repos.cljs
Normal file
@ -0,0 +1,60 @@
|
||||
(ns commiteth.repos
|
||||
(:require [re-frame.core :as rf]))
|
||||
|
||||
|
||||
(defn repo-toggle-button [enabled busy on-click]
|
||||
(let [add-busy-styles (fn [x] (conj x (when busy {:class (str "busy loading")})))
|
||||
button (if enabled
|
||||
[:div.ui.button.small.repo-added-button (add-busy-styles {})
|
||||
[:i.icon.check]
|
||||
"Added"]
|
||||
[:div.ui.button.small
|
||||
(add-busy-styles {:on-click on-click})
|
||||
"Add"])]
|
||||
|
||||
[:div.ui.two.column.container
|
||||
button
|
||||
(when enabled
|
||||
[:a.ui.item.remove-link {:on-click on-click} "Remove"])]))
|
||||
|
||||
|
||||
(defn repo-card [repo]
|
||||
[:div.ui.card
|
||||
[:div.content
|
||||
[:div.repo-label (:full_name repo)]
|
||||
[:div.repo-description (:description repo)]]
|
||||
[:div.repo-button-container
|
||||
[repo-toggle-button
|
||||
(:enabled repo)
|
||||
(:busy? repo)
|
||||
#(rf/dispatch [:toggle-repo repo])]]])
|
||||
|
||||
(defn repo-group-title [group login]
|
||||
[:h3
|
||||
(if (= group login)
|
||||
"Personal repositories"
|
||||
group)])
|
||||
|
||||
|
||||
(defn repos-list []
|
||||
(let [repos (rf/subscribe [:repos])
|
||||
user (rf/subscribe [:user])
|
||||
repo-groups (keys @repos)]
|
||||
(fn []
|
||||
(into [:div.ui.container]
|
||||
(for [[group group-repos]
|
||||
(map (fn [group] [group (get @repos group)]) repo-groups)]
|
||||
[:div [repo-group-title group (:login @user)]
|
||||
(into [:div.ui.cards]
|
||||
(map repo-card group-repos))])))))
|
||||
|
||||
|
||||
(defn repos-page []
|
||||
(let [repos-loading? (rf/subscribe [:repos-loading?])]
|
||||
(fn []
|
||||
(if @repos-loading?
|
||||
[:div.ui.container
|
||||
[:p]
|
||||
[:div.ui.active.dimmer
|
||||
[:div.ui.loader]]]
|
||||
[repos-list]))))
|
@ -5,11 +5,22 @@
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'postgrotesk-medium';
|
||||
src: url('/fonts/PostGrotesk-Medium.woff') format('woff');
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
|
||||
.ui.button {
|
||||
background-color: #b0f1ee;
|
||||
white-space: nowrap;
|
||||
padding: 1em 1.2em 1em;
|
||||
}
|
||||
.ui.small.button {
|
||||
font-size: 15px!important;
|
||||
}
|
||||
|
||||
|
||||
.commiteth-header {
|
||||
background-color: #2f3f44!important;
|
||||
@ -27,7 +38,6 @@
|
||||
}
|
||||
|
||||
.ui.header {
|
||||
//font-weight: normal;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@ -62,13 +72,17 @@ span.dropdown.icon {
|
||||
font-family: 'postgrotesk';
|
||||
}
|
||||
|
||||
.ui.tabular {
|
||||
.ui.attached.tabular {
|
||||
background-color: #2f3f44;
|
||||
border-bottom: none;
|
||||
.ui.item {
|
||||
padding: 1.5em;
|
||||
padding: 1em;
|
||||
color: #fff;
|
||||
opacity: 0.98;
|
||||
cursor: pointer;
|
||||
font-family: 'postgrotesk';
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
}
|
||||
.ui.item.active {
|
||||
border-radius: .8rem .8rem 0 0!important;
|
||||
@ -79,24 +93,52 @@ span.dropdown.icon {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.repo-label {
|
||||
color: #1bb5c1;
|
||||
font-weight: bold;
|
||||
font-family: 'postgrotesk-medium';
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.repo-description {
|
||||
color: #474951;
|
||||
font-size: 15px;
|
||||
font-weight: normal;
|
||||
line-height: 24px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.repo-button-container {
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
padding: 0em 1em 0em;
|
||||
font-family: 'postgrotesk-medium';
|
||||
font-size: 15px;
|
||||
a {
|
||||
padding: 1em;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.page-content {
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
|
||||
.ui.button.repo-added-button {
|
||||
color: white;
|
||||
background-color: #61deb0;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.ui.cards>.card {
|
||||
border: #e7e7e7 solid 0.1em;
|
||||
box-shadow: none;
|
||||
border-radius: 0.3em;
|
||||
padding: 0.8em 1em 1.1em;
|
||||
|
||||
}
|
||||
.ui.segment {
|
||||
// margin: 2.5em;
|
||||
// line-height: 3em;
|
||||
border: #e7e7e7 solid 0.1em;
|
||||
box-shadow: none;
|
||||
border-radius: 0.3em;
|
||||
|
Loading…
x
Reference in New Issue
Block a user