Added a boilerplate luminus project

+re-frame +swagger +oauth - probably this is massive overkill
This commit is contained in:
Simon Brooke 2019-02-17 12:35:35 +00:00
parent 94abf7aae8
commit bad860f78e
42 changed files with 1132 additions and 0 deletions

14
.gitignore vendored Normal file
View file

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

28
Capstanfile Normal file
View file

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

7
Dockerfile Normal file
View file

@ -0,0 +1,7 @@
FROM openjdk:8-alpine
COPY target/uberjar/ireadit.jar /ireadit/app.jar
EXPOSE 3000
CMD ["java", "-jar", "/ireadit/app.jar"]

1
Procfile Normal file
View file

@ -0,0 +1 @@
web: java -cp target/uberjar/ireadit.jar clojure.main -m ireadit.core

10
env/dev/clj/ireadit/dev_middleware.clj vendored Normal file
View file

@ -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]})))

14
env/dev/clj/ireadit/env.clj vendored Normal file
View file

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

12
env/dev/clj/ireadit/figwheel.clj vendored Normal file
View file

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

21
env/dev/clj/user.clj vendored Normal file
View file

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

13
env/dev/cljs/ireadit/app.cljs vendored Normal file
View file

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

1
env/dev/resources/config.edn vendored Normal file
View file

@ -0,0 +1 @@
{}

35
env/dev/resources/logback.xml vendored Normal file
View file

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

11
env/prod/clj/ireadit/env.clj vendored Normal file
View file

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

7
env/prod/cljs/ireadit/app.cljs vendored Normal file
View file

@ -0,0 +1,7 @@
(ns ireadit.app
(:require [ireadit.core :as core]))
;;ignore println statements in prod
(set! *print-fn* (fn [& _]))
(core/init!)

2
env/prod/resources/config.edn vendored Normal file
View file

@ -0,0 +1,2 @@
{:prod true
:port 3000}

24
env/prod/resources/logback.xml vendored Normal file
View file

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

1
env/test/resources/config.edn vendored Normal file
View file

@ -0,0 +1 @@
{}

35
env/test/resources/logback.xml vendored Normal file
View file

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

142
project.clj Normal file
View file

@ -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 {}})

82
resources/docs/docs.md Normal file
View file

@ -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.

55
resources/html/error.html Normal file
View file

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

46
resources/html/home.html Normal file
View file

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

View file

@ -0,0 +1,9 @@
html,
body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
height: 100%;
}
nav {
margin-bottom: 20px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View file

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

49
src/clj/ireadit/core.clj Normal file
View file

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

View file

@ -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"}))))))

View file

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

View file

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

View file

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

26
src/clj/ireadit/nrepl.clj Normal file
View file

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

35
src/clj/ireadit/oauth.clj Normal file
View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,2 @@
(ns ireadit.validation
(:require [struct.core :as st]))

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,6 @@
(ns ireadit.doo-runner
(:require [doo.runner :refer-macros [doo-tests]]
[ireadit.core-test]))
(doo-tests 'ireadit.core-test)