diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..00f42c8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +/target +/lib +/classes +/checkouts +pom.xml +dev-config.edn +test-config.edn +*.jar +*.class +/.lein-* +profiles.clj +/.env +.nrepl-port +/log diff --git a/Capstanfile b/Capstanfile new file mode 100644 index 0000000..c8d381a --- /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 /ireadit/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: + /ireadit/app.jar: ./target/uberjar/ireadit.jar + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5d8ded1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM openjdk:8-alpine + +COPY target/uberjar/ireadit.jar /ireadit/app.jar + +EXPOSE 3000 + +CMD ["java", "-jar", "/ireadit/app.jar"] diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..c32c812 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: java -cp target/uberjar/ireadit.jar clojure.main -m ireadit.core diff --git a/env/dev/clj/ireadit/dev_middleware.clj b/env/dev/clj/ireadit/dev_middleware.clj new file mode 100644 index 0000000..b577fe1 --- /dev/null +++ b/env/dev/clj/ireadit/dev_middleware.clj @@ -0,0 +1,10 @@ +(ns ireadit.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 {:app-namespaces ['ireadit]}))) diff --git a/env/dev/clj/ireadit/env.clj b/env/dev/clj/ireadit/env.clj new file mode 100644 index 0000000..7391f78 --- /dev/null +++ b/env/dev/clj/ireadit/env.clj @@ -0,0 +1,14 @@ +(ns ireadit.env + (:require [selmer.parser :as parser] + [clojure.tools.logging :as log] + [ireadit.dev-middleware :refer [wrap-dev]])) + +(def defaults + {:init + (fn [] + (parser/cache-off!) + (log/info "\n-=[ireadit started successfully using the development profile]=-")) + :stop + (fn [] + (log/info "\n-=[ireadit has shut down successfully]=-")) + :middleware wrap-dev}) diff --git a/env/dev/clj/ireadit/figwheel.clj b/env/dev/clj/ireadit/figwheel.clj new file mode 100644 index 0000000..3730e5b --- /dev/null +++ b/env/dev/clj/ireadit/figwheel.clj @@ -0,0 +1,12 @@ +(ns ireadit.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..273168e --- /dev/null +++ b/env/dev/clj/user.clj @@ -0,0 +1,21 @@ +(ns user + (:require [ireadit.config :refer [env]] + [clojure.spec.alpha :as s] + [expound.alpha :as expound] + [mount.core :as mount] + [ireadit.figwheel :refer [start-fw stop-fw cljs]] + [ireadit.core :refer [start-app]])) + +(alter-var-root #'s/*explain-out* (constantly expound/printer)) + +(defn start [] + (mount/start-without #'ireadit.core/repl-server)) + +(defn stop [] + (mount/stop-except #'ireadit.core/repl-server)) + +(defn restart [] + (stop) + (start)) + + diff --git a/env/dev/cljs/ireadit/app.cljs b/env/dev/cljs/ireadit/app.cljs new file mode 100644 index 0000000..b8dcc16 --- /dev/null +++ b/env/dev/cljs/ireadit/app.cljs @@ -0,0 +1,13 @@ +(ns ^:figwheel-no-load ireadit.app + (:require [ireadit.core :as core] + [cljs.spec.alpha :as s] + [expound.alpha :as expound] + [devtools.core :as devtools])) + +(set! s/*explain-out* expound/printer) + +(enable-console-print!) + +(devtools/install!) + +(core/init!) diff --git a/env/dev/resources/config.edn b/env/dev/resources/config.edn new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/env/dev/resources/config.edn @@ -0,0 +1 @@ +{} diff --git a/env/dev/resources/logback.xml b/env/dev/resources/logback.xml new file mode 100644 index 0000000..abe207e --- /dev/null +++ b/env/dev/resources/logback.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> +<configuration scan="true" scanPeriod="10 seconds"> + <statusListener class="ch.qos.logback.core.status.NopStatusListener" /> + <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> + <!-- encoders are assigned the type + ch.qos.logback.classic.encoder.PatternLayoutEncoder by default --> + <encoder> + <charset>UTF-8</charset> + <pattern>%date{ISO8601} [%thread] %-5level %logger{36} - %msg %n</pattern> + </encoder> + </appender> + <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> + <file>log/ireadit.log</file> + <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> + <fileNamePattern>log/ireadit.%d{yyyy-MM-dd}.%i.log</fileNamePattern> + <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> + <maxFileSize>100MB</maxFileSize> + </timeBasedFileNamingAndTriggeringPolicy> + <!-- keep 30 days of history --> + <maxHistory>30</maxHistory> + </rollingPolicy> + <encoder> + <charset>UTF-8</charset> + <pattern>%date{ISO8601} [%thread] %-5level %logger{36} - %msg %n</pattern> + </encoder> + </appender> + <logger name="org.apache.http" level="warn" /> + <logger name="org.xnio.nio" level="warn" /> + <logger name="io.undertow.session" level="warn" /> + <logger name="io.undertow.request" level="warn" /> + <root level="DEBUG"> + <appender-ref ref="STDOUT" /> + <appender-ref ref="FILE" /> + </root> +</configuration> diff --git a/env/prod/clj/ireadit/env.clj b/env/prod/clj/ireadit/env.clj new file mode 100644 index 0000000..3328e4a --- /dev/null +++ b/env/prod/clj/ireadit/env.clj @@ -0,0 +1,11 @@ +(ns ireadit.env + (:require [clojure.tools.logging :as log])) + +(def defaults + {:init + (fn [] + (log/info "\n-=[ireadit started successfully]=-")) + :stop + (fn [] + (log/info "\n-=[ireadit has shut down successfully]=-")) + :middleware identity}) diff --git a/env/prod/cljs/ireadit/app.cljs b/env/prod/cljs/ireadit/app.cljs new file mode 100644 index 0000000..16bcb92 --- /dev/null +++ b/env/prod/cljs/ireadit/app.cljs @@ -0,0 +1,7 @@ +(ns ireadit.app + (:require [ireadit.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..e24ec21 --- /dev/null +++ b/env/prod/resources/config.edn @@ -0,0 +1,2 @@ +{:prod true + :port 3000} diff --git a/env/prod/resources/logback.xml b/env/prod/resources/logback.xml new file mode 100644 index 0000000..fd95528 --- /dev/null +++ b/env/prod/resources/logback.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<configuration> + <statusListener class="ch.qos.logback.core.status.NopStatusListener" /> + <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> + <file>log/ireadit.log</file> + <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> + <fileNamePattern>log/ireadit.%d{yyyy-MM-dd}.%i.log</fileNamePattern> + <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> + <maxFileSize>100MB</maxFileSize> + </timeBasedFileNamingAndTriggeringPolicy> + <!-- keep 30 days of history --> + <maxHistory>30</maxHistory> + </rollingPolicy> + <encoder> + <charset>UTF-8</charset> + <pattern>%date{ISO8601} [%thread] %-5level %logger{36} - %msg %n</pattern> + </encoder> + </appender> + <logger name="org.apache.http" level="warn" /> + <logger name="org.xnio.nio" level="warn" /> + <root level="INFO"> + <appender-ref ref="FILE" /> + </root> +</configuration> diff --git a/env/test/resources/config.edn b/env/test/resources/config.edn new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/env/test/resources/config.edn @@ -0,0 +1 @@ +{} diff --git a/env/test/resources/logback.xml b/env/test/resources/logback.xml new file mode 100644 index 0000000..abe207e --- /dev/null +++ b/env/test/resources/logback.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> +<configuration scan="true" scanPeriod="10 seconds"> + <statusListener class="ch.qos.logback.core.status.NopStatusListener" /> + <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> + <!-- encoders are assigned the type + ch.qos.logback.classic.encoder.PatternLayoutEncoder by default --> + <encoder> + <charset>UTF-8</charset> + <pattern>%date{ISO8601} [%thread] %-5level %logger{36} - %msg %n</pattern> + </encoder> + </appender> + <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> + <file>log/ireadit.log</file> + <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> + <fileNamePattern>log/ireadit.%d{yyyy-MM-dd}.%i.log</fileNamePattern> + <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> + <maxFileSize>100MB</maxFileSize> + </timeBasedFileNamingAndTriggeringPolicy> + <!-- keep 30 days of history --> + <maxHistory>30</maxHistory> + </rollingPolicy> + <encoder> + <charset>UTF-8</charset> + <pattern>%date{ISO8601} [%thread] %-5level %logger{36} - %msg %n</pattern> + </encoder> + </appender> + <logger name="org.apache.http" level="warn" /> + <logger name="org.xnio.nio" level="warn" /> + <logger name="io.undertow.session" level="warn" /> + <logger name="io.undertow.request" level="warn" /> + <root level="DEBUG"> + <appender-ref ref="STDOUT" /> + <appender-ref ref="FILE" /> + </root> +</configuration> diff --git a/project.clj b/project.clj new file mode 100644 index 0000000..4d35bf4 --- /dev/null +++ b/project.clj @@ -0,0 +1,142 @@ +(defproject ireadit "0.1.0-SNAPSHOT" + + :description "FIXME: write description" + :url "http://example.com/FIXME" + + :dependencies [[baking-soda "0.2.0" :exclusions [cljsjs/react-bootstrap]] + [cheshire "5.8.1"] + [clj-commons/secretary "1.2.4"] + [clj-oauth "1.5.5"] + [cljs-ajax "0.8.0"] + [cljsjs/react-popper "0.10.4-0"] + [cljsjs/react-transition-group "2.4.0-0"] + [clojure.java-time "0.3.2"] + [com.cognitect/transit-clj "0.8.313"] + [compojure "1.6.1"] + [cprop "0.1.13"] + [day8.re-frame/http-fx "0.1.6"] + [funcool/struct "1.3.0"] + [luminus-immutant "0.2.5"] + [luminus-transit "0.1.1"] + [luminus/ring-ttl-session "0.3.2"] + [markdown-clj "1.0.7"] + [metosin/compojure-api "2.0.0-alpha28"] + [metosin/muuntaja "0.6.3"] + [metosin/ring-http-response "0.9.1"] + [mount "0.1.16"] + [nrepl "0.6.0"] + [org.clojure/clojure "1.10.0"] + [org.clojure/clojurescript "1.10.520" :scope "provided"] + [org.clojure/tools.cli "0.4.1"] + [org.clojure/tools.logging "0.4.1"] + [org.webjars.bower/tether "1.4.4"] + [org.webjars/bootstrap "4.2.1"] + [org.webjars/font-awesome "5.7.1"] + [org.webjars/webjars-locator "0.36"] + [re-frame "0.10.6"] + [reagent "0.8.1"] + [ring-webjars "0.2.0"] + [ring/ring-core "1.7.1"] + [ring/ring-defaults "0.3.2"] + [selmer "1.12.6"]] + + :min-lein-version "2.0.0" + + :source-paths ["src/clj" "src/cljs" "src/cljc"] + :test-paths ["test/clj"] + :resource-paths ["resources" "target/cljsbuild"] + :target-path "target/%s/" + :main ^:skip-aot ireadit.core + + :plugins [[lein-cljsbuild "1.1.7"] + [lein-immutant "2.1.0"]] + :clean-targets ^{:protect false} + [:target-path [:cljsbuild :builds :app :compiler :output-dir] [:cljsbuild :builds :app :compiler :output-to]] + :figwheel + {:http-server-root "public" + :server-logfile "log/figwheel-logfile.log" + :nrepl-port 7002 + :css-dirs ["resources/public/css"] + :nrepl-middleware [cider.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-dir "target/cljsbuild/public/js" + :output-to "target/cljsbuild/public/js/app.js" + :source-map "target/cljsbuild/public/js/app.js.map" + :optimizations :advanced + :pretty-print false + :infer-externs true + :closure-warnings + {:externs-validation :off :non-standard-jsdoc :off} + :externs ["react/externs/react.js"]}}}} + + + :aot :all + :uberjar-name "ireadit.jar" + :source-paths ["env/prod/clj"] + :resource-paths ["env/prod/resources"]} + + :dev [:project/dev :profiles/dev] + :test [:project/dev :project/test :profiles/test] + + :project/dev {:jvm-opts ["-Dconf=dev-config.edn"] + :dependencies [[binaryage/devtools "0.9.10"] + [cider/piggieback "0.4.0"] + [doo "0.1.11"] + [expound "0.7.2"] + [figwheel-sidecar "0.5.18"] + [pjstadig/humane-test-output "0.9.0"] + [prone "1.6.1"] + [re-frisk "0.5.4"] + [ring/ring-devel "1.7.1"] + [ring/ring-mock "0.3.2"]] + :plugins [[com.jakemccrary/lein-test-refresh "0.23.0"] + [lein-doo "0.1.11"] + [lein-figwheel "0.5.18"]] + :cljsbuild + {:builds + {:app + {:source-paths ["src/cljs" "src/cljc" "env/dev/cljs"] + :figwheel {:on-jsload "ireadit.core/mount-components"} + :compiler + {:main "ireadit.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 + :closure-defines {"re_frame.trace.trace_enabled_QMARK_" true} + :preloads [re-frisk.preload]}}}} + + + + :doo {:build "test"} + :source-paths ["env/dev/clj"] + :resource-paths ["env/dev/resources"] + :repl-options {:init-ns user} + :injections [(require 'pjstadig.humane-test-output) + (pjstadig.humane-test-output/activate!)]} + :project/test {:jvm-opts ["-Dconf=test-config.edn"] + :resource-paths ["env/test/resources"] + :cljsbuild + {:builds + {:test + {:source-paths ["src/cljc" "src/cljs" "test/cljs"] + :compiler + {:output-to "target/test.js" + :main "ireadit.doo-runner" + :optimizations :whitespace + :pretty-print true}}}} + + } + :profiles/dev {} + :profiles/test {}}) diff --git a/resources/docs/docs.md b/resources/docs/docs.md new file mode 100644 index 0000000..2a81d92 --- /dev/null +++ b/resources/docs/docs.md @@ -0,0 +1,82 @@ +<h2 class="alert alert-success">Congratulations, your <a class="alert-link" href="http://luminusweb.net">Luminus</a> site is ready!</h2> + +This page will help guide you through the first steps of building your site. + +#### Why are you seeing this page? + +The `home-routes` handler in the `ireadit.routes.home` namespace +defines the route that invokes the `home-page` function whenever an HTTP +request is made to the `/` URI using the `GET` method. + +``` +(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")))) +``` + +The `home-page` function will in turn call the `ireadit.layout/render` function +to render the HTML content: + +``` +(defn home-page [] + (layout/render "home.html")) +``` + +The page contains a link to the compiled ClojureScript found in the `target/cljsbuild/public` folder: + +``` +{% script "/js/app.js" %} +``` + +The rest of this page is rendered by ClojureScript found in the `src/cljs/ireadit/core.cljs` file. + + + +#### Organizing the routes + +The routes are aggregated and wrapped with middleware in the `ireadit.handler` namespace: + +``` +(defstate app + :start + (middleware/wrap-base + (routes + (-> #'home-routes + (wrap-routes middleware/wrap-csrf) + (wrap-routes middleware/wrap-formats)) + (route/not-found + (:body + (error-page {:status 404 + :title "page not found"})))))) +``` + +The `app` definition groups all the routes in the application into a single handler. +A default route group is added to handle the `404` case. + +<a class="btn btn-primary" href="http://www.luminusweb.net/docs/routes.md">learn more about routing »</a> + +The `home-routes` are wrapped with two middleware functions. The first enables CSRF protection. +The second takes care of serializing and deserializing various encoding formats, such as JSON. + +#### Managing your middleware + +Request middleware functions are located under the `ireadit.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 `ireadit.dev-middleware` namespace found in +the `env/dev/clj/` source path. + +<a class="btn btn-primary" href="http://www.luminusweb.net/docs/middleware.md">learn more about middleware »</a> + + + + +#### Need some help? + +Visit the [official documentation](http://www.luminusweb.net/docs) for examples +on how to accomplish common tasks with Luminus. The `#luminus` channel on the [Clojurians Slack](http://clojurians.net/) and [Google Group](https://groups.google.com/forum/#!forum/luminusweb) are both great places to seek help and discuss projects with other users. diff --git a/resources/html/error.html b/resources/html/error.html new file mode 100644 index 0000000..31dd41d --- /dev/null +++ b/resources/html/error.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<html> +<head> + <title>Something bad happened</title> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + {% style "/assets/bootstrap/css/bootstrap.min.css" %} + <style type="text/css"> + html { + height: 100%; + min-height: 100%; + min-width: 100%; + overflow: hidden; + width: 100%; + } + html body { + height: 100%; + margin: 0; + padding: 0; + width: 100%; + } + html .container-fluid { + display: table; + height: 100%; + padding: 0; + width: 100%; + } + html .row-fluid { + display: table-cell; + height: 100%; + vertical-align: middle; + } + </style> +</head> +<body> +<div class="container-fluid"> + <div class="row-fluid"> + <div class="col-lg-12"> + <div class="centering text-center"> + <div class="text-center"> + <h1><span class="text-danger">Error: {{status}}</span></h1> + <hr> + {% if title %} + <h2 class="without-margin">{{title}}</h2> + {% endif %} + {% if message %} + <h4 class="text-danger">{{message}}</h4> + {% endif %} + </div> + </div> + </div> + </div> +</div> +</body> +</html> diff --git a/resources/html/home.html b/resources/html/home.html new file mode 100644 index 0000000..9b20b69 --- /dev/null +++ b/resources/html/home.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="UTF-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <title>Welcome to ireadit</title> + </head> + <body> + + <div id="app"> + <div class="container-fluid"> + <div class="card-deck"> + <div class="card-block"> + <h4>Welcome to ireadit</h4> + <p>If you're seeing this message, that means you haven't yet compiled your ClojureScript!</p> + <p>Please run <code>lein figwheel</code> to start the ClojureScript compiler and reload the page.</p> + <h4>For better ClojureScript development experience in Chrome follow these steps:</h4> + <ul> + <li>Open DevTools + <li>Go to Settings ("three dots" icon in the upper right corner of DevTools > Menu > Settings F1 > General > Console) + <li>Check-in "Enable custom formatters" + <li>Close DevTools + <li>Open DevTools + </ul> + <p>See <a href="http://www.luminusweb.net/docs/clojurescript.md">ClojureScript</a> documentation for further details.</p> + </div> + </div> + </div> + </div> + + <!-- scripts and styles --> + {% style "/assets/bootstrap/css/bootstrap.min.css" %} + {% style "/assets/font-awesome/css/all.css" %} + {% style "/css/screen.css" %} + + {% script "/assets/jquery/jquery.min.js" %} + {% script "/assets/font-awesome/js/all.js" %} + {% script "/assets/tether/dist/js/tether.min.js" %} + {% script "/assets/bootstrap/js/bootstrap.min.js" %} + + <script type="text/javascript"> + var csrfToken = "{{csrf-token}}"; + </script> + {% script "/js/app.js" %} + </body> +</html> diff --git a/resources/public/css/screen.css b/resources/public/css/screen.css new file mode 100644 index 0000000..c64d9c0 --- /dev/null +++ b/resources/public/css/screen.css @@ -0,0 +1,9 @@ +html, +body { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + height: 100%; +} +nav { + margin-bottom: 20px; +} + diff --git a/resources/public/favicon.ico b/resources/public/favicon.ico new file mode 100644 index 0000000..0e50cb2 Binary files /dev/null and b/resources/public/favicon.ico differ diff --git a/resources/public/img/warning_clojure.png b/resources/public/img/warning_clojure.png new file mode 100644 index 0000000..78d59e9 Binary files /dev/null and b/resources/public/img/warning_clojure.png differ diff --git a/src/clj/ireadit/config.clj b/src/clj/ireadit/config.clj new file mode 100644 index 0000000..d3cb471 --- /dev/null +++ b/src/clj/ireadit/config.clj @@ -0,0 +1,12 @@ +(ns ireadit.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/ireadit/core.clj b/src/clj/ireadit/core.clj new file mode 100644 index 0000000..0e1a3c4 --- /dev/null +++ b/src/clj/ireadit/core.clj @@ -0,0 +1,49 @@ +(ns ireadit.core + (:require [ireadit.handler :as handler] + [ireadit.nrepl :as nrepl] + [luminus.http-server :as http] + [ireadit.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 :io-threads #(or % (* 2 (.availableProcessors (Runtime/getRuntime))))) + (update :port #(or (-> env :options :port) %)))) + :stop + (http/stop http-server)) + +(mount/defstate ^{:on-reload :noop} repl-server + :start + (when (env :nrepl-port) + (nrepl/start {:bind (env :nrepl-bind) + :port (env :nrepl-port)})) + :stop + (when repl-server + (nrepl/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] + (start-app args)) diff --git a/src/clj/ireadit/handler.clj b/src/clj/ireadit/handler.clj new file mode 100644 index 0000000..00f0f41 --- /dev/null +++ b/src/clj/ireadit/handler.clj @@ -0,0 +1,30 @@ +(ns ireadit.handler + (:require [ireadit.middleware :as middleware] + [ireadit.layout :refer [error-page]] + [ireadit.routes.home :refer [home-routes]] + [ireadit.routes.services :refer [service-routes]] + [ireadit.routes.oauth :refer [oauth-routes]] + [compojure.core :refer [routes wrap-routes]] + [ring.util.http-response :as response] + [compojure.route :as route] + [ireadit.env :refer [defaults]] + [mount.core :as mount])) + +(mount/defstate init-app + :start ((or (:init defaults) identity)) + :stop ((or (:stop defaults) identity))) + +(mount/defstate app + :start + (middleware/wrap-base + (routes + (-> #'home-routes + (wrap-routes middleware/wrap-csrf) + (wrap-routes middleware/wrap-formats)) + #'oauth-routes + #'service-routes + (route/not-found + (:body + (error-page {:status 404 + :title "page not found"})))))) + diff --git a/src/clj/ireadit/layout.clj b/src/clj/ireadit/layout.clj new file mode 100644 index 0000000..f0ba9ce --- /dev/null +++ b/src/clj/ireadit/layout.clj @@ -0,0 +1,37 @@ +(ns ireadit.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*]])) + + +(parser/set-resource-path! (clojure.java.io/resource "html")) +(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/html" + [template & [params]] + (content-type + (ok + (parser/render-file + template + (assoc params + :page template + :csrf-token *anti-forgery-token*))) + "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/ireadit/middleware.clj b/src/clj/ireadit/middleware.clj new file mode 100644 index 0000000..3c9d54e --- /dev/null +++ b/src/clj/ireadit/middleware.clj @@ -0,0 +1,53 @@ +(ns ireadit.middleware + (:require [ireadit.env :refer [defaults]] + [cheshire.generate :as cheshire] + [cognitect.transit :as transit] + [clojure.tools.logging :as log] + [ireadit.layout :refer [error-page]] + [ring.middleware.anti-forgery :refer [wrap-anti-forgery]] + [ring.middleware.webjars :refer [wrap-webjars]] + [ireadit.middleware.formats :as formats] + [muuntaja.middleware :refer [wrap-format wrap-params]] + [ireadit.config :refer [env]] + [ring.middleware.flash :refer [wrap-flash]] + [immutant.web.middleware :refer [wrap-session]] + [ring.middleware.defaults :refer [site-defaults wrap-defaults]]) + (:import + )) + +(defn wrap-internal-error [handler] + (fn [req] + (try + (handler req) + (catch Throwable t + (log/error t (.getMessage 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 (-> handler wrap-params (wrap-format formats/instance))] + (fn [request] + ;; disable wrap-formats for websockets + ;; since they're not compatible with this middleware + ((if (:websocket? request) handler wrapped) request)))) + +(defn wrap-base [handler] + (-> ((:middleware defaults) handler) + wrap-webjars + wrap-flash + (wrap-session {:cookie-attrs {:http-only true}}) + (wrap-defaults + (-> site-defaults + (assoc-in [:security :anti-forgery] false) + (dissoc :session))) + wrap-internal-error)) diff --git a/src/clj/ireadit/middleware/formats.clj b/src/clj/ireadit/middleware/formats.clj new file mode 100644 index 0000000..1b6facc --- /dev/null +++ b/src/clj/ireadit/middleware/formats.clj @@ -0,0 +1,14 @@ +(ns ireadit.middleware.formats + (:require [cognitect.transit :as transit] + [luminus-transit.time :as time] + [muuntaja.core :as m])) + +(def instance + (m/create + (-> m/default-options + (update-in + [:formats "application/transit+json" :decoder-opts] + (partial merge time/time-deserialization-handlers)) + (update-in + [:formats "application/transit+json" :encoder-opts] + (partial merge time/time-serialization-handlers))))) diff --git a/src/clj/ireadit/nrepl.clj b/src/clj/ireadit/nrepl.clj new file mode 100644 index 0000000..0ad85db --- /dev/null +++ b/src/clj/ireadit/nrepl.clj @@ -0,0 +1,26 @@ +(ns ireadit.nrepl + (:require [nrepl.server :as nrepl] + [clojure.tools.logging :as log])) + +(defn start + "Start a network repl for debugging on specified port followed by + an optional parameters map. The :bind, :transport-fn, :handler, + :ack-port and :greeting-fn will be forwarded to + clojure.tools.nrepl.server/start-server as they are." + [{:keys [port bind transport-fn handler ack-port greeting-fn]}] + (try + (log/info "starting nREPL server on port" port) + (nrepl/start-server :port port + :bind bind + :transport-fn transport-fn + :handler handler + :ack-port ack-port + :greeting-fn greeting-fn) + + (catch Throwable t + (log/error t "failed to start nREPL") + (throw t)))) + +(defn stop [server] + (nrepl/stop-server server) + (log/info "nREPL server stopped")) diff --git a/src/clj/ireadit/oauth.clj b/src/clj/ireadit/oauth.clj new file mode 100644 index 0000000..6102f62 --- /dev/null +++ b/src/clj/ireadit/oauth.clj @@ -0,0 +1,35 @@ +(ns ireadit.oauth + (:require [ireadit.config :refer [env]] + [oauth.client :as oauth] + [mount.core :refer [defstate]] + [clojure.tools.logging :as log])) + +(defstate consumer + :start (oauth/make-consumer + (env :oauth-consumer-key) + (env :oauth-consumer-secret) + (env :request-token-uri) + (env :access-token-uri) + (env :authorize-uri) + :hmac-sha1)) + +(defn oauth-callback-uri + "Generates the oauth request callback URI" + [{:keys [headers]}] + (str (headers "x-forwarded-proto") "://" (headers "host") "/oauth/oauth-callback")) + +(defn fetch-request-token + "Fetches a request token." + [request] + (let [callback-uri (oauth-callback-uri request)] + (log/info "Fetching request token using callback-uri" callback-uri) + (oauth/request-token consumer (oauth-callback-uri request)))) + +(defn fetch-access-token + [request_token] + (oauth/access-token consumer request_token (:oauth_verifier request_token))) + +(defn auth-redirect-uri + "Gets the URI the user should be redirected to when authenticating." + [request-token] + (str (oauth/user-approval-uri consumer request-token))) diff --git a/src/clj/ireadit/routes/home.clj b/src/clj/ireadit/routes/home.clj new file mode 100644 index 0000000..fcbf44d --- /dev/null +++ b/src/clj/ireadit/routes/home.clj @@ -0,0 +1,16 @@ +(ns ireadit.routes.home + (:require [ireadit.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/ireadit/routes/oauth.clj b/src/clj/ireadit/routes/oauth.clj new file mode 100644 index 0000000..1498cc1 --- /dev/null +++ b/src/clj/ireadit/routes/oauth.clj @@ -0,0 +1,34 @@ +(ns ireadit.routes.oauth + (:require [ring.util.http-response :refer [ok found]] + [compojure.core :refer [defroutes GET]] + [clojure.java.io :as io] + [ireadit.oauth :as oauth] + [clojure.tools.logging :as log])) + +(defn oauth-init + "Initiates the Twitter OAuth" + [request] + (-> (oauth/fetch-request-token request) + :oauth_token + oauth/auth-redirect-uri + found)) + +(defn oauth-callback + "Handles the callback from Twitter." + [{:keys [session params]}] + ; oauth request was denied by user + (if (:denied params) + (-> (found "/") + (assoc :flash {:denied true})) + ; fetch the request token and do anything else you wanna do if not denied. + (let [{:keys [user_id screen_name]} (oauth/fetch-access-token params)] + (log/info "successfully authenticated as" user_id screen_name) + (-> (found "/") + (assoc :session + (assoc session :user-id user_id :screen-name screen_name)))))) + + +(defroutes oauth-routes + (GET "/oauth/oauth-init" req (oauth-init req)) + (GET "/oauth/oauth-callback" [& req_token :as req] (oauth-callback req))) + diff --git a/src/clj/ireadit/routes/services.clj b/src/clj/ireadit/routes/services.clj new file mode 100644 index 0000000..660d6c6 --- /dev/null +++ b/src/clj/ireadit/routes/services.clj @@ -0,0 +1,45 @@ +(ns ireadit.routes.services + (:require [ring.util.http-response :refer :all] + [compojure.api.sweet :refer :all] + [schema.core :as s])) + +(def service-routes + (api + {:swagger {:ui "/swagger-ui" + :spec "/swagger.json" + :data {:info {:version "1.0.0" + :title "Sample API" + :description "Sample Services"}}}} + + (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/ireadit/validation.cljc b/src/cljc/ireadit/validation.cljc new file mode 100644 index 0000000..7848ef0 --- /dev/null +++ b/src/cljc/ireadit/validation.cljc @@ -0,0 +1,2 @@ +(ns ireadit.validation + (:require [struct.core :as st])) diff --git a/src/cljs/ireadit/ajax.cljs b/src/cljs/ireadit/ajax.cljs new file mode 100644 index 0000000..c481c93 --- /dev/null +++ b/src/cljs/ireadit/ajax.cljs @@ -0,0 +1,29 @@ +(ns ireadit.ajax + (:require [ajax.core :as ajax] + [luminus-transit.time :as time] + [cognitect.transit :as transit] + [re-frame.core :as rf])) + +(defn local-uri? [{:keys [uri]}] + (not (re-find #"^\w+?://" uri))) + +(defn default-headers [request] + (if (local-uri? request) + (-> request + (update :headers #(merge {"x-csrf-token" js/csrfToken} %))) + request)) + +;; injects transit serialization config into request options +(defn as-transit [opts] + (merge {:raw false + :format :transit + :response-format :transit + :reader (transit/reader :json time/time-deserialization-handlers) + :writer (transit/writer :json time/time-serialization-handlers)} + opts)) + +(defn load-interceptors! [] + (swap! ajax/default-interceptors + conj + (ajax/to-interceptor {:name "default headers" + :request default-headers}))) diff --git a/src/cljs/ireadit/core.cljs b/src/cljs/ireadit/core.cljs new file mode 100644 index 0000000..9161c4a --- /dev/null +++ b/src/cljs/ireadit/core.cljs @@ -0,0 +1,94 @@ +(ns ireadit.core + (:require [baking-soda.core :as b] + [day8.re-frame.http-fx] + [reagent.core :as r] + [re-frame.core :as rf] + [goog.events :as events] + [goog.history.EventType :as HistoryEventType] + [markdown.core :refer [md->html]] + [ireadit.ajax :as ajax] + [ireadit.events] + [secretary.core :as secretary]) + (:import goog.History)) + +; the navbar components are implemented via baking-soda [1] +; library that provides a ClojureScript interface for Reactstrap [2] +; Bootstrap 4 components. +; [1] https://github.com/gadfly361/baking-soda +; [2] http://reactstrap.github.io/ + +(defn nav-link [uri title page] + [b/NavItem + [b/NavLink + {:href uri + :active (when (= page @(rf/subscribe [:page])) "active")} + title]]) + +(defn navbar [] + (r/with-let [expanded? (r/atom true)] + [b/Navbar {:light true + :class-name "navbar-dark bg-primary" + :expand "md"} + [b/NavbarBrand {:href "/"} "ireadit"] + [b/NavbarToggler {:on-click #(swap! expanded? not)}] + [b/Collapse {:is-open @expanded? :navbar true} + [b/Nav {:class-name "mr-auto" :navbar true} + [nav-link "#/" "Home" :home] + [nav-link "#/about" "About" :about]]]])) + +(defn about-page [] + [:div.container + [:div.row + [:div.col-md-12 + [:img {:src "/img/warning_clojure.png"}]]]]) + +(defn home-page [] + [:div.container + (when-let [docs @(rf/subscribe [:docs])] + [:div.row>div.col-sm-12 + [:div {:dangerouslySetInnerHTML + {:__html (md->html docs)}}]])]) + +(def pages + {:home #'home-page + :about #'about-page}) + +(defn page [] + [:div + [navbar] + [(pages @(rf/subscribe [:page]))]]) + +;; ------------------------- +;; Routes + +(secretary/set-config! :prefix "#") + +(secretary/defroute "/" [] + (rf/dispatch [:navigate :home])) + +(secretary/defroute "/about" [] + (rf/dispatch [:navigate :about])) + +;; ------------------------- +;; 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 [] + (rf/clear-subscription-cache!) + (r/render [#'page] (.getElementById js/document "app"))) + +(defn init! [] + (rf/dispatch-sync [:navigate :home]) + (ajax/load-interceptors!) + (rf/dispatch [:fetch-docs]) + (hook-browser-navigation!) + (mount-components)) diff --git a/src/cljs/ireadit/events.cljs b/src/cljs/ireadit/events.cljs new file mode 100644 index 0000000..18b8ef9 --- /dev/null +++ b/src/cljs/ireadit/events.cljs @@ -0,0 +1,45 @@ +(ns ireadit.events + (:require [re-frame.core :as rf] + [ajax.core :as ajax])) + +;;dispatchers + +(rf/reg-event-db + :navigate + (fn [db [_ page]] + (assoc db :page page))) + +(rf/reg-event-db + :set-docs + (fn [db [_ docs]] + (assoc db :docs docs))) + +(rf/reg-event-fx + :fetch-docs + (fn [_ _] + {:http-xhrio {:method :get + :uri "/docs" + :response-format (ajax/raw-response-format) + :on-success [:set-docs]}})) + +(rf/reg-event-db + :common/set-error + (fn [db [_ error]] + (assoc db :common/error error))) + +;;subscriptions + +(rf/reg-sub + :page + (fn [db _] + (:page db))) + +(rf/reg-sub + :docs + (fn [db _] + (:docs db))) + +(rf/reg-sub + :common/error + (fn [db _] + (:common/error db))) diff --git a/test/clj/ireadit/test/handler.clj b/test/clj/ireadit/test/handler.clj new file mode 100644 index 0000000..5818c9e --- /dev/null +++ b/test/clj/ireadit/test/handler.clj @@ -0,0 +1,26 @@ +(ns ireadit.test.handler + (:require [clojure.test :refer :all] + [ring.mock.request :refer :all] + [ireadit.handler :refer :all] + [ireadit.middleware.formats :as formats] + [muuntaja.core :as m] + [mount.core :as mount])) + +(defn parse-json [body] + (m/decode formats/instance "application/json" body)) + +(use-fixtures + :once + (fn [f] + (mount/start #'ireadit.config/env + #'ireadit.handler/app) + (f))) + +(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/ireadit/core_test.cljs b/test/cljs/ireadit/core_test.cljs new file mode 100644 index 0000000..94d2701 --- /dev/null +++ b/test/cljs/ireadit/core_test.cljs @@ -0,0 +1,9 @@ +(ns ireadit.core-test + (:require [cljs.test :refer-macros [is are deftest testing use-fixtures]] + [pjstadig.humane-test-output] + [reagent.core :as reagent :refer [atom]] + [ireadit.core :as rc])) + +(deftest test-home + (is (= true true))) + diff --git a/test/cljs/ireadit/doo_runner.cljs b/test/cljs/ireadit/doo_runner.cljs new file mode 100644 index 0000000..c19c3fd --- /dev/null +++ b/test/cljs/ireadit/doo_runner.cljs @@ -0,0 +1,6 @@ +(ns ireadit.doo-runner + (:require [doo.runner :refer-macros [doo-tests]] + [ireadit.core-test])) + +(doo-tests 'ireadit.core-test) +