diff --git a/.lein-env b/.lein-env new file mode 100644 index 0000000..b0d286a --- /dev/null +++ b/.lein-env @@ -0,0 +1 @@ +{:dev true, :port 3000, :nrepl-port 7000, :log-level :trace} diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..52064e8 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: java $JVM_OPTS -cp target/mailhead.jar clojure.main -m mailhead.core diff --git a/env/dev/clj/mailhead/config.clj b/env/dev/clj/mailhead/config.clj new file mode 100644 index 0000000..768472f --- /dev/null +++ b/env/dev/clj/mailhead/config.clj @@ -0,0 +1,11 @@ +(ns mailhead.config + (:require [selmer.parser :as parser] + [taoensso.timbre :as timbre] + [mailhead.dev-middleware :refer [wrap-dev]])) + +(def defaults + {:init + (fn [] + (parser/cache-off!) + (timbre/info "\n-=[mailhead started successfully using the development profile]=-")) + :middleware wrap-dev}) diff --git a/env/dev/clj/mailhead/dev_middleware.clj b/env/dev/clj/mailhead/dev_middleware.clj new file mode 100644 index 0000000..c4fffdc --- /dev/null +++ b/env/dev/clj/mailhead/dev_middleware.clj @@ -0,0 +1,10 @@ +(ns mailhead.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/prod/clj/mailhead/config.clj b/env/prod/clj/mailhead/config.clj new file mode 100644 index 0000000..5b9ff4f --- /dev/null +++ b/env/prod/clj/mailhead/config.clj @@ -0,0 +1,8 @@ +(ns mailhead.config + (:require [taoensso.timbre :as timbre])) + +(def defaults + {:init + (fn [] + (timbre/info "\n-=[mailhead started successfully]=-")) + :middleware identity}) diff --git a/profiles.clj b/profiles.clj new file mode 100644 index 0000000..ab9c531 --- /dev/null +++ b/profiles.clj @@ -0,0 +1,2 @@ +{:profiles/dev {:env {}} + :profiles/test {:env {}}} diff --git a/project.clj b/project.clj new file mode 100644 index 0000000..0ba8cd5 --- /dev/null +++ b/project.clj @@ -0,0 +1,60 @@ +(defproject mailhead "0.1.0-SNAPSHOT" + + :description "FIXME: write description" + :url "http://example.com/FIXME" + + :dependencies [[org.clojure/clojure "1.7.0"] + [selmer "0.9.5"] + [markdown-clj "0.9.82"] + [environ "1.0.1"] + [metosin/ring-middleware-format "0.6.0"] + [metosin/ring-http-response "0.6.5"] + [bouncer "0.3.3"] + [org.clojure/tools.nrepl "0.2.12"] + [org.webjars/bootstrap "3.3.6"] + [org.webjars/jquery "2.1.4"] + [com.taoensso/tower "3.0.2"] + [com.taoensso/timbre "4.1.4"] + [com.fzakaria/slf4j-timbre "0.2.1"] + [compojure "1.4.0"] + [ring-webjars "0.1.1"] + [ring/ring-defaults "0.1.5"] + [ring "1.4.0" :exclusions [ring/ring-jetty-adapter]] + [mount "0.1.5"] + [org.immutant/web "2.1.1" :exclusions [ch.qos.logback/logback-classic]]] + + :min-lein-version "2.0.0" + :uberjar-name "mailhead.jar" + :jvm-opts ["-server"] + + :main mailhead.core + + :plugins [[lein-environ "1.0.1"]] + :profiles + {:uberjar {:omit-source true + :env {:production true} + :aot :all + :source-paths ["env/prod/clj"]} + :dev [:project/dev :profiles/dev] + :test [:project/test :profiles/test] + :project/dev {:dependencies [[prone "0.8.2"] + [ring/ring-mock "0.3.0"] + [ring/ring-devel "1.4.0"] + [pjstadig/humane-test-output "0.7.1"]] + + + :source-paths ["env/dev/clj"] + :repl-options {:init-ns mailhead.core} + :injections [(require 'pjstadig.humane-test-output) + (pjstadig.humane-test-output/activate!)] + ;;when :nrepl-port is set the application starts the nREPL server on load + :env {:dev true + :port 3000 + :nrepl-port 7000 + :log-level :trace}} + :project/test {:env {:test true + :port 3001 + :nrepl-port 7001 + :log-level :trace}} + :profiles/dev {} + :profiles/test {}}) diff --git a/resources/docs/docs.md b/resources/docs/docs.md new file mode 100644 index 0000000..b646c79 --- /dev/null +++ b/resources/docs/docs.md @@ -0,0 +1,23 @@ + + +### Managing Your Middleware + +Request middleware functions are located under the `mailhead.middleware` namespace. + +This namespace is reserved for any custom middleware for the application. Some default middleware is +already defined here. The middleware is assembled in the `wrap-base` function. + +Middleware used for development is placed in the `mailhead.dev-middleware` namespace found in +the `env/dev/clj/` source path. + +### Here are some links to get started + +1. [HTML templating](http://www.luminusweb.net/docs/html_templating.md) +2. [Accessing the database](http://www.luminusweb.net/docs/database.md) +3. [Serving static resources](http://www.luminusweb.net/docs/static_resources.md) +4. [Setting response types](http://www.luminusweb.net/docs/responses.md) +5. [Defining routes](http://www.luminusweb.net/docs/routes.md) +6. [Adding middleware](http://www.luminusweb.net/docs/middleware.md) +7. [Sessions and cookies](http://www.luminusweb.net/docs/sessions_cookies.md) +8. [Security](http://www.luminusweb.net/docs/security.md) +9. [Deploying the application](http://www.luminusweb.net/docs/deployment.md) diff --git a/resources/public/css/screen.css b/resources/public/css/screen.css new file mode 100644 index 0000000..eb470df --- /dev/null +++ b/resources/public/css/screen.css @@ -0,0 +1,58 @@ +html, +body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + height: 100%; + padding-top: 40px; +} +{% if cljs %} +@-moz-keyframes three-quarters-loader { + 0% { + -moz-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -moz-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-webkit-keyframes three-quarters-loader { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@keyframes three-quarters-loader { + 0% { + -moz-transform: rotate(0deg); + -ms-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -moz-transform: rotate(360deg); + -ms-transform: rotate(360deg); + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} +/* :not(:required) hides this rule from IE9 and below */ +.three-quarters-loader:not(:required) { + -moz-animation: three-quarters-loader 1250ms infinite linear; + -webkit-animation: three-quarters-loader 1250ms infinite linear; + animation: three-quarters-loader 1250ms infinite linear; + border: 8px solid #38e; + border-right-color: transparent; + border-radius: 16px; + box-sizing: border-box; + display: inline-block; + position: relative; + overflow: hidden; + text-indent: -9999px; + width: 32px; + height: 32px; +} +{% endif %} diff --git a/resources/public/favicon.ico b/resources/public/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/resources/templates/about.html b/resources/templates/about.html new file mode 100644 index 0000000..5317223 --- /dev/null +++ b/resources/templates/about.html @@ -0,0 +1,4 @@ +{% extends "base.html" %} +{% block content %} +

this is the story of mailhead... work in progress

+{% endblock %} diff --git a/resources/templates/base.html b/resources/templates/base.html new file mode 100644 index 0000000..9fa9fa9 --- /dev/null +++ b/resources/templates/base.html @@ -0,0 +1,61 @@ + + + + + + Welcome to mailhead + + + {% style "/assets/bootstrap/css/bootstrap.min.css" %} + {% style "/assets/bootstrap/css/bootstrap-theme.min.css" %} + {% style "/css/screen.css" %} + + + + + +
+ {% block content %} + {% endblock %} +
+ + + {% script "/assets/jquery/jquery.min.js" %} + {% script "/assets/bootstrap/js/bootstrap.min.js" %} + {% script "/assets/bootstrap/js/collapse.js" %} + + + {% block page-scripts %} + {% endblock %} + + diff --git a/resources/templates/error.html b/resources/templates/error.html new file mode 100644 index 0000000..6fcd237 --- /dev/null +++ b/resources/templates/error.html @@ -0,0 +1,56 @@ + + + + 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..b6ba349 --- /dev/null +++ b/resources/templates/home.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} +{% block content %} +
+

Welcome to mailhead

+

Time to start building your site!

+

Learn more »

+
+ +
+
+ {{docs|markdown}} +
+
+{% endblock %} diff --git a/src/mailhead/core.clj b/src/mailhead/core.clj new file mode 100644 index 0000000..bff7881 --- /dev/null +++ b/src/mailhead/core.clj @@ -0,0 +1,64 @@ +(ns mailhead.core + (:require [mailhead.handler :refer [app init destroy]] + [immutant.web :as immutant] + [clojure.tools.nrepl.server :as nrepl] + [taoensso.timbre :as timbre] + [environ.core :refer [env]]) + (:gen-class)) + +(defonce nrepl-server (atom nil)) + +(defn parse-port [port] + (when port + (cond + (string? port) (Integer/parseInt port) + (number? port) port + :else (throw (Exception. (str "invalid port value: " port)))))) + +(defn stop-nrepl [] + (when-let [server @nrepl-server] + (nrepl/stop-server server))) + +(defn start-nrepl + "Start a network repl for debugging when the :nrepl-port is set in the environment." + [] + (if @nrepl-server + (timbre/error "nREPL is already running!") + (when-let [port (env :nrepl-port)] + (try + (->> port + (parse-port) + (nrepl/start-server :port) + (reset! nrepl-server)) + (timbre/info "nREPL server started on port" port) + (catch Throwable t + (timbre/error t "failed to start nREPL")))))) + +(defn http-port [port] + (parse-port (or port (env :port) 3000))) + +(defonce http-server (atom nil)) + +(defn start-http-server [port] + (init) + (reset! http-server (immutant/run app :host "0.0.0.0" :port port))) + +(defn stop-http-server [] + (when @http-server + (destroy) + (immutant/stop @http-server) + (reset! http-server nil))) + +(defn stop-app [] + (stop-nrepl) + (stop-http-server) + (shutdown-agents)) + +(defn start-app [[port]] + (.addShutdownHook (Runtime/getRuntime) (Thread. stop-app)) + (start-nrepl) + (start-http-server (http-port port)) + (timbre/info "server started on port:" (:port @http-server))) + +(defn -main [& args] + (start-app args)) diff --git a/src/mailhead/handler.clj b/src/mailhead/handler.clj new file mode 100644 index 0000000..d323d79 --- /dev/null +++ b/src/mailhead/handler.clj @@ -0,0 +1,48 @@ +(ns mailhead.handler + (:require [compojure.core :refer [defroutes routes wrap-routes]] + [mailhead.layout :refer [error-page]] + [mailhead.routes.home :refer [home-routes]] + [mailhead.middleware :as middleware] + [compojure.route :as route] + [taoensso.timbre :as timbre] + [taoensso.timbre.appenders.3rd-party.rotor :as rotor] + [selmer.parser :as parser] + [environ.core :refer [env]] + [mailhead.config :refer [defaults]] + [mount.core :as mount])) + +(defn init + "init will be called once when + app is deployed as a servlet on + an app server such as Tomcat + put any initialization code here" + [] + + (timbre/merge-config! + {:level ((fnil keyword :info) (env :log-level)) + :appenders {:rotor (rotor/rotor-appender + {:path (or (env :log-path) "mailhead.log") + :max-size (* 512 1024) + :backlog 10})}}) + (doseq [component (:started (mount/start))] + (timbre/info component "started")) + ((:init defaults))) + +(defn destroy + "destroy will be called when your application + shuts down, put any clean up code here" + [] + (timbre/info "mailhead is shutting down...") + (doseq [component (:stopped (mount/stop))] + (timbre/info component "stopped")) + (timbre/info "shutdown complete!")) + +(def app-routes + (routes + (wrap-routes #'home-routes middleware/wrap-csrf) + (route/not-found + (:body + (error-page {:status 404 + :title "page not found"}))))) + +(def app (middleware/wrap-base #'app-routes)) diff --git a/src/mailhead/layout.clj b/src/mailhead/layout.clj new file mode 100644 index 0000000..f9ef91f --- /dev/null +++ b/src/mailhead/layout.clj @@ -0,0 +1,39 @@ +(ns mailhead.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 *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/mailhead/middleware.clj b/src/mailhead/middleware.clj new file mode 100644 index 0000000..09a5bd1 --- /dev/null +++ b/src/mailhead/middleware.clj @@ -0,0 +1,61 @@ +(ns mailhead.middleware + (:require [mailhead.layout :refer [*app-context* error-page]] + [taoensso.timbre :as timbre] + [environ.core :refer [env]] + [ring.middleware.flash :refer [wrap-flash]] + [immutant.web.middleware :refer [wrap-session]] + [ring.middleware.webjars :refer [wrap-webjars]] + [ring.middleware.defaults :refer [site-defaults wrap-defaults]] + [ring.middleware.anti-forgery :refer [wrap-anti-forgery]] + [ring.middleware.format :refer [wrap-restful-format]] + [mailhead.config :refer [defaults]]) + (: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 + (timbre/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] + (wrap-restful-format handler {:formats [:json-kw :transit-json :transit-msgpack]})) + +(defn wrap-base [handler] + (-> ((:middleware defaults) handler) + wrap-formats + 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/mailhead/routes/home.clj b/src/mailhead/routes/home.clj new file mode 100644 index 0000000..896d9d0 --- /dev/null +++ b/src/mailhead/routes/home.clj @@ -0,0 +1,17 @@ +(ns mailhead.routes.home + (:require [mailhead.layout :as layout] + [compojure.core :refer [defroutes GET]] + [ring.util.http-response :refer [ok]] + [clojure.java.io :as io])) + +(defn home-page [] + (layout/render + "home.html" {:docs (-> "docs/docs.md" io/resource slurp)})) + +(defn about-page [] + (layout/render "about.html")) + +(defroutes home-routes + (GET "/" [] (home-page)) + (GET "/about" [] (about-page))) +