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)))
+