commit c4bab5a6eda842705a96086f22b0fe40039cdff1 Author: kagel Date: Sun Aug 21 00:36:09 2016 +0300 Luminus skeleton + useless github button diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9366d06 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +/target +/lib +/classes +/checkouts +pom.xml +*.jar +*.class +/.lein-* +profiles.clj +/.env +.nrepl-port +/log +.idea +*.iml +*.ipr +/resources/public/css/screen.css +*.log diff --git a/Capstanfile b/Capstanfile new file mode 100644 index 0000000..486eaea --- /dev/null +++ b/Capstanfile @@ -0,0 +1,28 @@ + +# +# Name of the base image. Capstan will download this automatically from +# Cloudius S3 repository. +# +#base: cloudius/osv +base: cloudius/osv-openjdk8 + +# +# The command line passed to OSv to start up the application. +# +cmdline: /java.so -jar /commiteth/app.jar + +# +# The command to use to build the application. +# You can use any build tool/command (make/rake/lein/boot) - this runs locally on your machine +# +# For Leiningen, you can use: +#build: lein uberjar +# For Boot, you can use: +#build: boot build + +# +# List of files that are included in the generated image. +# +files: + /commiteth/app.jar: ./target/uberjar/commiteth.jar + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..35724ae --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM java:8-alpine +MAINTAINER Your Name + +ADD target/uberjar/commiteth.jar /commiteth/app.jar + +EXPOSE 3000 + +CMD ["java", "-jar", "/commiteth/app.jar"] diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..1133927 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: java $JVM_OPTS -cp target/uberjar/commiteth.jar clojure.main -m commiteth.core diff --git a/README.md b/README.md new file mode 100644 index 0000000..37e05cf --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# commiteth + +generated using Luminus version "2.9.10.94" + +FIXME + +## Prerequisites + +* You will need [Leiningen][1] 2.0 or above installed. + +* [SassC][2] libsass command-line compiler is required + +[1]: https://github.com/technomancy/leiningen +[2]: http://github.com/sass/sassc + +## Running + + lein run + lein figwheel + lein auto sassc once + +## License + +Copyright © 2016 FIXME diff --git a/env/dev/clj/commiteth/dev_middleware.clj b/env/dev/clj/commiteth/dev_middleware.clj new file mode 100644 index 0000000..c3d9458 --- /dev/null +++ b/env/dev/clj/commiteth/dev_middleware.clj @@ -0,0 +1,10 @@ +(ns commiteth.dev-middleware + (:require [ring.middleware.reload :refer [wrap-reload]] + [selmer.middleware :refer [wrap-error-page]] + [prone.middleware :refer [wrap-exceptions]])) + +(defn wrap-dev [handler] + (-> handler + wrap-reload + wrap-error-page + wrap-exceptions)) diff --git a/env/dev/clj/commiteth/env.clj b/env/dev/clj/commiteth/env.clj new file mode 100644 index 0000000..9191370 --- /dev/null +++ b/env/dev/clj/commiteth/env.clj @@ -0,0 +1,14 @@ +(ns commiteth.env + (:require [selmer.parser :as parser] + [clojure.tools.logging :as log] + [commiteth.dev-middleware :refer [wrap-dev]])) + +(def defaults + {:init + (fn [] + (parser/cache-off!) + (log/info "\n-=[commiteth started successfully using the development profile]=-")) + :stop + (fn [] + (log/info "\n-=[commiteth has shut down successfully]=-")) + :middleware wrap-dev}) diff --git a/env/dev/clj/commiteth/figwheel.clj b/env/dev/clj/commiteth/figwheel.clj new file mode 100644 index 0000000..c8dbb5e --- /dev/null +++ b/env/dev/clj/commiteth/figwheel.clj @@ -0,0 +1,12 @@ +(ns commiteth.figwheel + (:require [figwheel-sidecar.repl-api :as ra])) + +(defn start-fw [] + (ra/start-figwheel!)) + +(defn stop-fw [] + (ra/stop-figwheel!)) + +(defn cljs [] + (ra/cljs-repl)) + diff --git a/env/dev/clj/user.clj b/env/dev/clj/user.clj new file mode 100644 index 0000000..a484a99 --- /dev/null +++ b/env/dev/clj/user.clj @@ -0,0 +1,16 @@ +(ns user + (:require [mount.core :as mount] + [commiteth.figwheel :refer [start-fw stop-fw cljs]] + commiteth.core)) + +(defn start [] + (mount/start-without #'commiteth.core/repl-server)) + +(defn stop [] + (mount/stop-except #'commiteth.core/repl-server)) + +(defn restart [] + (stop) + (start)) + + diff --git a/env/dev/cljs/commiteth/dev.cljs b/env/dev/cljs/commiteth/dev.cljs new file mode 100644 index 0000000..ad14423 --- /dev/null +++ b/env/dev/cljs/commiteth/dev.cljs @@ -0,0 +1,14 @@ +(ns ^:figwheel-no-load commiteth.app + (:require [commiteth.core :as core] + [devtools.core :as devtools] + [figwheel.client :as figwheel :include-macros true])) + +(enable-console-print!) + +(figwheel/watch-and-reload + :websocket-url "ws://localhost:3449/figwheel-ws" + :on-jsload core/mount-components) + +(devtools/install!) + +(core/init!) diff --git a/env/dev/resources/config.edn b/env/dev/resources/config.edn new file mode 100644 index 0000000..dba383f --- /dev/null +++ b/env/dev/resources/config.edn @@ -0,0 +1,6 @@ +{:dev true + :port 3000 + ;; when :nrepl-port is set the application starts the nREPL server on load + :nrepl-port 7000 + :github-access-token "c9323a357c2beeebe4e8d618e0fda47dc9e15f62" + :github-user "kagel"} diff --git a/env/dev/resources/logback.xml b/env/dev/resources/logback.xml new file mode 100644 index 0000000..df633fb --- /dev/null +++ b/env/dev/resources/logback.xml @@ -0,0 +1,39 @@ + + + + + + + UTF-8 + %date{ISO8601} [%thread] %-5level %logger{36} - %msg %n + + + + log/commiteth.log + + log/commiteth.%d{yyyy-MM-dd}.%i.log + + 100MB + + + 30 + + + UTF-8 + %date{ISO8601} [%thread] %-5level %logger{36} - %msg %n + + + + + + + + + + + + + + + diff --git a/env/prod/clj/commiteth/env.clj b/env/prod/clj/commiteth/env.clj new file mode 100644 index 0000000..3260b79 --- /dev/null +++ b/env/prod/clj/commiteth/env.clj @@ -0,0 +1,11 @@ +(ns commiteth.env + (:require [clojure.tools.logging :as log])) + +(def defaults + {:init + (fn [] + (log/info "\n-=[commiteth started successfully]=-")) + :stop + (fn [] + (log/info "\n-=[commiteth has shut down successfully]=-")) + :middleware identity}) diff --git a/env/prod/cljs/commiteth/prod.cljs b/env/prod/cljs/commiteth/prod.cljs new file mode 100644 index 0000000..c5810a9 --- /dev/null +++ b/env/prod/cljs/commiteth/prod.cljs @@ -0,0 +1,7 @@ +(ns commiteth.app + (:require [commiteth.core :as core])) + +;;ignore println statements in prod +(set! *print-fn* (fn [& _])) + +(core/init!) diff --git a/env/prod/resources/config.edn b/env/prod/resources/config.edn new file mode 100644 index 0000000..f8213e5 --- /dev/null +++ b/env/prod/resources/config.edn @@ -0,0 +1,4 @@ +{:production true + :port 3000 + :github-access-token "c9323a357c2beeebe4e8d618e0fda47dc9e15f62" + :github-user "kagel"} diff --git a/env/prod/resources/logback.xml b/env/prod/resources/logback.xml new file mode 100644 index 0000000..978c90c --- /dev/null +++ b/env/prod/resources/logback.xml @@ -0,0 +1,28 @@ + + + + + log/commiteth.log + + log/commiteth.%d{yyyy-MM-dd}.%i.log + + 100MB + + + 30 + + + UTF-8 + %date{ISO8601} [%thread] %-5level %logger{36} - %msg %n + + + + + + + + + + + + diff --git a/env/test/resources/config.edn b/env/test/resources/config.edn new file mode 100644 index 0000000..ca9d9b1 --- /dev/null +++ b/env/test/resources/config.edn @@ -0,0 +1,6 @@ +{:test true + :port 3001 + ;; when :nrepl-port is set the application starts the nREPL server on load + :nrepl-port 7001 + :github-access-token "c9323a357c2beeebe4e8d618e0fda47dc9e15f62" + :github-user "kagel"} diff --git a/project.clj b/project.clj new file mode 100644 index 0000000..88df704 --- /dev/null +++ b/project.clj @@ -0,0 +1,143 @@ +(defproject commiteth "0.1.0-SNAPSHOT" + + :description "FIXME: write description" + :url "http://example.com/FIXME" + + :dependencies [[metosin/compojure-api "1.1.6"] + [re-frame "0.8.0"] + [cljs-ajax "0.5.8"] + [secretary "1.2.3"] + [reagent-utils "0.2.0"] + [reagent "0.6.0-rc"] + [org.clojure/clojurescript "1.9.225" :scope "provided"] + [org.clojure/clojure "1.8.0"] + [selmer "1.0.7"] + [markdown-clj "0.9.89"] + [ring-middleware-format "0.7.0"] + [metosin/ring-http-response "0.8.0"] + [bouncer "1.0.0"] + [org.webjars/bootstrap "4.0.0-alpha.3"] + [org.webjars/font-awesome "4.6.3"] + [org.webjars/bootstrap-social "5.0.0"] + [org.webjars.bower/tether "1.3.3"] + [org.clojure/tools.logging "0.3.1"] + [compojure "1.5.1"] + [ring-webjars "0.1.1"] + [ring/ring-defaults "0.2.1"] + [mount "0.1.10"] + [cprop "0.1.9"] + [org.clojure/tools.cli "0.3.5"] + [luminus-nrepl "0.1.4"] + [buddy "1.0.0"] + [luminus-migrations "0.2.6"] + [conman "0.6.0"] + [org.postgresql/postgresql "9.4.1209"] + [org.webjars/webjars-locator-jboss-vfs "0.1.0"] + [luminus-immutant "0.2.2"] + [tentacles "0.5.1"]] + + :min-lein-version "2.0.0" + + :jvm-opts ["-server" "-Dconf=.lein-env"] + :source-paths ["src/clj" "src/cljc"] + :resource-paths ["resources" "target/cljsbuild"] + :target-path "target/%s/" + :main commiteth.core + :migratus {:store :database :db ~(get (System/getenv) "DATABASE_URL")} + + :plugins [[lein-cprop "1.0.1"] + [migratus-lein "0.4.1"] + [lein-cljsbuild "1.1.3"] + [lein-immutant "2.1.0"] + [lein-sassc "0.10.4"] + [lein-auto "0.1.2"]] + :sassc + [{:src "resources/scss/screen.scss" + :output-to "resources/public/css/screen.css" + :style "nested" + :import-path "resources/scss"}] + + :auto + {"sassc" {:file-pattern #"\.(scss|sass)$" :paths ["resources/scss"]}} + + :hooks [leiningen.sassc] + :clean-targets ^{:protect false} +[:target-path [:cljsbuild :builds :app :compiler :output-dir] [:cljsbuild :builds :app :compiler :output-to]] + :figwheel + {:http-server-root "public" + :nrepl-port 7002 + :css-dirs ["resources/public/css"] + :nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]} + + + :profiles + {:uberjar {:omit-source true + :prep-tasks ["compile" ["cljsbuild" "once" "min"]] + :cljsbuild + {:builds + {:min + {:source-paths ["src/cljc" "src/cljs" "env/prod/cljs"] + :compiler + {:output-to "target/cljsbuild/public/js/app.js" + :externs ["react/externs/react.js"] + :optimizations :advanced + :pretty-print false + :closure-warnings + {:externs-validation :off :non-standard-jsdoc :off}}}}} + + + :aot :all + :uberjar-name "commiteth.jar" + :source-paths ["env/prod/clj"] + :resource-paths ["env/prod/resources"]} + + :dev [:project/dev :profiles/dev] + :test [:project/test :profiles/test] + + :project/dev {:dependencies [[prone "1.1.1"] + [ring/ring-mock "0.3.0"] + [ring/ring-devel "1.5.0"] + [pjstadig/humane-test-output "0.8.1"] + [doo "0.1.7"] + [binaryage/devtools "0.8.1"] + [figwheel-sidecar "0.5.4-7"] + [com.cemerick/piggieback "0.2.2-SNAPSHOT"]] + :plugins [[com.jakemccrary/lein-test-refresh "0.14.0"] + [lein-doo "0.1.7"] + [lein-figwheel "0.5.4-7"] + [org.clojure/clojurescript "1.9.225"]] + :cljsbuild + {:builds + {:app + {:source-paths ["src/cljs" "src/cljc" "env/dev/cljs"] + :compiler + {:main "commiteth.app" + :asset-path "/js/out" + :output-to "target/cljsbuild/public/js/app.js" + :output-dir "target/cljsbuild/public/js/out" + :source-map true + :optimizations :none + :pretty-print true}}}} + + + + :doo {:build "test"} + :source-paths ["env/dev/clj" "test/clj"] + :resource-paths ["env/dev/resources"] + :repl-options {:init-ns user} + :injections [(require 'pjstadig.humane-test-output) + (pjstadig.humane-test-output/activate!)]} + :project/test {:resource-paths ["env/dev/resources" "env/test/resources"] + :cljsbuild + {:builds + {:test + {:source-paths ["src/cljc" "src/cljs" "test/cljs"] + :compiler + {:output-to "target/test.js" + :main "commiteth.doo-runner" + :optimizations :whitespace + :pretty-print true}}}} + + } + :profiles/dev {} + :profiles/test {}}) diff --git a/resources/public/favicon.ico b/resources/public/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/resources/scss/screen.scss b/resources/scss/screen.scss new file mode 100644 index 0000000..46b8178 --- /dev/null +++ b/resources/scss/screen.scss @@ -0,0 +1,5 @@ +html, body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + height: 100%; + padding-top: 0px; +} diff --git a/resources/sql/queries.sql b/resources/sql/queries.sql new file mode 100644 index 0000000..4191f67 --- /dev/null +++ b/resources/sql/queries.sql @@ -0,0 +1,21 @@ +-- :name create-user! :! :n +-- :doc creates a new user record +INSERT INTO users +(id, first_name, last_name, email, pass) +VALUES (:id, :first_name, :last_name, :email, :pass) + +-- :name update-user! :! :n +-- :doc update an existing user record +UPDATE users +SET first_name = :first_name, last_name = :last_name, email = :email +WHERE id = :id + +-- :name get-user :? :1 +-- :doc retrieve a user given the id. +SELECT * FROM users +WHERE id = :id + +-- :name delete-user! :! :n +-- :doc delete a user given the id +DELETE FROM users +WHERE id = :id diff --git a/resources/templates/error.html b/resources/templates/error.html new file mode 100644 index 0000000..470e4a7 --- /dev/null +++ b/resources/templates/error.html @@ -0,0 +1,59 @@ + + + + Something bad happened + + + {% style "/assets/bootstrap/css/bootstrap.min.css" %} + {% style "/assets/bootstrap/css/bootstrap-theme.min.css" %} + + + +
+
+
+
+
+

Error: {{status}}

+
+ {% if title %} +

{{title}}

+ {% endif %} + {% if message %} +

{{message}}

+ {% endif %} +
+
+
+
+
+ + diff --git a/resources/templates/home.html b/resources/templates/home.html new file mode 100644 index 0000000..6ac2dd3 --- /dev/null +++ b/resources/templates/home.html @@ -0,0 +1,27 @@ + + + + + + Welcome to commiteth + + + +
+
+
+
+ + +{% style "/assets/bootstrap/css/bootstrap.min.css" %} +{% style "/assets/bootstrap-social/bootstrap-social.css" %} +{% style "/assets/font-awesome/css/font-awesome.min.css" %} +{% style "/css/screen.css" %} + + +{% script "/js/app.js" %} + + diff --git a/src/clj/commiteth/config.clj b/src/clj/commiteth/config.clj new file mode 100644 index 0000000..9a284f2 --- /dev/null +++ b/src/clj/commiteth/config.clj @@ -0,0 +1,10 @@ +(ns commiteth.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)])) diff --git a/src/clj/commiteth/core.clj b/src/clj/commiteth/core.clj new file mode 100644 index 0000000..e5e6812 --- /dev/null +++ b/src/clj/commiteth/core.clj @@ -0,0 +1,58 @@ +(ns commiteth.core + (:require [commiteth.handler :as handler] + [luminus.repl-server :as repl] + [luminus.http-server :as http] + [luminus-migrations.core :as migrations] + [commiteth.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 :port #(or (-> env :options :port) %)))) + :stop + (http/stop http-server)) + +(mount/defstate ^{:on-reload :noop} +repl-server + :start + (when-let [nrepl-port (env :nrepl-port)] + (repl/start {:port nrepl-port})) + :stop + (when repl-server + (repl/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] + (cond + (some #{"migrate" "rollback"} args) + (do + (mount/start #'commiteth.config/env) + (migrations/migrate args (select-keys env [:database-url])) + (System/exit 0)) + :else + (start-app args))) + diff --git a/src/clj/commiteth/db/core.clj b/src/clj/commiteth/db/core.clj new file mode 100644 index 0000000..6d31c42 --- /dev/null +++ b/src/clj/commiteth/db/core.clj @@ -0,0 +1,71 @@ +(ns commiteth.db.core + (:require + [cheshire.core :refer [generate-string parse-string]] + [clojure.java.jdbc :as jdbc] + [conman.core :as conman] + [commiteth.config :refer [env]] + [mount.core :refer [defstate]]) + (:import org.postgresql.util.PGobject + java.sql.Array + clojure.lang.IPersistentMap + clojure.lang.IPersistentVector + [java.sql + BatchUpdateException + Date + Timestamp + PreparedStatement])) + +(defstate ^:dynamic *db* + :start (conman/connect! {:jdbc-url (env :database-url)}) + :stop (conman/disconnect! *db*)) + +(conman/bind-connection *db* "sql/queries.sql") + +(defn to-date [^java.sql.Date sql-date] + (-> sql-date (.getTime) (java.util.Date.))) + +(extend-protocol jdbc/IResultSetReadColumn + Date + (result-set-read-column [v _ _] (to-date v)) + + Timestamp + (result-set-read-column [v _ _] (to-date v)) + + Array + (result-set-read-column [v _ _] (vec (.getArray v))) + + PGobject + (result-set-read-column [pgobj _metadata _index] + (let [type (.getType pgobj) + value (.getValue pgobj)] + (case type + "json" (parse-string value true) + "jsonb" (parse-string value true) + "citext" (str value) + value)))) + +(extend-type java.util.Date + jdbc/ISQLParameter + (set-parameter [v ^PreparedStatement stmt ^long idx] + (.setTimestamp stmt idx (Timestamp. (.getTime v))))) + +(defn to-pg-json [value] + (doto (PGobject.) + (.setType "jsonb") + (.setValue (generate-string value)))) + +(extend-type clojure.lang.IPersistentVector + jdbc/ISQLParameter + (set-parameter [v ^java.sql.PreparedStatement stmt ^long idx] + (let [conn (.getConnection stmt) + meta (.getParameterMetaData stmt) + type-name (.getParameterTypeName meta idx)] + (if-let [elem-type (when (= (first type-name) \_) (apply str (rest type-name)))] + (.setObject stmt idx (.createArrayOf conn elem-type (to-array v))) + (.setObject stmt idx (to-pg-json v)))))) + +(extend-protocol jdbc/ISQLValue + IPersistentMap + (sql-value [value] (to-pg-json value)) + IPersistentVector + (sql-value [value] (to-pg-json value))) diff --git a/src/clj/commiteth/handler.clj b/src/clj/commiteth/handler.clj new file mode 100644 index 0000000..6527512 --- /dev/null +++ b/src/clj/commiteth/handler.clj @@ -0,0 +1,27 @@ +(ns commiteth.handler + (:require [compojure.core :refer [routes wrap-routes]] + [commiteth.layout :refer [error-page]] + [commiteth.routes.home :refer [home-routes]] + [commiteth.routes.services :refer [service-routes]] + [compojure.route :as route] + [commiteth.env :refer [defaults]] + [mount.core :as mount] + [commiteth.middleware :as middleware])) + +(mount/defstate init-app + :start ((or (:init defaults) identity)) + :stop ((or (:stop defaults) identity))) + +(def app-routes + (routes + (-> #'home-routes + (wrap-routes middleware/wrap-csrf) + (wrap-routes middleware/wrap-formats)) + #'service-routes + (route/not-found + (:body + (error-page {:status 404 + :title "page not found"}))))) + + +(defn app [] (middleware/wrap-base #'app-routes)) diff --git a/src/clj/commiteth/layout.clj b/src/clj/commiteth/layout.clj new file mode 100644 index 0000000..b10f380 --- /dev/null +++ b/src/clj/commiteth/layout.clj @@ -0,0 +1,39 @@ +(ns commiteth.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*]])) + +(declare ^:dynamic *identity*) +(declare ^:dynamic *app-context*) +(parser/set-resource-path! (clojure.java.io/resource "templates")) +(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/templates" + [template & [params]] + (content-type + (ok + (parser/render-file + template + (assoc params + :page template + :csrf-token *anti-forgery-token* + :servlet-context *app-context*))) + "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)}) diff --git a/src/clj/commiteth/middleware.clj b/src/clj/commiteth/middleware.clj new file mode 100644 index 0000000..347a7e9 --- /dev/null +++ b/src/clj/commiteth/middleware.clj @@ -0,0 +1,93 @@ +(ns commiteth.middleware + (:require [commiteth.env :refer [defaults]] + [clojure.tools.logging :as log] + [commiteth.layout :refer [*app-context* error-page]] + [ring.middleware.anti-forgery :refer [wrap-anti-forgery]] + [ring.middleware.webjars :refer [wrap-webjars]] + [ring.middleware.format :refer [wrap-restful-format]] + [commiteth.config :refer [env]] + [ring.middleware.flash :refer [wrap-flash]] + [immutant.web.middleware :refer [wrap-session]] + [ring.middleware.defaults :refer [site-defaults wrap-defaults]] + [buddy.auth.middleware :refer [wrap-authentication wrap-authorization]] + [buddy.auth.backends.session :refer [session-backend]] + [buddy.auth.accessrules :refer [restrict]] + [buddy.auth :refer [authenticated?]] + [commiteth.layout :refer [*identity*]]) + (:import [javax.servlet ServletContext])) + +(defn wrap-context [handler] + (fn [request] + (binding [*app-context* + (if-let [context (:servlet-context request)] + ;; If we're not inside a servlet environment + ;; (for example when using mock requests), then + ;; .getContextPath might not exist + (try (.getContextPath ^ServletContext context) + (catch IllegalArgumentException _ context)) + ;; if the context is not specified in the request + ;; we check if one has been specified in the environment + ;; instead + (:app-context env))] + (handler request)))) + +(defn wrap-internal-error [handler] + (fn [req] + (try + (handler req) + (catch Throwable t + (log/error 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 (wrap-restful-format + handler + {:formats [:json-kw :transit-json :transit-msgpack]})] + (fn [request] + ;; disable wrap-formats for websockets + ;; since they're not compatible with this middleware + ((if (:websocket? request) handler wrapped) request)))) + +(defn on-error [request response] + (error-page + {:status 403 + :title (str "Access to " (:uri request) " is not authorized")})) + +(defn wrap-restricted [handler] + (restrict handler {:handler authenticated? + :on-error on-error})) + +(defn wrap-identity [handler] + (fn [request] + (binding [*identity* (get-in request [:session :identity])] + (handler request)))) + +(defn wrap-auth [handler] + (let [backend (session-backend)] + (-> handler + wrap-identity + (wrap-authentication backend) + (wrap-authorization backend)))) + +(defn wrap-base [handler] + (-> ((:middleware defaults) handler) + wrap-auth + wrap-webjars + wrap-flash + (wrap-session {:cookie-attrs {:http-only true}}) + (wrap-defaults + (-> site-defaults + (assoc-in [:security :anti-forgery] false) + (dissoc :session))) + wrap-context + wrap-internal-error)) diff --git a/src/clj/commiteth/routes/home.clj b/src/clj/commiteth/routes/home.clj new file mode 100644 index 0000000..85ac87d --- /dev/null +++ b/src/clj/commiteth/routes/home.clj @@ -0,0 +1,14 @@ +(ns commiteth.routes.home + (:require [commiteth.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")))) + diff --git a/src/clj/commiteth/routes/services.clj b/src/clj/commiteth/routes/services.clj new file mode 100644 index 0000000..4598a80 --- /dev/null +++ b/src/clj/commiteth/routes/services.clj @@ -0,0 +1,66 @@ +(ns commiteth.routes.services + (:require [ring.util.http-response :refer :all] + [compojure.api.sweet :refer :all] + [schema.core :as s] + [compojure.api.meta :refer [restructure-param]] + [buddy.auth.accessrules :refer [restrict]] + [buddy.auth :refer [authenticated?]])) + +(defn access-error [_ _] + (unauthorized {:error "unauthorized"})) + +(defn wrap-restricted [handler rule] + (restrict handler {:handler rule + :on-error access-error})) + +(defmethod restructure-param :auth-rules + [_ rule acc] + (update-in acc [:middleware] conj [wrap-restricted rule])) + +(defmethod restructure-param :current-user + [_ binding acc] + (update-in acc [:letks] into [binding `(:identity ~'+compojure-api-request+)])) + +(defapi service-routes + {:swagger {:ui "/swagger-ui" + :spec "/swagger.json" + :data {:info {:version "1.0.0" + :title "Sample API" + :description "Sample Services"}}}} + + (GET "/authenticated" [] + :auth-rules authenticated? + :current-user user + (ok {:user user})) + (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)))))) diff --git a/src/cljc/commiteth/validation.cljc b/src/cljc/commiteth/validation.cljc new file mode 100644 index 0000000..eae8a18 --- /dev/null +++ b/src/cljc/commiteth/validation.cljc @@ -0,0 +1,3 @@ +(ns commiteth.validation + (:require [bouncer.core :as b] + [bouncer.validators :as v])) diff --git a/src/cljs/commiteth/ajax.cljs b/src/cljs/commiteth/ajax.cljs new file mode 100644 index 0000000..4884c34 --- /dev/null +++ b/src/cljs/commiteth/ajax.cljs @@ -0,0 +1,18 @@ +(ns commiteth.ajax + (:require [ajax.core :as ajax])) + +(defn default-headers [request] + (-> request + (update :uri #(str js/context %)) + (update + :headers + #(merge + % + {"Accept" "application/transit+json" + "x-csrf-token" js/csrfToken})))) + +(defn load-interceptors! [] + (swap! ajax/default-interceptors + conj + (ajax/to-interceptor {:name "default headers" + :request default-headers}))) diff --git a/src/cljs/commiteth/core.cljs b/src/cljs/commiteth/core.cljs new file mode 100644 index 0000000..b5e720e --- /dev/null +++ b/src/cljs/commiteth/core.cljs @@ -0,0 +1,76 @@ +(ns commiteth.core + (:require [reagent.core :as r] + [re-frame.core :as rf] + [secretary.core :as secretary] + [goog.events :as events] + [goog.history.EventType :as HistoryEventType] + [markdown.core :refer [md->html]] + [ajax.core :refer [GET POST]] + [commiteth.ajax :refer [load-interceptors!]] + [commiteth.handlers] + [commiteth.subscriptions]) + (:import goog.History)) + +(defn nav-link [uri title page collapsed?] + (let [selected-page (rf/subscribe [:page])] [:li.nav-item + {:class (when (= page @selected-page) "active")} + [:a.nav-link + {:href uri + :on-click #(reset! collapsed? true)} title]])) + +(defn navbar [] + (r/with-let [collapsed? (r/atom true)] + [:nav.navbar.navbar-light.bg-faded + [:button.navbar-toggler.hidden-sm-up + {:on-click #(swap! collapsed? not)} "☰"] + [:div.collapse.navbar-toggleable-xs + (when-not @collapsed? {:class "in"}) + [:a.navbar-brand {:href "#/"} "commiteth"] + [:ul.nav.navbar-nav + [nav-link "#/" "Home" :home collapsed?]]]])) + +(defn home-page [] + [:div.container + [:div.jumbotron + [:h1 "Welcome to commitETH"] + [:p [:a.btn.btn-block.btn-social.btn-github + {:href "http://github.com"} + [:i.fa.fa-github] + "Sign in with GitHub"]]]]) + +(def pages + {:home #'home-page}) + +(defn page [] + [:div + [navbar] + [(pages @(rf/subscribe [:page]))]]) + +;; ------------------------- +;; Routes +(secretary/set-config! :prefix "#") + +(secretary/defroute "/" [] + (rf/dispatch [:set-active-page :home])) + +;; ------------------------- +;; 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 [] + (r/render [#'page] (.getElementById js/document "app"))) + +(defn init! [] + (rf/dispatch-sync [:initialize-db]) + (load-interceptors!) + (hook-browser-navigation!) + (mount-components)) diff --git a/src/cljs/commiteth/db.cljs b/src/cljs/commiteth/db.cljs new file mode 100644 index 0000000..d251dd0 --- /dev/null +++ b/src/cljs/commiteth/db.cljs @@ -0,0 +1,4 @@ +(ns commiteth.db) + +(def default-db + {:page :home}) diff --git a/src/cljs/commiteth/handlers.cljs b/src/cljs/commiteth/handlers.cljs new file mode 100644 index 0000000..d3441ce --- /dev/null +++ b/src/cljs/commiteth/handlers.cljs @@ -0,0 +1,13 @@ +(ns commiteth.handlers + (:require [commiteth.db :as db] + [re-frame.core :refer [dispatch reg-event-db]])) + +(reg-event-db + :initialize-db + (fn [_ _] + db/default-db)) + +(reg-event-db + :set-active-page + (fn [db [_ page]] + (assoc db :page page))) diff --git a/src/cljs/commiteth/subscriptions.cljs b/src/cljs/commiteth/subscriptions.cljs new file mode 100644 index 0000000..7d75cc0 --- /dev/null +++ b/src/cljs/commiteth/subscriptions.cljs @@ -0,0 +1,12 @@ +(ns commiteth.subscriptions + (:require [re-frame.core :refer [reg-sub]])) + +(reg-sub + :page + (fn [db _] + (:page db))) + +(reg-sub + :docs + (fn [db _] + (:docs db))) diff --git a/test/clj/commiteth/test/db/core.clj b/test/clj/commiteth/test/db/core.clj new file mode 100644 index 0000000..c03d31c --- /dev/null +++ b/test/clj/commiteth/test/db/core.clj @@ -0,0 +1,36 @@ +(ns commiteth.test.db.core + (:require [commiteth.db.core :refer [*db*] :as db] + [luminus-migrations.core :as migrations] + [clojure.test :refer :all] + [clojure.java.jdbc :as jdbc] + [commiteth.config :refer [env]] + [mount.core :as mount])) + +(use-fixtures + :once + (fn [f] + (mount/start + #'commiteth.config/env + #'commiteth.db.core/*db*) + (migrations/migrate ["migrate"] (select-keys env [:database-url])) + (f))) + +(deftest test-users + (jdbc/with-db-transaction [t-conn *db*] + (jdbc/db-set-rollback-only! t-conn) + (is (= 1 (db/create-user! + t-conn + {:id "1" + :first_name "Sam" + :last_name "Smith" + :email "sam.smith@example.com" + :pass "pass"}))) + (is (= {:id "1" + :first_name "Sam" + :last_name "Smith" + :email "sam.smith@example.com" + :pass "pass" + :admin nil + :last_login nil + :is_active nil} + (db/get-user t-conn {:id "1"}))))) diff --git a/test/clj/commiteth/test/handler.clj b/test/clj/commiteth/test/handler.clj new file mode 100644 index 0000000..0d50bdc --- /dev/null +++ b/test/clj/commiteth/test/handler.clj @@ -0,0 +1,13 @@ +(ns commiteth.test.handler + (:require [clojure.test :refer :all] + [ring.mock.request :refer :all] + [commiteth.handler :refer :all])) + +(deftest test-app + (testing "main route" + (let [response ((app) (request :get "/"))] + (is (= 200 (:status response))))) + + (testing "not-found route" + (let [response ((app) (request :get "/invalid"))] + (is (= 404 (:status response)))))) diff --git a/test/cljs/commiteth/core_test.cljs b/test/cljs/commiteth/core_test.cljs new file mode 100644 index 0000000..858bd95 --- /dev/null +++ b/test/cljs/commiteth/core_test.cljs @@ -0,0 +1,8 @@ +(ns commiteth.core-test + (:require [cljs.test :refer-macros [is are deftest testing use-fixtures]] + [reagent.core :as reagent :refer [atom]] + [commiteth.core :as rc])) + +(deftest test-home + (is (= true true))) + diff --git a/test/cljs/commiteth/doo_runner.cljs b/test/cljs/commiteth/doo_runner.cljs new file mode 100644 index 0000000..e355546 --- /dev/null +++ b/test/cljs/commiteth/doo_runner.cljs @@ -0,0 +1,6 @@ +(ns commiteth.doo-runner + (:require [doo.runner :refer-macros [doo-tests]] + [commiteth.core-test])) + +(doo-tests 'commiteth.core-test) +