From 51d24007e73edbdbff7ced99138e8b92c98933fa Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Mon, 28 Jan 2019 22:16:41 +0200 Subject: [PATCH 1/2] Add frontend auth example --- examples/frontend-auth/README.md | 13 ++ examples/frontend-auth/checkouts/reitit-core | 1 + .../frontend-auth/checkouts/reitit-frontend | 1 + .../frontend-auth/checkouts/reitit-schema | 1 + examples/frontend-auth/project.clj | 54 +++++++ .../frontend-auth/resources/public/index.html | 10 ++ examples/frontend-auth/src/backend/server.clj | 11 ++ examples/frontend-auth/src/frontend/core.cljs | 149 ++++++++++++++++++ 8 files changed, 240 insertions(+) create mode 100644 examples/frontend-auth/README.md create mode 120000 examples/frontend-auth/checkouts/reitit-core create mode 120000 examples/frontend-auth/checkouts/reitit-frontend create mode 120000 examples/frontend-auth/checkouts/reitit-schema create mode 100644 examples/frontend-auth/project.clj create mode 100644 examples/frontend-auth/resources/public/index.html create mode 100644 examples/frontend-auth/src/backend/server.clj create mode 100644 examples/frontend-auth/src/frontend/core.cljs diff --git a/examples/frontend-auth/README.md b/examples/frontend-auth/README.md new file mode 100644 index 00000000..de588f59 --- /dev/null +++ b/examples/frontend-auth/README.md @@ -0,0 +1,13 @@ +# reitit-frontend + controllers example + +## Usage + +```clj +> lein figwheel +``` + +Go with browser to http://localhost:3449 + +## License + +Copyright © 2018 Metosin Oy diff --git a/examples/frontend-auth/checkouts/reitit-core b/examples/frontend-auth/checkouts/reitit-core new file mode 120000 index 00000000..a59d247e --- /dev/null +++ b/examples/frontend-auth/checkouts/reitit-core @@ -0,0 +1 @@ +../../../modules/reitit-core \ No newline at end of file diff --git a/examples/frontend-auth/checkouts/reitit-frontend b/examples/frontend-auth/checkouts/reitit-frontend new file mode 120000 index 00000000..20cdd448 --- /dev/null +++ b/examples/frontend-auth/checkouts/reitit-frontend @@ -0,0 +1 @@ +../../../modules/reitit-frontend \ No newline at end of file diff --git a/examples/frontend-auth/checkouts/reitit-schema b/examples/frontend-auth/checkouts/reitit-schema new file mode 120000 index 00000000..a68c7f05 --- /dev/null +++ b/examples/frontend-auth/checkouts/reitit-schema @@ -0,0 +1 @@ +../../../modules/reitit-schema \ No newline at end of file diff --git a/examples/frontend-auth/project.clj b/examples/frontend-auth/project.clj new file mode 100644 index 00000000..6a4098de --- /dev/null +++ b/examples/frontend-auth/project.clj @@ -0,0 +1,54 @@ +(defproject frontend "0.1.0-SNAPSHOT" + :description "FIXME: write description" + :url "http://example.com/FIXME" + :license {:name "Eclipse Public License" + :url "http://www.eclipse.org/legal/epl-v10.html"} + + :dependencies [[org.clojure/clojure "1.10.0"] + [ring-server "0.5.0"] + [reagent "0.8.1"] + [ring "1.7.1"] + [compojure "1.6.1"] + [hiccup "1.0.5"] + [org.clojure/clojurescript "1.10.439"] + [metosin/reitit "0.2.13"] + [metosin/reitit-schema "0.2.13"] + [metosin/reitit-frontend "0.2.13"] + ;; Just for pretty printting the match + [fipp "0.6.14"]] + + :plugins [[lein-cljsbuild "1.1.7"] + [lein-figwheel "0.5.18"]] + + :source-paths [] + :resource-paths ["resources" "target/cljsbuild"] + + :profiles {:dev {:dependencies [[binaryage/devtools "0.9.10"]]}} + + :cljsbuild + {:builds + [{:id "app" + :figwheel true + :source-paths ["src"] + :watch-paths ["src" "checkouts/reitit-frontend/src"] + :compiler {:main "frontend.core" + :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 + :preloads [devtools.preload]}} + {:id "min" + :source-paths ["src"] + :compiler {:output-to "target/cljsbuild/public/js/app.js" + :output-dir "target/cljsbuild/public/js" + :source-map "target/cljsbuild/public/js/app.js.map" + :optimizations :advanced + :pretty-print false}}]} + + :figwheel {:http-server-root "public" + :server-port 3449 + :nrepl-port 7002 + ;; Server index.html for all routes for HTML5 routing + :ring-handler backend.server/handler}) diff --git a/examples/frontend-auth/resources/public/index.html b/examples/frontend-auth/resources/public/index.html new file mode 100644 index 00000000..ce1dd45b --- /dev/null +++ b/examples/frontend-auth/resources/public/index.html @@ -0,0 +1,10 @@ + + + + Reitit frontend example + + +
+ + + diff --git a/examples/frontend-auth/src/backend/server.clj b/examples/frontend-auth/src/backend/server.clj new file mode 100644 index 00000000..88171cfa --- /dev/null +++ b/examples/frontend-auth/src/backend/server.clj @@ -0,0 +1,11 @@ +(ns backend.server + (:require [clojure.java.io :as io] + [ring.util.response :as resp] + [ring.middleware.content-type :as content-type])) + +(def handler + (-> (fn [request] + (or (resp/resource-response (:uri request) {:root "public"}) + (-> (resp/resource-response "index.html" {:root "public"}) + (resp/content-type "text/html")))) + content-type/wrap-content-type)) diff --git a/examples/frontend-auth/src/frontend/core.cljs b/examples/frontend-auth/src/frontend/core.cljs new file mode 100644 index 00000000..a080532b --- /dev/null +++ b/examples/frontend-auth/src/frontend/core.cljs @@ -0,0 +1,149 @@ +(ns frontend.core + (:require [reagent.core :as r] + [reitit.frontend :as rf] + [reitit.frontend.easy :as rfe] + [reitit.frontend.controllers :as rfc] + [reitit.coercion :as rc] + [reitit.coercion.schema :as rsc] + [schema.core :as s] + [fipp.edn :as fedn])) + +(defonce state (r/atom {:user nil})) + +(defn home-page [] + [:div + [:h2 "Welcome to frontend"] + [:p "Look at console log for controller calls."]]) + +(defn item-page [match] + (let [{:keys [path query]} (:parameters match) + {:keys [id]} path] + [:div + [:ul + [:li [:a {:href (rfe/href ::item {:id 1})} "Item 1"]] + [:li [:a {:href (rfe/href ::item {:id 2} {:foo "bar"})} "Item 2"]]] + (if id + [:h2 "Selected item " id]) + (if (:foo query) + [:p "Optional foo query param: " (:foo query)])])) + +(defn login-done [user] + (swap! state assoc :user user)) + +(defn login [user] + ;; In real app one would send API call here to create session or retrieve token or something + ;; and the callback would update app-state + (js/setTimeout #(login-done user) 250)) + +(defn logout [] + (swap! state assoc :user nil)) + +(defn login-view [] + (let [form (r/atom {})] + (fn [] + [:div + [:form + {:on-submit (fn [e] + (.preventDefault e) + (if (and (:username @form) + (:password @form)) + (login @form)))} + + [:label "Username"] + [:input + {:default-value "" + :on-change #(swap! form assoc :username %)}] + + [:label "Password"] + [:input + {:default-value "" + :on-change #(swap! form assoc :password %)}] + + [:button + {:type "submit"} + "Login"]]]))) + +(defn about-page [] + [:div + [:p "This view is public."]]) + +(defn main-view [] + (let [{:keys [user match]} @state + route-data (:data match)] + [:div + [:ul + [:li [:a {:href (rfe/href ::frontpage)} "Frontpage"]] + [:li [:a {:href (rfe/href ::about)} "About (public)"]] + [:li [:a {:href (rfe/href ::item-list)} "Item list"]] + (if user + [:li [:a {:on-click (fn [e] + (.preventDefault e) + (logout)) + :href "#"} + "Logout"]])] + ;; If user is authenticated + ;; or if this route has been defined as public, else login view + (if (or user + (:public? route-data)) + (if match + (let [view (:view route-data)] + [view match])) + [login-view]) + [:pre (with-out-str (fedn/pprint @state))]])) + +(defn log-fn [& params] + (fn [_] + (apply js/console.log params))) + +(def routes + (rf/router + ["/" + ["" + {:name ::frontpage + :view home-page + :controllers [{:start (log-fn "start" "frontpage controller") + :stop (log-fn "stop" "frontpage controller")}]}] + + ["about" + {:name ::about + :view about-page + :public? true}] + + ["items" + ;; Shared data for sub-routes + {:view item-page + :controllers [{:start (log-fn "start" "items controller") + :stop (log-fn "stop" "items controller")}]} + + ["" + {:name ::item-list + :controllers [{:start (log-fn "start" "item-list controller") + :stop (log-fn "stop" "item-list controller")}]}] + ["/:id" + {:name ::item + :parameters {:path {:id s/Int} + :query {(s/optional-key :foo) s/Keyword}} + :controllers [{:params (fn [match] + (:path (:parameters match))) + :start (fn [params] + (js/console.log "start" "item controller" (:id params))) + :stop (fn [params] + (js/console.log "stop" "item controller" (:id params)))}]}]]] + {:data {:controllers [{:start (log-fn "start" "root-controller") + :stop (log-fn "stop" "root controller")}] + :coercion rsc/coercion + :public? false}})) + +(defn init! [] + (rfe/start! + routes + (fn [new-match] + (swap! state (fn [state] + (if new-match + (if (:user state) + (assoc state :match (assoc new-match :controllers (rfc/apply-controllers (:controllers (:match state)) new-match))) + (assoc state :match new-match)))))) + {:use-fragment true}) + (r/render [main-view] (.getElementById js/document "app"))) + +(init!) From 6df97df4241595d37dab0dcd429fff91a947ba6c Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Mon, 28 Jan 2019 22:19:53 +0200 Subject: [PATCH 2/2] Add comment --- examples/frontend-auth/src/frontend/core.cljs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/frontend-auth/src/frontend/core.cljs b/examples/frontend-auth/src/frontend/core.cljs index a080532b..17bd99e6 100644 --- a/examples/frontend-auth/src/frontend/core.cljs +++ b/examples/frontend-auth/src/frontend/core.cljs @@ -140,6 +140,10 @@ (fn [new-match] (swap! state (fn [state] (if new-match + ;; Only run the controllers, which are likely to call authentcated APIs, + ;; if user has been authenticated. + ;; Alternative solution could be to always run controllers, + ;; check authentication status in each controller, or check authentication status in API calls. (if (:user state) (assoc state :match (assoc new-match :controllers (rfc/apply-controllers (:controllers (:match state)) new-match))) (assoc state :match new-match))))))