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