diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ad22456
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+/target
+/lib
+/classes
+/checkouts
+pom.xml
+*.jar
+*.class
+/.lein-*
+profiles.clj
+/.env
+*.log
diff --git a/Procfile b/Procfile
new file mode 100644
index 0000000..e5c7ada
--- /dev/null
+++ b/Procfile
@@ -0,0 +1 @@
+web: java $JVM_OPTS -cp target/guestbook.jar clojure.main -m guestbook.core
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c2b5e87
--- /dev/null
+++ b/README.md
@@ -0,0 +1,19 @@
+# guestbook
+
+FIXME
+
+## Prerequisites
+
+You will need [Leiningen][1] 2.0 or above installed.
+
+[1]: https://github.com/technomancy/leiningen
+
+## Running
+
+To start a web server for the application, run:
+
+ lein ring server
+
+## License
+
+Copyright © 2015 FIXME
diff --git a/migrations/201506232402-add-users-table.down.sql b/migrations/201506232402-add-users-table.down.sql
new file mode 100644
index 0000000..cc1f647
--- /dev/null
+++ b/migrations/201506232402-add-users-table.down.sql
@@ -0,0 +1 @@
+DROP TABLE users;
diff --git a/migrations/201506232402-add-users-table.up.sql b/migrations/201506232402-add-users-table.up.sql
new file mode 100644
index 0000000..b95c101
--- /dev/null
+++ b/migrations/201506232402-add-users-table.up.sql
@@ -0,0 +1,9 @@
+CREATE TABLE users
+(id VARCHAR(20) PRIMARY KEY,
+ first_name VARCHAR(30),
+ last_name VARCHAR(30),
+ email VARCHAR(30),
+ admin BOOLEAN,
+ last_login TIME,
+ is_active BOOLEAN,
+ pass VARCHAR(100));
diff --git a/project.clj b/project.clj
new file mode 100644
index 0000000..cc90f87
--- /dev/null
+++ b/project.clj
@@ -0,0 +1,65 @@
+(defproject guestbook "0.1.0-SNAPSHOT"
+
+ :description "FIXME: write description"
+ :url "http://example.com/FIXME"
+
+ :dependencies [[org.clojure/clojure "1.7.0-RC2"]
+ [selmer "0.8.2"]
+ [com.taoensso/timbre "3.4.0"]
+ [com.taoensso/tower "3.0.2"]
+ [markdown-clj "0.9.66"]
+ [environ "1.0.0"]
+ [compojure "1.3.4"]
+ [ring/ring-defaults "0.1.5"]
+ [ring/ring-session-timeout "0.1.0"]
+ [metosin/ring-middleware-format "0.6.0"]
+ [metosin/ring-http-response "0.6.2"]
+ [bouncer "0.3.3"]
+ [prone "0.8.2"]
+ [org.clojure/tools.nrepl "0.2.10"]
+ [ring-server "0.4.0"]
+ [ragtime "0.3.9"]
+ [org.clojure/java.jdbc "0.3.7"]
+ [instaparse "1.4.0"]
+ [yesql "0.5.0-rc2"]
+ [com.h2database/h2 "1.4.187"]]
+
+ :min-lein-version "2.0.0"
+ :uberjar-name "guestbook.jar"
+ :jvm-opts ["-server"]
+
+;;enable to start the nREPL server when the application launches
+;:env {:repl-port 7001}
+
+ :main guestbook.core
+
+ :plugins [[lein-ring "0.9.1"]
+ [lein-environ "1.0.0"]
+ [lein-ancient "0.6.5"]
+ [ragtime/ragtime.lein "0.3.8"]]
+
+
+
+ :ring {:handler guestbook.handler/app
+ :init guestbook.handler/init
+ :destroy guestbook.handler/destroy
+ :uberwar-name "guestbook.war"}
+
+
+
+ :profiles
+ {:uberjar {:omit-source true
+ :env {:production true}
+
+ :aot :all}
+ :dev {:dependencies [[ring-mock "0.1.5"]
+ [ring/ring-devel "1.3.2"]
+ [pjstadig/humane-test-output "0.7.0"]
+ ]
+
+
+
+ :repl-options {:init-ns guestbook.core}
+ :injections [(require 'pjstadig.humane-test-output)
+ (pjstadig.humane-test-output/activate!)]
+ :env {:dev true}}})
diff --git a/resources/docs/docs.md b/resources/docs/docs.md
new file mode 100644
index 0000000..727fed9
--- /dev/null
+++ b/resources/docs/docs.md
@@ -0,0 +1,37 @@
+
+
+### Database Configuration is Required
+
+Before continuing please follow the steps below to configure your database connection and run the necessary migrations.
+
+* Run `lein run migrate` in the root of the project to create the tables.
+* Restart the application.
+
+
+
+### Managing Your Middleware
+
+Request middleware functions are located under the `guestbook.middleware` namespace.
+A request logging helper called `log-request` is defined below:
+
+```clojure
+(defn log-request [handler]
+ (fn [req]
+ (timbre/debug req)
+ (handler req)))
+```
+
+This namespace also defines two vectors for organizing the middleware called `development-middleware` and `production-middleware`.
+Any middleware that you only wish to run in development mode, such as `log-request`, should be added to the first vector.
+
+### 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..d759510
--- /dev/null
+++ b/resources/public/css/screen.css
@@ -0,0 +1,6 @@
+html,
+body {
+ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ height: 100%;
+ padding-top: 40px;
+}
diff --git a/resources/sql/queries.sql b/resources/sql/queries.sql
new file mode 100644
index 0000000..8027729
--- /dev/null
+++ b/resources/sql/queries.sql
@@ -0,0 +1,16 @@
+-- name: create-user!
+-- 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!
+-- update an existing user record
+UPDATE users
+SET first_name = :first_name, last_name = :last_name, email = :email
+WHERE id = :id
+
+-- name: get-user
+-- retrieve a user given the id.
+SELECT * FROM users
+WHERE id = :id
diff --git a/resources/templates/about.html b/resources/templates/about.html
new file mode 100644
index 0000000..917f564
--- /dev/null
+++ b/resources/templates/about.html
@@ -0,0 +1,4 @@
+{% extends "base.html" %}
+{% block content %}
+ this is the story of guestbook... work in progress
+{% endblock %}
diff --git a/resources/templates/base.html b/resources/templates/base.html
new file mode 100644
index 0000000..e78c30a
--- /dev/null
+++ b/resources/templates/base.html
@@ -0,0 +1,46 @@
+
+
+
+
+ Welcome to guestbook
+
+
+
+
+
+
+ {% block content %}
+ {% endblock %}
+
+
+
+
+ {% style "/css/screen.css" %}
+
+
+
+
+
+ {% block page-scripts %}
+ {% endblock %}
+
+
+
diff --git a/resources/templates/error.html b/resources/templates/error.html
new file mode 100644
index 0000000..13f5e0a
--- /dev/null
+++ b/resources/templates/error.html
@@ -0,0 +1,52 @@
+
+
+
+ Something bad happened
+
+
+
+
+
+
+
+
+
+
+
+
+
Error: 500
+
+ Something very bad has happened!
+ We've dispatched a team of highly trained gnomes to take care of the problem.
+
+
+
+
+
+
+
diff --git a/resources/templates/home.html b/resources/templates/home.html
new file mode 100644
index 0000000..5b5c583
--- /dev/null
+++ b/resources/templates/home.html
@@ -0,0 +1,14 @@
+{% extends "base.html" %}
+{% block content %}
+
+
Welcome to guestbook
+
Time to start building your site!
+
Learn more »
+
+
+
+
+ {{docs|markdown}}
+
+
+{% endblock %}
diff --git a/src/guestbook/core.clj b/src/guestbook/core.clj
new file mode 100644
index 0000000..785f8c3
--- /dev/null
+++ b/src/guestbook/core.clj
@@ -0,0 +1,52 @@
+(ns guestbook.core
+ (:require [guestbook.handler :refer [app init destroy]]
+ [ring.adapter.jetty :refer [run-jetty]]
+
+ [ring.middleware.reload :as reload]
+ [ragtime.main]
+ [taoensso.timbre :as timbre]
+ [environ.core :refer [env]])
+ (:gen-class))
+
+(defn parse-port [[port]]
+ (Integer/parseInt (or port (env :port) "3000")))
+
+
+
+
+
+(defonce server (atom nil))
+
+(defn start-server [port]
+ (init)
+ (reset! server
+ (run-jetty
+ (if (env :dev) (reload/wrap-reload #'app) app)
+ {:port port
+ :join? false})))
+
+(defn stop-server []
+ (when @server
+ (destroy)
+ (.stop @server)
+ (reset! server nil)))
+
+(defn start-app [args]
+ (let [port (parse-port args)]
+ (.addShutdownHook (Runtime/getRuntime) (Thread. stop-server))
+ (start-server port)))
+
+
+(defn migrate [args]
+ (ragtime.main/-main
+ "-r" "ragtime.sql.database"
+ "-d" (env :database-url)
+ "-m" "ragtime.sql.files/migrations"
+ (clojure.string/join args)))
+
+(defn -main [& args]
+ (case (first args)
+ "migrate" (migrate args)
+ "rollback" (migrate args)
+ (start-app args)))
+
diff --git a/src/guestbook/db/core.clj b/src/guestbook/db/core.clj
new file mode 100644
index 0000000..badf168
--- /dev/null
+++ b/src/guestbook/db/core.clj
@@ -0,0 +1,16 @@
+(ns guestbook.db.core
+ (:require
+ [yesql.core :refer [defqueries]]
+ [clojure.java.io :as io]))
+
+(def db-store (str (.getName (io/file ".")) "/site.db"))
+
+(def db-spec
+ {:classname "org.h2.Driver"
+ :subprotocol "h2"
+ :subname db-store
+ :make-pool? true
+ :naming {:keys clojure.string/lower-case
+ :fields clojure.string/upper-case}})
+
+(defqueries "sql/queries.sql" {:connection db-spec})
diff --git a/src/guestbook/handler.clj b/src/guestbook/handler.clj
new file mode 100644
index 0000000..97165cf
--- /dev/null
+++ b/src/guestbook/handler.clj
@@ -0,0 +1,75 @@
+(ns guestbook.handler
+ (:require [compojure.core :refer [defroutes routes wrap-routes]]
+ [guestbook.routes.home :refer [home-routes]]
+
+ [guestbook.middleware :as middleware]
+ [guestbook.session :as session]
+ [compojure.route :as route]
+ [taoensso.timbre :as timbre]
+ [taoensso.timbre.appenders.rotor :as rotor]
+ [selmer.parser :as parser]
+ [environ.core :refer [env]]
+ [clojure.tools.nrepl.server :as nrepl]))
+
+(defonce nrepl-server (atom nil))
+
+(defroutes base-routes
+ (route/resources "/")
+ (route/not-found "Not Found"))
+
+(defn start-nrepl
+ "Start a network repl for debugging when the :repl-port is set in the environment."
+ []
+ (when-let [port (env :repl-port)]
+ (try
+ (reset! nrepl-server (nrepl/start-server :port port))
+ (timbre/info "nREPL server started on port" port)
+ (catch Throwable t
+ (timbre/error "failed to start nREPL" t)))))
+
+(defn stop-nrepl []
+ (when-let [server @nrepl-server]
+ (nrepl/stop-server server)))
+
+(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/set-config!
+ [:appenders :rotor]
+ {:min-level (if (env :dev) :trace :info)
+ :enabled? true
+ :async? false ; should be always false for rotor
+ :max-message-per-msecs nil
+ :fn rotor/appender-fn})
+
+ (timbre/set-config!
+ [:shared-appender-config :rotor]
+ {:path "guestbook.log" :max-size (* 512 1024) :backlog 10})
+
+ (if (env :dev) (parser/cache-off!))
+ (start-nrepl)
+ ;;start the expired session cleanup job
+ (session/start-cleanup-job!)
+ (timbre/info (str
+ "\n-=[guestbook started successfully"
+ (when (env :dev) "using the development profile")
+ "]=-")))
+
+(defn destroy
+ "destroy will be called when your application
+ shuts down, put any clean up code here"
+ []
+ (timbre/info "guestbook is shutting down...")
+ (stop-nrepl)
+ (timbre/info "shutdown complete!"))
+
+(def app
+ (-> (routes
+
+ (wrap-routes #'home-routes middleware/wrap-csrf)
+ #'base-routes)
+ middleware/wrap-base))
diff --git a/src/guestbook/layout.clj b/src/guestbook/layout.clj
new file mode 100644
index 0000000..9532f1e
--- /dev/null
+++ b/src/guestbook/layout.clj
@@ -0,0 +1,26 @@
+(ns guestbook.layout
+ (:require [selmer.parser :as parser]
+ [selmer.filters :as filters]
+ [markdown.core :refer [md-to-html-string]]
+ [ring.util.response :refer [content-type response]]
+ [compojure.response :refer [Renderable]]
+ [ring.util.anti-forgery :refer [anti-forgery-field]]
+ [ring.middleware.anti-forgery :refer [*anti-forgery-token*]]
+ [environ.core :refer [env]]))
+
+
+(declare ^:dynamic *servlet-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 [template & [params]]
+ (-> template
+ (parser/render-file
+ (assoc params
+ :page template
+ :dev (env :dev)
+ :csrf-token *anti-forgery-token*
+ :servlet-context *servlet-context*))
+ response
+ (content-type "text/html; charset=utf-8")))
diff --git a/src/guestbook/middleware.clj b/src/guestbook/middleware.clj
new file mode 100644
index 0000000..ed5e524
--- /dev/null
+++ b/src/guestbook/middleware.clj
@@ -0,0 +1,65 @@
+(ns guestbook.middleware
+ (:require [guestbook.session :as session]
+ [guestbook.layout :refer [*servlet-context*]]
+ [taoensso.timbre :as timbre]
+ [environ.core :refer [env]]
+ [clojure.java.io :as io]
+ [selmer.middleware :refer [wrap-error-page]]
+ [prone.middleware :refer [wrap-exceptions]]
+ [ring.util.response :refer [redirect]]
+ [ring.middleware.defaults :refer [site-defaults wrap-defaults]]
+ [ring.middleware.anti-forgery :refer [wrap-anti-forgery]]
+ [ring.middleware.session-timeout :refer [wrap-idle-session-timeout]]
+ [ring.middleware.session.memory :refer [memory-store]]
+ [ring.middleware.format :refer [wrap-restful-format]]
+
+
+ ))
+
+(defn wrap-servlet-context [handler]
+ (fn [request]
+ (binding [*servlet-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 context)
+ (catch IllegalArgumentException _ context)))]
+ (handler request))))
+
+(defn wrap-internal-error [handler]
+ (fn [req]
+ (try
+ (handler req)
+ (catch Throwable t
+ (timbre/error t)
+ {:status 500
+ :headers {"Content-Type" "text/html"}
+ :body (-> "templates/error.html" io/resource slurp)}))))
+
+(defn wrap-dev [handler]
+ (if (env :dev)
+ (-> handler
+ wrap-error-page
+ wrap-exceptions)
+ handler))
+
+(defn wrap-csrf [handler]
+ (wrap-anti-forgery handler))
+
+(defn wrap-formats [handler]
+ (wrap-restful-format handler :formats [:json-kw :transit-json :transit-msgpack]))
+
+(defn wrap-base [handler]
+ (-> handler
+ wrap-dev
+ (wrap-idle-session-timeout
+ {:timeout (* 60 30)
+ :timeout-response (redirect "/")})
+ wrap-formats
+ (wrap-defaults
+ (-> site-defaults
+ (assoc-in [:security :anti-forgery] false)
+ (assoc-in [:session :store] (memory-store session/mem))))
+ wrap-servlet-context
+ wrap-internal-error))
diff --git a/src/guestbook/routes/home.clj b/src/guestbook/routes/home.clj
new file mode 100644
index 0000000..643b469
--- /dev/null
+++ b/src/guestbook/routes/home.clj
@@ -0,0 +1,17 @@
+(ns guestbook.routes.home
+ (:require [guestbook.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)))
+
diff --git a/src/guestbook/session.clj b/src/guestbook/session.clj
new file mode 100644
index 0000000..f63a94b
--- /dev/null
+++ b/src/guestbook/session.clj
@@ -0,0 +1,20 @@
+(ns guestbook.session)
+
+(defonce mem (atom {}))
+(def half-hour 1800000)
+
+(defn- current-time []
+ (quot (System/currentTimeMillis) 1000))
+
+(defn- expired? [[id session]]
+ (pos? (- (:ring.middleware.session-timeout/idle-timeout session) (current-time))))
+
+(defn clear-expired-sessions []
+ (clojure.core/swap! mem #(->> % (filter expired?) (into {}))))
+
+(defn start-cleanup-job! []
+ (future
+ (loop []
+ (clear-expired-sessions)
+ (Thread/sleep half-hour)
+ (recur))))
diff --git a/test/guestbook/test/handler.clj b/test/guestbook/test/handler.clj
new file mode 100644
index 0000000..a5537bd
--- /dev/null
+++ b/test/guestbook/test/handler.clj
@@ -0,0 +1,13 @@
+(ns guestbook.test.handler
+ (:require [clojure.test :refer :all]
+ [ring.mock.request :refer :all]
+ [guestbook.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))))))