diff --git a/examples/class-registry/README.md b/examples/class-registry/README.md index 7581b63..c5a5c7c 100644 --- a/examples/class-registry/README.md +++ b/examples/class-registry/README.md @@ -1,19 +1,16 @@ # views.reagent Example - Class Registry -This is a "Class Registry" application that has a lot of CRUD operations -in it which allow users to manage students and professors, as well as -classes and then assign the students/professors to those classes. The -idea is _very_ loosely based off one of the Om tutorial applications -(the data used is almost identical). +This is a "Class Registry" application that has a lot of CRUD operations in it which allow users +to manage students and professors, as well as classes and then assign the students/professors to +those classes. The idea is _very_ loosely based off one of the Om tutorial applications (the data +used is almost identical). -Note that this example is somewhat complicated as there are several -lists of data shown in the UI, all of which are completely editable -to the user. While the code is longer as a result, this still serves -as a more interesting example with several views being used (some -using parameters). +Note that this example is somewhat complicated as there are several lists of data shown in the UI, +all of which are completely editable to the user. While the code is longer as a result, this still +serves as a more interesting example with several views being used (some using parameters). -Definitely take a look at the [Todo MVC][1] example app before diving into -this and also be sure you're familiar with Reagent. +Definitely take a look at the [Todo MVC][1] example app before diving into this and also be sure +you're familiar with Reagent. [1]: https://github.com/gered/views.reagent/tree/master/examples/todomvc @@ -21,9 +18,16 @@ this and also be sure you're familiar with Reagent. ### Creating the Database -This example app uses a PostgreSQL database. The SQL script to create -it is in `create_db.sql`. You can easily pipe it into `psql` at a -command line to create it quickly, for example: +This example app uses a PostgreSQL database. The SQL script to create it is in `create_db.sql`. + +A Docker compose file `pgsql.docker-compose.yml` is provided and pre-configured to allow you to +quickly spin up a PostgreSQL database that will be pre-initialized via `create_db.sql` through +Docker. + + $ docker-compose -f pgsql.docker-compose.yml up + +Alternatively, if you already have a PostgreSQL database available, you can run the +`create_db.sql` via `psql` easily enough: $ psql < create_db.sql @@ -34,12 +38,10 @@ command line to create it quickly, for example: To build everything and run in one step: $ lein rundemo - -Then open up a web browser or two and head to http://localhost:8080/ -to see the web app in action. -If you want to run this application in a REPL, just be sure to build -the ClojureScript: +Then open up a web browser or two and head to http://localhost:8080/ to see the web app in action. + +If you want to run this application in a REPL, just be sure to build the ClojureScript: $ lein cljsbuild once @@ -47,5 +49,4 @@ And then in the REPL you can just run: (-main) -to start the web app (you should be put in the correct namespace -immediately). +to start the web app (you should be put in the correct namespace immediately). diff --git a/examples/class-registry/project.clj b/examples/class-registry/project.clj index 738a894..2e084fc 100644 --- a/examples/class-registry/project.clj +++ b/examples/class-registry/project.clj @@ -1,29 +1,28 @@ (defproject class-registry "0.1.0-SNAPSHOT" - :dependencies [[org.clojure/clojure "1.8.0"] - [org.clojure/clojurescript "1.8.51"] - [ring "1.4.0"] - [ring/ring-defaults "0.2.0" :exclusions [javax.servlet/servlet-api]] - [ring-middleware-format "0.7.0"] - [compojure "1.4.0"] - [org.immutant/web "2.1.4"] + :dependencies [[org.clojure/clojure "1.10.3"] + [org.clojure/clojurescript "1.10.773"] + [ring "1.9.4"] + [ring/ring-defaults "0.3.3" :exclusions [javax.servlet/servlet-api]] + [ring-middleware-format "0.7.4"] + [compojure "1.6.2"] + [org.immutant/web "2.1.10"] - [org.clojure/java.jdbc "0.6.1"] - [org.postgresql/postgresql "9.4.1208.jre7"] - [com.taoensso/sente "1.8.1"] - [gered/views "1.5"] - [gered/views.sql "0.1"] - [gered/views.reagent "0.1"] - [gered/views.reagent.sente "0.1"] + [org.clojure/java.jdbc "0.7.12"] + [org.postgresql/postgresql "42.3.1"] + [com.taoensso/sente "1.16.2"] + [net.gered/views "1.6-SNAPSHOT"] + [net.gered/views.sql "0.2-SNAPSHOT"] + [net.gered/views.reagent "0.2-SNAPSHOT"] + [net.gered/views.reagent.sente "0.2-SNAPSHOT"] [hiccup "1.0.5"] - [reagent "0.6.0-alpha2"] + [reagent "1.1.0"] + [cljs-ajax "0.8.4"] [cljsjs/bootstrap "3.3.6-1"] - [cljs-ajax "0.5.4"] + [cljsjs/react "17.0.2-0"] + [cljsjs/react-dom "17.0.2-0"]] - [environ "1.0.3"]] - - :plugins [[lein-cljsbuild "1.1.3"] - [lein-environ "1.0.3"]] + :plugins [[lein-cljsbuild "1.1.8"]] :main class-registry.server @@ -40,10 +39,9 @@ :optimizations :none :pretty-print true}}}} - :profiles {:dev {:env {:dev "true"}} + :profiles {:dev {} - :uberjar {:env {} - :aot :all + :uberjar {:aot :all :hooks [leiningen.cljsbuild] :cljsbuild {:jar true :builds {:main diff --git a/examples/class-registry/src/class_registry/client.cljs b/examples/class-registry/src/class_registry/client.cljs index eb28822..bbb4809 100644 --- a/examples/class-registry/src/class_registry/client.cljs +++ b/examples/class-registry/src/class_registry/client.cljs @@ -2,7 +2,8 @@ (:require [clojure.string :as string] [reagent.core :as r] - [ajax.core :refer [POST default-interceptors to-interceptor]] + [reagent.dom :as rdom] + [ajax.core :as ajax] [taoensso.sente :as sente] [views.reagent.client.component :refer [view-cursor] :refer-macros [defvc]] [views.reagent.sente.client :as vr])) @@ -31,20 +32,20 @@ ;; AJAX actions -(defn add-person! [person] (POST "/people/add" {:params {:person person}})) -(defn save-person! [person] (POST "/people/update" {:params {:person person}})) -(defn delete-person! [id] (POST "/people/delete" {:params {:id id}})) -(defn add-class! [class] (POST "/class/add" {:params {:class class}})) -(defn save-class! [class] (POST "/class/update" {:params {:class class}})) -(defn delete-class! [id] (POST "/class/delete" {:params {:id id}})) +(defn add-person! [person] (ajax/POST "/people/add" {:params {:person person}})) +(defn save-person! [person] (ajax/POST "/people/update" {:params {:person person}})) +(defn delete-person! [id] (ajax/POST "/people/delete" {:params {:id id}})) +(defn add-class! [class] (ajax/POST "/class/add" {:params {:class class}})) +(defn save-class! [class] (ajax/POST "/class/update" {:params {:class class}})) +(defn delete-class! [id] (ajax/POST "/class/delete" {:params {:id id}})) (defn add-registration! [class-id people-id] - (POST "/registry/add" {:params {:class-id class-id :people-id people-id}})) + (ajax/POST "/registry/add" {:params {:class-id class-id :people-id people-id}})) (defn remove-registration! [id] - (POST "/registry/remove" {:params {:id id}})) + (ajax/POST "/registry/remove" {:params {:id id}})) @@ -363,35 +364,34 @@ -;; AJAX CSRF stuff - -(defn get-anti-forgery-token - [] - (if-let [hidden-field (.getElementById js/document "__anti-forgery-token")] - (.-value hidden-field))) - -(def csrf-interceptor - (to-interceptor {:name "CSRF Interceptor" - :request #(assoc-in % [:headers "X-CSRF-Token"] (get-anti-forgery-token))})) - -(swap! default-interceptors (partial cons csrf-interceptor)) - - - ;; Sente event/message handler (defn sente-event-msg-handler [{:keys [event id client-id] :as ev}] - (let [[ev-id ev-data] event] - (cond - (and (= :chsk/state ev-id) - (:open? ev-data)) - (vr/on-open! @sente-socket ev) + (cond + (vr/chsk-open-event? ev) + (vr/on-open! @sente-socket ev) - (= :chsk/recv id) - (when-not (vr/on-receive! @sente-socket ev) - ; TODO: any code here needed to handle app-specific receive events - )))) + (= :chsk/recv id) + (when-not (vr/on-receive! @sente-socket ev) + ; TODO: any code here needed to handle app-specific receive events + ))) + + + +;; Utility functions for dealing with CSRF Token garbage in AJAX requests. + +(defn get-csrf-token + [] + (when-let [csrf-token-element (.querySelector js/document "meta[name=\"csrf-token\"]")] + (.-content csrf-token-element))) + +(defn add-csrf-token-ajax-interceptor! + [csrf-token] + (let [interceptor (ajax/to-interceptor + {:name "CSRF Interceptor" + :request #(assoc-in % [:headers "X-CSRF-Token"] csrf-token)})] + (swap! ajax/default-interceptors #(cons interceptor %)))) @@ -399,9 +399,19 @@ (defn ^:export run [] - (reset! sente-socket (sente/make-channel-socket! "/chsk" {})) - (sente/start-chsk-router! (:ch-recv @sente-socket) sente-event-msg-handler) + (enable-console-print!) - (vr/init! @sente-socket {}) + (let [csrf-token (get-csrf-token)] + (if csrf-token (add-csrf-token-ajax-interceptor! csrf-token)) - (r/render-component [class-registry-app] (.getElementById js/document "app"))) + ; Sente setup. create the socket, storing it in an atom and set up a event + ; handler using sente's own message router functionality. + (reset! sente-socket (sente/make-channel-socket! "/chsk" csrf-token {})) + + ; set up a handler for sente events + (sente/start-chsk-router! (:ch-recv @sente-socket) sente-event-msg-handler) + + ; Configure views.reagent for use with Sente. + (vr/init! @sente-socket {}) + + (rdom/render [class-registry-app] (.getElementById js/document "app")))) diff --git a/examples/class-registry/src/class_registry/server.clj b/examples/class-registry/src/class_registry/server.clj index 94bd04e..5bac374 100644 --- a/examples/class-registry/src/class_registry/server.clj +++ b/examples/class-registry/src/class_registry/server.clj @@ -3,23 +3,20 @@ (:require [compojure.core :refer [routes GET POST]] [compojure.route :as route] + [ring.middleware.anti-forgery :refer [*anti-forgery-token*]] [ring.middleware.defaults :refer [wrap-defaults site-defaults]] [ring.middleware.format :refer [wrap-restful-format]] - [ring.util.anti-forgery :refer [anti-forgery-field]] [ring.util.response :refer [response]] [taoensso.sente :as sente] [taoensso.sente.server-adapters.immutant :refer [sente-web-server-adapter]] [immutant.web :as immutant] [hiccup.page :refer [html5 include-css include-js]] [hiccup.element :refer [javascript-tag]] - [environ.core :refer [env]] [clojure.java.jdbc :as jdbc] [views.sql.core :refer [vexec! with-view-transaction]] [views.sql.view :refer [view]] [views.reagent.sente.server :as vr])) -(def dev? (boolean (env :dev))) - (def db {:classname "org.postgresql.Driver" :subprotocol "postgresql" :subname "//localhost/class_registry" @@ -147,12 +144,12 @@ [:head [:title "Class Registry - views.reagent Example"] [:meta {:name "viewport" :content "width=device-width, initial-scale=1"}] + [:meta {:name "csrf-token" :content *anti-forgery-token*}] (include-css "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" "app.css") (include-js "cljs/app.js")] [:body - (anti-forgery-field) [:div#app [:h1 "This will be replaced by the Class Registry app when the ClojureScript is compiled."]] (javascript-tag "class_registry.client.run();")])) @@ -203,7 +200,7 @@ (-> app-routes (wrap-restful-format :formats [:transit-json]) (wrap-sente "/chsk") - (wrap-defaults (assoc-in site-defaults [:security :anti-forgery] (not dev?))))) + (wrap-defaults site-defaults)))