Added a boilerplate luminus project
+re-frame +swagger +oauth - probably this is massive overkill
This commit is contained in:
parent
94abf7aae8
commit
bad860f78e
42 changed files with 1132 additions and 0 deletions
12
src/clj/ireadit/config.clj
Normal file
12
src/clj/ireadit/config.clj
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
(ns ireadit.config
|
||||
(:require [cprop.core :refer [load-config]]
|
||||
[cprop.source :as source]
|
||||
[mount.core :refer [args defstate]]))
|
||||
|
||||
(defstate env
|
||||
:start
|
||||
(load-config
|
||||
:merge
|
||||
[(args)
|
||||
(source/from-system-props)
|
||||
(source/from-env)]))
|
||||
49
src/clj/ireadit/core.clj
Normal file
49
src/clj/ireadit/core.clj
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
(ns ireadit.core
|
||||
(:require [ireadit.handler :as handler]
|
||||
[ireadit.nrepl :as nrepl]
|
||||
[luminus.http-server :as http]
|
||||
[ireadit.config :refer [env]]
|
||||
[clojure.tools.cli :refer [parse-opts]]
|
||||
[clojure.tools.logging :as log]
|
||||
[mount.core :as mount])
|
||||
(:gen-class))
|
||||
|
||||
(def cli-options
|
||||
[["-p" "--port PORT" "Port number"
|
||||
:parse-fn #(Integer/parseInt %)]])
|
||||
|
||||
(mount/defstate ^{:on-reload :noop} http-server
|
||||
:start
|
||||
(http/start
|
||||
(-> env
|
||||
(assoc :handler #'handler/app)
|
||||
(update :io-threads #(or % (* 2 (.availableProcessors (Runtime/getRuntime)))))
|
||||
(update :port #(or (-> env :options :port) %))))
|
||||
:stop
|
||||
(http/stop http-server))
|
||||
|
||||
(mount/defstate ^{:on-reload :noop} repl-server
|
||||
:start
|
||||
(when (env :nrepl-port)
|
||||
(nrepl/start {:bind (env :nrepl-bind)
|
||||
:port (env :nrepl-port)}))
|
||||
:stop
|
||||
(when repl-server
|
||||
(nrepl/stop repl-server)))
|
||||
|
||||
|
||||
(defn stop-app []
|
||||
(doseq [component (:stopped (mount/stop))]
|
||||
(log/info component "stopped"))
|
||||
(shutdown-agents))
|
||||
|
||||
(defn start-app [args]
|
||||
(doseq [component (-> args
|
||||
(parse-opts cli-options)
|
||||
mount/start-with-args
|
||||
:started)]
|
||||
(log/info component "started"))
|
||||
(.addShutdownHook (Runtime/getRuntime) (Thread. stop-app)))
|
||||
|
||||
(defn -main [& args]
|
||||
(start-app args))
|
||||
30
src/clj/ireadit/handler.clj
Normal file
30
src/clj/ireadit/handler.clj
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
(ns ireadit.handler
|
||||
(:require [ireadit.middleware :as middleware]
|
||||
[ireadit.layout :refer [error-page]]
|
||||
[ireadit.routes.home :refer [home-routes]]
|
||||
[ireadit.routes.services :refer [service-routes]]
|
||||
[ireadit.routes.oauth :refer [oauth-routes]]
|
||||
[compojure.core :refer [routes wrap-routes]]
|
||||
[ring.util.http-response :as response]
|
||||
[compojure.route :as route]
|
||||
[ireadit.env :refer [defaults]]
|
||||
[mount.core :as mount]))
|
||||
|
||||
(mount/defstate init-app
|
||||
:start ((or (:init defaults) identity))
|
||||
:stop ((or (:stop defaults) identity)))
|
||||
|
||||
(mount/defstate app
|
||||
:start
|
||||
(middleware/wrap-base
|
||||
(routes
|
||||
(-> #'home-routes
|
||||
(wrap-routes middleware/wrap-csrf)
|
||||
(wrap-routes middleware/wrap-formats))
|
||||
#'oauth-routes
|
||||
#'service-routes
|
||||
(route/not-found
|
||||
(:body
|
||||
(error-page {:status 404
|
||||
:title "page not found"}))))))
|
||||
|
||||
37
src/clj/ireadit/layout.clj
Normal file
37
src/clj/ireadit/layout.clj
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
(ns ireadit.layout
|
||||
(:require [selmer.parser :as parser]
|
||||
[selmer.filters :as filters]
|
||||
[markdown.core :refer [md-to-html-string]]
|
||||
[ring.util.http-response :refer [content-type ok]]
|
||||
[ring.util.anti-forgery :refer [anti-forgery-field]]
|
||||
[ring.middleware.anti-forgery :refer [*anti-forgery-token*]]))
|
||||
|
||||
|
||||
(parser/set-resource-path! (clojure.java.io/resource "html"))
|
||||
(parser/add-tag! :csrf-field (fn [_ _] (anti-forgery-field)))
|
||||
(filters/add-filter! :markdown (fn [content] [:safe (md-to-html-string content)]))
|
||||
|
||||
(defn render
|
||||
"renders the HTML template located relative to resources/html"
|
||||
[template & [params]]
|
||||
(content-type
|
||||
(ok
|
||||
(parser/render-file
|
||||
template
|
||||
(assoc params
|
||||
:page template
|
||||
:csrf-token *anti-forgery-token*)))
|
||||
"text/html; charset=utf-8"))
|
||||
|
||||
(defn error-page
|
||||
"error-details should be a map containing the following keys:
|
||||
:status - error status
|
||||
:title - error title (optional)
|
||||
:message - detailed error message (optional)
|
||||
|
||||
returns a response map with the error page as the body
|
||||
and the status specified by the status key"
|
||||
[error-details]
|
||||
{:status (:status error-details)
|
||||
:headers {"Content-Type" "text/html; charset=utf-8"}
|
||||
:body (parser/render-file "error.html" error-details)})
|
||||
53
src/clj/ireadit/middleware.clj
Normal file
53
src/clj/ireadit/middleware.clj
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
(ns ireadit.middleware
|
||||
(:require [ireadit.env :refer [defaults]]
|
||||
[cheshire.generate :as cheshire]
|
||||
[cognitect.transit :as transit]
|
||||
[clojure.tools.logging :as log]
|
||||
[ireadit.layout :refer [error-page]]
|
||||
[ring.middleware.anti-forgery :refer [wrap-anti-forgery]]
|
||||
[ring.middleware.webjars :refer [wrap-webjars]]
|
||||
[ireadit.middleware.formats :as formats]
|
||||
[muuntaja.middleware :refer [wrap-format wrap-params]]
|
||||
[ireadit.config :refer [env]]
|
||||
[ring.middleware.flash :refer [wrap-flash]]
|
||||
[immutant.web.middleware :refer [wrap-session]]
|
||||
[ring.middleware.defaults :refer [site-defaults wrap-defaults]])
|
||||
(:import
|
||||
))
|
||||
|
||||
(defn wrap-internal-error [handler]
|
||||
(fn [req]
|
||||
(try
|
||||
(handler req)
|
||||
(catch Throwable t
|
||||
(log/error t (.getMessage t))
|
||||
(error-page {:status 500
|
||||
:title "Something very bad has happened!"
|
||||
:message "We've dispatched a team of highly trained gnomes to take care of the problem."})))))
|
||||
|
||||
(defn wrap-csrf [handler]
|
||||
(wrap-anti-forgery
|
||||
handler
|
||||
{:error-response
|
||||
(error-page
|
||||
{:status 403
|
||||
:title "Invalid anti-forgery token"})}))
|
||||
|
||||
|
||||
(defn wrap-formats [handler]
|
||||
(let [wrapped (-> handler wrap-params (wrap-format formats/instance))]
|
||||
(fn [request]
|
||||
;; disable wrap-formats for websockets
|
||||
;; since they're not compatible with this middleware
|
||||
((if (:websocket? request) handler wrapped) request))))
|
||||
|
||||
(defn wrap-base [handler]
|
||||
(-> ((:middleware defaults) handler)
|
||||
wrap-webjars
|
||||
wrap-flash
|
||||
(wrap-session {:cookie-attrs {:http-only true}})
|
||||
(wrap-defaults
|
||||
(-> site-defaults
|
||||
(assoc-in [:security :anti-forgery] false)
|
||||
(dissoc :session)))
|
||||
wrap-internal-error))
|
||||
14
src/clj/ireadit/middleware/formats.clj
Normal file
14
src/clj/ireadit/middleware/formats.clj
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
(ns ireadit.middleware.formats
|
||||
(:require [cognitect.transit :as transit]
|
||||
[luminus-transit.time :as time]
|
||||
[muuntaja.core :as m]))
|
||||
|
||||
(def instance
|
||||
(m/create
|
||||
(-> m/default-options
|
||||
(update-in
|
||||
[:formats "application/transit+json" :decoder-opts]
|
||||
(partial merge time/time-deserialization-handlers))
|
||||
(update-in
|
||||
[:formats "application/transit+json" :encoder-opts]
|
||||
(partial merge time/time-serialization-handlers)))))
|
||||
26
src/clj/ireadit/nrepl.clj
Normal file
26
src/clj/ireadit/nrepl.clj
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
(ns ireadit.nrepl
|
||||
(:require [nrepl.server :as nrepl]
|
||||
[clojure.tools.logging :as log]))
|
||||
|
||||
(defn start
|
||||
"Start a network repl for debugging on specified port followed by
|
||||
an optional parameters map. The :bind, :transport-fn, :handler,
|
||||
:ack-port and :greeting-fn will be forwarded to
|
||||
clojure.tools.nrepl.server/start-server as they are."
|
||||
[{:keys [port bind transport-fn handler ack-port greeting-fn]}]
|
||||
(try
|
||||
(log/info "starting nREPL server on port" port)
|
||||
(nrepl/start-server :port port
|
||||
:bind bind
|
||||
:transport-fn transport-fn
|
||||
:handler handler
|
||||
:ack-port ack-port
|
||||
:greeting-fn greeting-fn)
|
||||
|
||||
(catch Throwable t
|
||||
(log/error t "failed to start nREPL")
|
||||
(throw t))))
|
||||
|
||||
(defn stop [server]
|
||||
(nrepl/stop-server server)
|
||||
(log/info "nREPL server stopped"))
|
||||
35
src/clj/ireadit/oauth.clj
Normal file
35
src/clj/ireadit/oauth.clj
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
(ns ireadit.oauth
|
||||
(:require [ireadit.config :refer [env]]
|
||||
[oauth.client :as oauth]
|
||||
[mount.core :refer [defstate]]
|
||||
[clojure.tools.logging :as log]))
|
||||
|
||||
(defstate consumer
|
||||
:start (oauth/make-consumer
|
||||
(env :oauth-consumer-key)
|
||||
(env :oauth-consumer-secret)
|
||||
(env :request-token-uri)
|
||||
(env :access-token-uri)
|
||||
(env :authorize-uri)
|
||||
:hmac-sha1))
|
||||
|
||||
(defn oauth-callback-uri
|
||||
"Generates the oauth request callback URI"
|
||||
[{:keys [headers]}]
|
||||
(str (headers "x-forwarded-proto") "://" (headers "host") "/oauth/oauth-callback"))
|
||||
|
||||
(defn fetch-request-token
|
||||
"Fetches a request token."
|
||||
[request]
|
||||
(let [callback-uri (oauth-callback-uri request)]
|
||||
(log/info "Fetching request token using callback-uri" callback-uri)
|
||||
(oauth/request-token consumer (oauth-callback-uri request))))
|
||||
|
||||
(defn fetch-access-token
|
||||
[request_token]
|
||||
(oauth/access-token consumer request_token (:oauth_verifier request_token)))
|
||||
|
||||
(defn auth-redirect-uri
|
||||
"Gets the URI the user should be redirected to when authenticating."
|
||||
[request-token]
|
||||
(str (oauth/user-approval-uri consumer request-token)))
|
||||
16
src/clj/ireadit/routes/home.clj
Normal file
16
src/clj/ireadit/routes/home.clj
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
(ns ireadit.routes.home
|
||||
(:require [ireadit.layout :as layout]
|
||||
[compojure.core :refer [defroutes GET]]
|
||||
[ring.util.http-response :as response]
|
||||
[clojure.java.io :as io]))
|
||||
|
||||
(defn home-page []
|
||||
(layout/render "home.html"))
|
||||
|
||||
(defroutes home-routes
|
||||
(GET "/" []
|
||||
(home-page))
|
||||
(GET "/docs" []
|
||||
(-> (response/ok (-> "docs/docs.md" io/resource slurp))
|
||||
(response/header "Content-Type" "text/plain; charset=utf-8"))))
|
||||
|
||||
34
src/clj/ireadit/routes/oauth.clj
Normal file
34
src/clj/ireadit/routes/oauth.clj
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
(ns ireadit.routes.oauth
|
||||
(:require [ring.util.http-response :refer [ok found]]
|
||||
[compojure.core :refer [defroutes GET]]
|
||||
[clojure.java.io :as io]
|
||||
[ireadit.oauth :as oauth]
|
||||
[clojure.tools.logging :as log]))
|
||||
|
||||
(defn oauth-init
|
||||
"Initiates the Twitter OAuth"
|
||||
[request]
|
||||
(-> (oauth/fetch-request-token request)
|
||||
:oauth_token
|
||||
oauth/auth-redirect-uri
|
||||
found))
|
||||
|
||||
(defn oauth-callback
|
||||
"Handles the callback from Twitter."
|
||||
[{:keys [session params]}]
|
||||
; oauth request was denied by user
|
||||
(if (:denied params)
|
||||
(-> (found "/")
|
||||
(assoc :flash {:denied true}))
|
||||
; fetch the request token and do anything else you wanna do if not denied.
|
||||
(let [{:keys [user_id screen_name]} (oauth/fetch-access-token params)]
|
||||
(log/info "successfully authenticated as" user_id screen_name)
|
||||
(-> (found "/")
|
||||
(assoc :session
|
||||
(assoc session :user-id user_id :screen-name screen_name))))))
|
||||
|
||||
|
||||
(defroutes oauth-routes
|
||||
(GET "/oauth/oauth-init" req (oauth-init req))
|
||||
(GET "/oauth/oauth-callback" [& req_token :as req] (oauth-callback req)))
|
||||
|
||||
45
src/clj/ireadit/routes/services.clj
Normal file
45
src/clj/ireadit/routes/services.clj
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
(ns ireadit.routes.services
|
||||
(:require [ring.util.http-response :refer :all]
|
||||
[compojure.api.sweet :refer :all]
|
||||
[schema.core :as s]))
|
||||
|
||||
(def service-routes
|
||||
(api
|
||||
{:swagger {:ui "/swagger-ui"
|
||||
:spec "/swagger.json"
|
||||
:data {:info {:version "1.0.0"
|
||||
:title "Sample API"
|
||||
:description "Sample Services"}}}}
|
||||
|
||||
(context "/api" []
|
||||
:tags ["thingie"]
|
||||
|
||||
(GET "/plus" []
|
||||
:return Long
|
||||
:query-params [x :- Long, {y :- Long 1}]
|
||||
:summary "x+y with query-parameters. y defaults to 1."
|
||||
(ok (+ x y)))
|
||||
|
||||
(POST "/minus" []
|
||||
:return Long
|
||||
:body-params [x :- Long, y :- Long]
|
||||
:summary "x-y with body-parameters."
|
||||
(ok (- x y)))
|
||||
|
||||
(GET "/times/:x/:y" []
|
||||
:return Long
|
||||
:path-params [x :- Long, y :- Long]
|
||||
:summary "x*y with path-parameters"
|
||||
(ok (* x y)))
|
||||
|
||||
(POST "/divide" []
|
||||
:return Double
|
||||
:form-params [x :- Long, y :- Long]
|
||||
:summary "x/y with form-parameters"
|
||||
(ok (/ x y)))
|
||||
|
||||
(GET "/power" []
|
||||
:return Long
|
||||
:header-params [x :- Long, y :- Long]
|
||||
:summary "x^y with header-parameters"
|
||||
(ok (long (Math/pow x y)))))))
|
||||
2
src/cljc/ireadit/validation.cljc
Normal file
2
src/cljc/ireadit/validation.cljc
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
(ns ireadit.validation
|
||||
(:require [struct.core :as st]))
|
||||
29
src/cljs/ireadit/ajax.cljs
Normal file
29
src/cljs/ireadit/ajax.cljs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
(ns ireadit.ajax
|
||||
(:require [ajax.core :as ajax]
|
||||
[luminus-transit.time :as time]
|
||||
[cognitect.transit :as transit]
|
||||
[re-frame.core :as rf]))
|
||||
|
||||
(defn local-uri? [{:keys [uri]}]
|
||||
(not (re-find #"^\w+?://" uri)))
|
||||
|
||||
(defn default-headers [request]
|
||||
(if (local-uri? request)
|
||||
(-> request
|
||||
(update :headers #(merge {"x-csrf-token" js/csrfToken} %)))
|
||||
request))
|
||||
|
||||
;; injects transit serialization config into request options
|
||||
(defn as-transit [opts]
|
||||
(merge {:raw false
|
||||
:format :transit
|
||||
:response-format :transit
|
||||
:reader (transit/reader :json time/time-deserialization-handlers)
|
||||
:writer (transit/writer :json time/time-serialization-handlers)}
|
||||
opts))
|
||||
|
||||
(defn load-interceptors! []
|
||||
(swap! ajax/default-interceptors
|
||||
conj
|
||||
(ajax/to-interceptor {:name "default headers"
|
||||
:request default-headers})))
|
||||
94
src/cljs/ireadit/core.cljs
Normal file
94
src/cljs/ireadit/core.cljs
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
(ns ireadit.core
|
||||
(:require [baking-soda.core :as b]
|
||||
[day8.re-frame.http-fx]
|
||||
[reagent.core :as r]
|
||||
[re-frame.core :as rf]
|
||||
[goog.events :as events]
|
||||
[goog.history.EventType :as HistoryEventType]
|
||||
[markdown.core :refer [md->html]]
|
||||
[ireadit.ajax :as ajax]
|
||||
[ireadit.events]
|
||||
[secretary.core :as secretary])
|
||||
(:import goog.History))
|
||||
|
||||
; the navbar components are implemented via baking-soda [1]
|
||||
; library that provides a ClojureScript interface for Reactstrap [2]
|
||||
; Bootstrap 4 components.
|
||||
; [1] https://github.com/gadfly361/baking-soda
|
||||
; [2] http://reactstrap.github.io/
|
||||
|
||||
(defn nav-link [uri title page]
|
||||
[b/NavItem
|
||||
[b/NavLink
|
||||
{:href uri
|
||||
:active (when (= page @(rf/subscribe [:page])) "active")}
|
||||
title]])
|
||||
|
||||
(defn navbar []
|
||||
(r/with-let [expanded? (r/atom true)]
|
||||
[b/Navbar {:light true
|
||||
:class-name "navbar-dark bg-primary"
|
||||
:expand "md"}
|
||||
[b/NavbarBrand {:href "/"} "ireadit"]
|
||||
[b/NavbarToggler {:on-click #(swap! expanded? not)}]
|
||||
[b/Collapse {:is-open @expanded? :navbar true}
|
||||
[b/Nav {:class-name "mr-auto" :navbar true}
|
||||
[nav-link "#/" "Home" :home]
|
||||
[nav-link "#/about" "About" :about]]]]))
|
||||
|
||||
(defn about-page []
|
||||
[:div.container
|
||||
[:div.row
|
||||
[:div.col-md-12
|
||||
[:img {:src "/img/warning_clojure.png"}]]]])
|
||||
|
||||
(defn home-page []
|
||||
[:div.container
|
||||
(when-let [docs @(rf/subscribe [:docs])]
|
||||
[:div.row>div.col-sm-12
|
||||
[:div {:dangerouslySetInnerHTML
|
||||
{:__html (md->html docs)}}]])])
|
||||
|
||||
(def pages
|
||||
{:home #'home-page
|
||||
:about #'about-page})
|
||||
|
||||
(defn page []
|
||||
[:div
|
||||
[navbar]
|
||||
[(pages @(rf/subscribe [:page]))]])
|
||||
|
||||
;; -------------------------
|
||||
;; Routes
|
||||
|
||||
(secretary/set-config! :prefix "#")
|
||||
|
||||
(secretary/defroute "/" []
|
||||
(rf/dispatch [:navigate :home]))
|
||||
|
||||
(secretary/defroute "/about" []
|
||||
(rf/dispatch [:navigate :about]))
|
||||
|
||||
;; -------------------------
|
||||
;; History
|
||||
;; must be called after routes have been defined
|
||||
(defn hook-browser-navigation! []
|
||||
(doto (History.)
|
||||
(events/listen
|
||||
HistoryEventType/NAVIGATE
|
||||
(fn [event]
|
||||
(secretary/dispatch! (.-token event))))
|
||||
(.setEnabled true)))
|
||||
|
||||
;; -------------------------
|
||||
;; Initialize app
|
||||
(defn mount-components []
|
||||
(rf/clear-subscription-cache!)
|
||||
(r/render [#'page] (.getElementById js/document "app")))
|
||||
|
||||
(defn init! []
|
||||
(rf/dispatch-sync [:navigate :home])
|
||||
(ajax/load-interceptors!)
|
||||
(rf/dispatch [:fetch-docs])
|
||||
(hook-browser-navigation!)
|
||||
(mount-components))
|
||||
45
src/cljs/ireadit/events.cljs
Normal file
45
src/cljs/ireadit/events.cljs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
(ns ireadit.events
|
||||
(:require [re-frame.core :as rf]
|
||||
[ajax.core :as ajax]))
|
||||
|
||||
;;dispatchers
|
||||
|
||||
(rf/reg-event-db
|
||||
:navigate
|
||||
(fn [db [_ page]]
|
||||
(assoc db :page page)))
|
||||
|
||||
(rf/reg-event-db
|
||||
:set-docs
|
||||
(fn [db [_ docs]]
|
||||
(assoc db :docs docs)))
|
||||
|
||||
(rf/reg-event-fx
|
||||
:fetch-docs
|
||||
(fn [_ _]
|
||||
{:http-xhrio {:method :get
|
||||
:uri "/docs"
|
||||
:response-format (ajax/raw-response-format)
|
||||
:on-success [:set-docs]}}))
|
||||
|
||||
(rf/reg-event-db
|
||||
:common/set-error
|
||||
(fn [db [_ error]]
|
||||
(assoc db :common/error error)))
|
||||
|
||||
;;subscriptions
|
||||
|
||||
(rf/reg-sub
|
||||
:page
|
||||
(fn [db _]
|
||||
(:page db)))
|
||||
|
||||
(rf/reg-sub
|
||||
:docs
|
||||
(fn [db _]
|
||||
(:docs db)))
|
||||
|
||||
(rf/reg-sub
|
||||
:common/error
|
||||
(fn [db _]
|
||||
(:common/error db)))
|
||||
Loading…
Add table
Add a link
Reference in a new issue