├── resources └── public │ ├── favicon.ico │ ├── img │ ├── banner.png │ ├── screen.png │ └── marque-sm.png │ ├── index.html │ ├── css │ └── style.css │ └── js │ └── vendor │ └── amazon-cognito-identity.min.js ├── .gitignore ├── src └── bib │ └── customer │ └── client │ ├── views │ ├── utils.cljs │ ├── navbar.cljs │ ├── home.cljs │ └── auth │ │ ├── verify.cljs │ │ ├── signin.cljs │ │ ├── register.cljs │ │ └── forgot.cljs │ ├── state │ ├── usersync.cljs │ └── auth.cljs │ ├── store.cljs │ ├── core.cljs │ └── events │ └── auth.cljs ├── externs └── amazon-cognito-identity.externs.js ├── dev └── user.clj ├── README.md ├── scss └── style.scss └── project.clj /resources/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kironroy101/biblio/HEAD/resources/public/favicon.ico -------------------------------------------------------------------------------- /resources/public/img/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kironroy101/biblio/HEAD/resources/public/img/banner.png -------------------------------------------------------------------------------- /resources/public/img/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kironroy101/biblio/HEAD/resources/public/img/screen.png -------------------------------------------------------------------------------- /resources/public/img/marque-sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kironroy101/biblio/HEAD/resources/public/img/marque-sm.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /resources/public/js/compiled/** 2 | figwheel_server.log 3 | pom.xml 4 | *jar 5 | /lib/ 6 | /classes/ 7 | /out/ 8 | /target/ 9 | .lein-deps-sum 10 | .lein-repl-history 11 | .lein-plugins/ 12 | .repl 13 | .nrepl-port 14 | .DS_Store -------------------------------------------------------------------------------- /src/bib/customer/client/views/utils.cljs: -------------------------------------------------------------------------------- 1 | (ns bib.customer.client.views.utils 2 | (:require [reagent.core :as reagent :refer [atom]])) 3 | 4 | (defn swap-on-value-changed! [cursor] 5 | #(swap! cursor (fn [c] (-> % .-target .-value)))) 6 | 7 | (defn on-enter-fn [cb] 8 | (fn [e] (when (= 13 (.-charCode e)) (cb)))) 9 | -------------------------------------------------------------------------------- /src/bib/customer/client/state/usersync.cljs: -------------------------------------------------------------------------------- 1 | (ns bib.customer.client.state.usersync 2 | (:require-macros [cljs.core.async.macros :refer [go go-loop]] 3 | [mount.core :refer [defstate]]) 4 | (:require [mount.core :as mount] 5 | [cljs.core.async :as async :refer [timeout chan ! alts! close!]] 6 | [bib.customer.client.events.auth :as auth-events])) 7 | 8 | (def user-sync-pid (atom nil)) 9 | 10 | (defn start-cognito-user-sync [] 11 | (let [close-ch (chan) 12 | id (random-uuid)] 13 | (reset! user-sync-pid id) 14 | (go-loop [] 15 | (when (= @user-sync-pid id) 16 | (auth-events/update-user-info) 17 | ( 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/bib/customer/client/views/navbar.cljs: -------------------------------------------------------------------------------- 1 | (ns bib.customer.client.views.navbar 2 | (:require [bib.customer.client.store :refer [app-state nav!]] 3 | [bib.customer.client.views.utils :as view-utils] 4 | [bib.customer.client.events.auth :as auth-events])) 5 | 6 | (defn- not-signed-in-auth-links [] 7 | [:ul.navbar-right 8 | #_[:li [:a {:href "#/register"} "Register"]] 9 | #_[:li [:a {:href "#/verify"} "Verify Registration"]] 10 | #_[:li [:a {:href "#/forgot-password"} "Forgot Password"]] 11 | #_[:li [:a {:href "#/reset-forgot-password"} "Reset Password"]] 12 | [:li [:a {:href "#/signin"} 13 | [:i.fa.fa-sign-in] 14 | [:span "Sign In" ]]]]) 15 | 16 | (defn- signed-in-auth-links [] 17 | [:ul.navbar-right 18 | [:li [:a {:on-click #((auth-events/signout-user) 19 | (nav! :signin))} 20 | [:i.fa.fa-sign-out] 21 | [:span "Sign Out" ]]]]) 22 | 23 | (defn view [] 24 | [:nav.navbar 25 | [:div.container-fluid 26 | (let [signed-in (not-empty (:cognito-user @app-state))] 27 | [:div 28 | [:div.navbar-left 29 | [:a.brand {:href "/"} 30 | [:img {:src "img/marque-sm.png"}]]] 31 | (if-not signed-in 32 | [not-signed-in-auth-links] 33 | [signed-in-auth-links]) 34 | (when-let [em (get-in @app-state [:cognito-user :email])] 35 | [:p.navbar-text.navbar-right "Hello, " em])])]]) 36 | -------------------------------------------------------------------------------- /src/bib/customer/client/views/home.cljs: -------------------------------------------------------------------------------- 1 | (ns bib.customer.client.views.home 2 | (:require-macros [cljs.core.async.macros :refer [go]]) 3 | (:require 4 | [bib.customer.client.store :refer [app-state nav!]] 5 | [bib.customer.client.views.utils :as view-utils] 6 | [bib.customer.client.events.auth :as auth-events] 7 | [cljs.core.async :refer [timeout cognito` object to reflect the PoolID, ClientID, and Region for the Cognito UserPool and Client associated with the app. 29 | 30 | ### Launch the Project 31 | 32 | Run the following commands from the root of the project: 33 | 34 | ``` 35 | lein figwheel 36 | ``` 37 | 38 | The project will build and a browser window will launch showing the sign-in page. 39 | 40 | ``` 41 | lein auto sassc once 42 | ``` 43 | 44 | --- 45 | 46 | ## Contacting Us / Contributions 47 | 48 | Please use the [Github Issues](https://github.com/atowndata/trawler/issues) page for questions, ideas and bug reports. Pull requests are welcome. 49 | 50 | Trawler was built by the consulting team at [ATown Data](https://www.atowndata.com). Please [contact us](https://atowndata.com/contact/) if you have a project you'd like to talk to us about! 51 | 52 | 53 | ## License 54 | 55 | Distributed under the [Eclipse Public License](https://www.eclipse.org/legal/epl-v10.html) (the same license as Clojure). 56 | Copyright © 2017 [ATown Data](https://www.atowndata.com) 57 | -------------------------------------------------------------------------------- /src/bib/customer/client/core.cljs: -------------------------------------------------------------------------------- 1 | (ns bib.customer.client.core 2 | (:require-macros [cljs.core.async.macros :refer [go go-loop]] 3 | [mount.core :refer [defstate]]) 4 | (:require [reagent.core :as reagent :refer [atom]] 5 | [mount.core :as mount] 6 | [cljs.core.async :as async :refer [timeout chan ! alts! close!]] 7 | [secretary.core :as secretary :refer-macros [defroute]] 8 | [goog.events :as events] 9 | [goog.history.EventType :as EventType] 10 | [goog.history.Html5History :as Html5History] 11 | [bib.customer.client.store :refer [app-state nav! register-routes]] 12 | [bib.customer.client.core.views.auth.register :as register] 13 | [bib.customer.client.core.views.auth.signin :as signin] 14 | [bib.customer.client.core.views.auth.verify :as verify] 15 | [bib.customer.client.views.home :as home] 16 | [bib.customer.client.views.navbar :as navbar] 17 | [bib.customer.client.views.auth.forgot :as forgot] 18 | [bib.customer.client.events.auth :as auth-events] 19 | [bib.customer.client.state.auth :as auth-state] 20 | [bib.customer.client.views.utils :as view-utils] 21 | [bib.customer.client.state.usersync :refer [user-sync]]) 22 | 23 | 24 | (:import goog.History 25 | [goog.history Html5History] 26 | )) 27 | 28 | (enable-console-print!) 29 | 30 | (defmulti render-view (fn [v & args] v)) 31 | 32 | (defn wrap-content [& c] 33 | [:div.container 34 | [:div.row 35 | [:div.col-md-12 36 | c]]]) 37 | 38 | (defmethod render-view :default 39 | [_ s] 40 | (home/view)) 41 | 42 | (defmethod render-view :register 43 | [_ s] [register/view]) 44 | 45 | (defmethod render-view :verify 46 | [_ s] [verify/view]) 47 | 48 | (defmethod render-view :signin 49 | [_ s] [signin/view]) 50 | 51 | (defmethod render-view :forgot-password-trigger 52 | [_ s] [forgot/trigger-recovery-view]) 53 | 54 | (defmethod render-view :forgot-password-complete 55 | [_ s] [forgot/input-recovery-view]) 56 | 57 | (defn page [] 58 | (fn [] [:div 59 | [navbar/view] 60 | [:div.container 61 | (render-view (get @app-state :page) app-state)]])) 62 | 63 | 64 | (defn mount-components [] 65 | (mount/start #'user-sync) 66 | (reagent/render-component 67 | [page] (. js/document (getElementById "app")))) 68 | 69 | (mount-components) 70 | 71 | 72 | (defn on-js-reload [] 73 | (mount/stop #'user-sync) 74 | (mount/start #'user-sync) 75 | ;; optionally touch your app-state to force rerendering depending on 76 | ;; your application 77 | ;; (swap! app-state update-in [:__figwheel_counter] inc) 78 | ) 79 | -------------------------------------------------------------------------------- /src/bib/customer/client/views/auth/verify.cljs: -------------------------------------------------------------------------------- 1 | (ns bib.customer.client.core.views.auth.verify 2 | (:require 3 | [reagent.core :as reagent :refer [atom]] 4 | [bouncer.core :as b] 5 | [bouncer.validators :as v] 6 | [bib.customer.client.views.utils :as view-utils] 7 | [bib.customer.client.events.auth :as auth-events] 8 | [bib.customer.client.state.auth :as auth-state])) 9 | 10 | (def subscribe (partial auth-state/subscribe-form :verify)) 11 | 12 | (defn validate-view [form-vals] 13 | (b/validate form-vals 14 | :email [v/required v/email] 15 | :code v/required)) 16 | 17 | (defn view [state] 18 | (let [email-cursor (subscribe :email) 19 | code-cursor (subscribe :code) 20 | flash-cursor (subscribe :flash) 21 | touched (atom {:email false :code false}) 22 | has-verr (fn [vs field] 23 | (and 24 | (contains? vs field) 25 | (field @touched))) 26 | fields (subscribe)] 27 | (fn [] 28 | (let [verrs (first (validate-view @fields))] 29 | [:div.row>div.col-sm-6.col-sm-offset-3.col-md-6.col-md-offset-3.col-lg-4.col-lg-offset-4 30 | [:div.user-verify.auth-panel 31 | [:div.panel.panel-default 32 | [:div.panel-heading>h1 "Confirm Your Account"] 33 | [:div.panel-body 34 | (into [:div] 35 | (map 36 | (fn [v] [:div.alert 37 | {:class (name (:type v))} 38 | (:message v)]) 39 | @flash-cursor)) 40 | [:div.form 41 | [:div.form-group 42 | {:class (when (has-verr verrs :email) "has-error")} 43 | [:label {:for "email-input"} "Email Address"] 44 | [:input#email-input.form-control 45 | {:type "email" :placeholder "Email" 46 | :value @email-cursor 47 | :on-key-press 48 | (view-utils/on-enter-fn auth-events/verify-user) 49 | :on-change 50 | (comp 51 | (fn [_] (swap! touched assoc :email true)) 52 | (view-utils/swap-on-value-changed! email-cursor))}] 53 | (when (has-verr verrs :email) 54 | [:small.text-help (:email verrs)])] 55 | [:div.form-group 56 | {:class (when (has-verr verrs :code) "has-error")} 57 | [:label {:for "code-input"} "Code"] 58 | [:input#code-input.form-control 59 | {:type "code" :placeholder "Code" 60 | :value @code-cursor 61 | :on-key-press 62 | (view-utils/on-enter-fn auth-events/verify-user) 63 | :on-change 64 | (comp 65 | (fn [_] (swap! touched assoc :code true)) 66 | (view-utils/swap-on-value-changed! code-cursor))}] 67 | (when (has-verr verrs :code) 68 | [:small.text-help (:code verrs)])] 69 | [:a.btn.btn-default.btn-block 70 | {:on-click #(auth-events/verify-user) 71 | :class (when-not (empty? verrs) 72 | "disabled")} 73 | "Verify"]] ]] 74 | [:a.btn.btn-secondary.btn-block {:on-click 75 | (fn [_] (when (some? @email-cursor) 76 | (auth-events/resend-verification-code)))} 77 | "Resend Confirmation Code?"]] ] )))) 78 | -------------------------------------------------------------------------------- /src/bib/customer/client/views/auth/signin.cljs: -------------------------------------------------------------------------------- 1 | (ns bib.customer.client.core.views.auth.signin 2 | (:require 3 | [reagent.core :as reagent :refer [atom]] 4 | [bouncer.core :as b] 5 | [bouncer.validators :as v] 6 | [bib.customer.client.store :refer [app-state nav!]] 7 | [bib.customer.client.views.utils :as view-utils] 8 | [bib.customer.client.events.auth :as auth-events] 9 | [bib.customer.client.state.auth :as auth-state])) 10 | 11 | (def subscribe (partial auth-state/subscribe-form :signin)) 12 | 13 | (defn validate-view [form-vals] 14 | (b/validate form-vals 15 | :email [v/required v/email] 16 | :password v/required)) 17 | 18 | (defn view [state] 19 | (let [email-cursor (subscribe :email) 20 | password-cursor (subscribe :password) 21 | flash-cursor (subscribe :flash) 22 | touched (atom {:email false :password false :confirm-password false}) 23 | has-verr (fn [vs field] 24 | (and 25 | (contains? vs field) 26 | (field @touched))) 27 | fields (subscribe)] 28 | (fn [] 29 | (let [verrs (first (validate-view @fields)) 30 | signed-in (not-empty (:cognito-user @app-state)) 31 | t (get-in @app-state [:cognito-user :jwt-token])] 32 | (if signed-in 33 | (do (nav! :home) 34 | [:div]) 35 | (do [:div.row 36 | [:div.col-sm-6.col-sm-offset-3.col-md-6.col-md-offset-3.col-lg-4.col-lg-offset-4 37 | [:div.user-signin.auth-panel 38 | [:div.panel.panel-default 39 | [:div.panel-heading>h1 "Welcome back"] 40 | [:div.panel-body 41 | (into [:div] 42 | (map 43 | (fn [v] [:div.alert 44 | {:class (name (:type v))} 45 | (:message v)]) 46 | @flash-cursor)) 47 | [:div.form 48 | [:div.form-group 49 | {:class (when (has-verr verrs :email) "has-error")} 50 | [:label {:for "email-input"} "Email Address"] 51 | [:input#email-input.form-control 52 | {:type "email" :placeholder "Email" 53 | :value @email-cursor 54 | :on-change 55 | (comp 56 | (fn [_] (swap! touched assoc :email true)) 57 | (view-utils/swap-on-value-changed! email-cursor)) 58 | :on-key-press 59 | (view-utils/on-enter-fn auth-events/signin-user) 60 | }] 61 | (when (has-verr verrs :email) 62 | [:small.help-block (:email verrs)])] 63 | [:div.form-group.password-form-group 64 | {:class (when (has-verr verrs :password) "has-error")} 65 | [:label {:for "password-input"} "Password"] 66 | [:input#password-input.form-control 67 | {:type "password" :placeholder "Password" 68 | :value @password-cursor 69 | :on-change 70 | (comp 71 | (fn [_] (swap! touched assoc :password true)) 72 | (view-utils/swap-on-value-changed! password-cursor) ) 73 | :on-key-press 74 | (view-utils/on-enter-fn auth-events/signin-user) 75 | }] 76 | (when (has-verr verrs :password) 77 | [:small.help-block (:password verrs)])] 78 | [:div.form-group.clearfix 79 | [:small [:a.pull-right {:href "#/forgot-password"} "Forgot your password?"] ]] 80 | [:a.btn.btn-default.btn-block 81 | {:on-click #(auth-events/signin-user) 82 | :class (when-not (empty? verrs) 83 | "disabled") } 84 | "Sign in to your account"]]]] 85 | [:a.btn.btn-secondary.btn-block {:href "#/register"} 86 | [:span "Don't have an account? " ] 87 | [:u "Sign up" ]] ] ] ] ) ))))) 88 | -------------------------------------------------------------------------------- /src/bib/customer/client/views/auth/register.cljs: -------------------------------------------------------------------------------- 1 | (ns bib.customer.client.core.views.auth.register 2 | (:require 3 | [reagent.core :as reagent :refer [atom]] 4 | [bouncer.core :as b] 5 | [bouncer.validators :as v] 6 | [bib.customer.client.views.utils :as view-utils] 7 | [bib.customer.client.events.auth :as auth-events] 8 | [bib.customer.client.state.auth :as auth-state])) 9 | 10 | (def subscribe (partial auth-state/subscribe-form :register)) 11 | 12 | (defn validate-view [form-vals] 13 | (b/validate form-vals 14 | :email [v/required v/email] 15 | :password v/required 16 | :confirm-password [v/required [(fn [cp] 17 | (= cp (:password form-vals))) 18 | :message 19 | "Password and Confirm Password must match"]])) 20 | 21 | (defn view [] 22 | (let [email-cursor (subscribe :email) 23 | password-cursor (subscribe :password) 24 | confirm-password-cursor (subscribe :confirm-password) 25 | flash-cursor (subscribe :flash) 26 | touched (atom {:email false :password false :confirm-password false}) 27 | has-verr (fn [vs field] 28 | (and 29 | (contains? vs field) 30 | (field @touched))) 31 | fields (subscribe)] 32 | (fn [] 33 | (let [verrs (first (validate-view @fields))] 34 | [:div.row>div.col-sm-6.col-sm-offset-3.col-md-6.col-md-offset-3.col-lg-4.col-lg-offset-4 35 | [:div.user-register.auth-panel 36 | [:div.panel.panel-default 37 | [:div.panel-heading>h1 "Create Your Account"] 38 | 39 | [:div.panel-body 40 | (into [:div] 41 | (map 42 | (fn [v] [:div.alert.alert-danger 43 | {:class (name (:type v))} 44 | (:message v)]) 45 | @flash-cursor)) 46 | [:div.form 47 | [:div.form-group 48 | {:class (when (has-verr verrs :email) "has-error")} 49 | [:label {:for "email-input"} "Email Address"] 50 | [:input#email-input.form-control 51 | {:type "email" :placeholder "Email" 52 | :value @email-cursor 53 | :on-key-press 54 | (view-utils/on-enter-fn auth-events/register-new-user) 55 | :on-change 56 | (comp 57 | (fn [_] (swap! touched assoc :email true)) 58 | (view-utils/swap-on-value-changed! email-cursor))}] 59 | (when (has-verr verrs :email) 60 | [:small.text-help (:email verrs)])] 61 | [:div.form-group 62 | {:class (when (has-verr verrs :password) "has-error")} 63 | [:label {:for "password-input"} "Password"] 64 | [:input#password-input.form-control 65 | {:type "password" :placeholder "Password" 66 | :value @password-cursor 67 | :on-key-press 68 | (view-utils/on-enter-fn auth-events/register-new-user) 69 | :on-change 70 | (comp 71 | (fn [_] (swap! touched assoc :password true)) 72 | (view-utils/swap-on-value-changed! password-cursor) )}] 73 | (when (has-verr verrs :password) 74 | [:small.text-help (:password verrs)])] 75 | [:div.form-group 76 | {:class (when (has-verr verrs :confirm-password) "has-error")} 77 | [:label {:for "confirm-password-input"} "Confirm Password"] 78 | [:input#password-input.form-control 79 | {:type "password" :placeholder "Confirm Password" 80 | :value @confirm-password-cursor 81 | :on-key-press 82 | (view-utils/on-enter-fn auth-events/register-new-user) 83 | :on-change 84 | (comp 85 | (fn [_] (swap! touched assoc :confirm-password true)) 86 | (view-utils/swap-on-value-changed! confirm-password-cursor))}] 87 | (when (has-verr verrs :confirm-password) 88 | [:small.text-help (:confirm-password verrs)])] 89 | [:a.btn.btn-default.btn-block 90 | {:on-click #(auth-events/register-new-user) 91 | :class (when-not (empty? verrs) 92 | "disabled")} 93 | "Register"]] ] 94 | ] 95 | [:a.btn.btn-secondary.btn-block {:href "#/signin"} 96 | [:span "Already have an account? " ] 97 | [:u "Sign in" ]] 98 | 99 | ] ] )))) 100 | -------------------------------------------------------------------------------- /resources/public/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #f6f9fc; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; } 4 | 5 | input[type="email"]::-webkit-input-placeholder, 6 | input[type="text"]::-webkit-input-placeholder, 7 | input[type="password"]::-webkit-input-placeholder { 8 | /* Chrome/Opera/Safari */ 9 | color: rgba(136, 152, 170, 0.3); } 10 | 11 | input[type="email"]::-moz-placeholder, 12 | input[type="text"]::-moz-placeholder, 13 | input[type="password"]::-moz-placeholder { 14 | /* Firefox 19+ */ 15 | color: rgba(136, 152, 170, 0.3); } 16 | 17 | input[type="email"]:-ms-input-placeholder, 18 | input[type="text"]:-ms-input-placeholder, 19 | input[type="password"]:-ms-input-placeholder { 20 | /* IE 10+ */ 21 | color: rgba(136, 152, 170, 0.3); } 22 | 23 | input[type="email"]:-moz-placeholder, 24 | input[type="password"]:-moz-placeholder { 25 | /* Firefox 18- */ 26 | color: rgba(136, 152, 170, 0.3); } 27 | 28 | a { 29 | color: #6772e5; 30 | transition: all 0.15s ease; } 31 | a:hover { 32 | color: #32325d; 33 | text-decoration: none; } 34 | 35 | .btn-default { 36 | transition: all 0.15s ease; } 37 | .btn-default, .btn-default:hover { 38 | background: #6772e5; 39 | color: white; 40 | border: 0; } 41 | .btn-default:hover { 42 | background: #8898aa; } 43 | 44 | label { 45 | font-weight: 400; 46 | font-size: 12px; 47 | text-transform: uppercase; 48 | color: #8898aa; } 49 | 50 | .btn { 51 | padding-top: 10px; 52 | padding-bottom: 10px; } 53 | .form .btn { 54 | margin-top: 20px; } 55 | 56 | .panel-default { 57 | margin: 40px 0; 58 | border-radius: 4px; 59 | box-shadow: 0 15px 35px rgba(50, 50, 93, 0.1), 0 5px 15px rgba(0, 0, 0, 0.07); 60 | transition-property: color, background-color, box-shadow, transform; 61 | transition-duration: .15s; 62 | border: 0; } 63 | .panel-default > .panel-heading { 64 | background: none; 65 | border-color: rgba(207, 215, 223, 0.35); 66 | text-align: center; } 67 | .panel-default > .panel-heading h1 { 68 | margin: 13px 0 10px; 69 | font-size: 17px; 70 | line-height: 26px; 71 | font-weight: 600; 72 | text-transform: uppercase; 73 | letter-spacing: .025em; 74 | color: #6772e5; } 75 | 76 | .panel-body { 77 | padding: 35px 40px 40px; } 78 | 79 | .navbar { 80 | background: #232a40; 81 | border-radius: 0; 82 | border: 0; } 83 | .navbar, 84 | .navbar .navbar-text { 85 | color: white; } 86 | .navbar a { 87 | cursor: pointer; 88 | color: #d0deee; } 89 | .navbar a:hover { 90 | color: #f6f9fc; } 91 | 92 | .navbar-right { 93 | float: right !important; } 94 | 95 | .navbar-left { 96 | float: left !important; } 97 | 98 | ul.navbar-right { 99 | margin: 0 0 0 10px; 100 | padding: 0; } 101 | ul.navbar-right li { 102 | list-style: none; } 103 | ul.navbar-right li a { 104 | display: inline-block; 105 | padding: 15px 20px; } 106 | ul.navbar-right li a i { 107 | display: inline-block; 108 | margin-right: 7px; } 109 | 110 | .brand { 111 | position: relative; 112 | display: block; 113 | width: 55px; 114 | height: 50px; 115 | margin-left: -15px; 116 | background: #6772e5; } 117 | .brand img { 118 | position: absolute; 119 | top: 50%; 120 | left: 50%; 121 | margin-top: -18px; 122 | margin-left: -16px; 123 | width: 31px; 124 | height: auto; } 125 | 126 | .password-form-group { 127 | margin-bottom: 5px; } 128 | 129 | .loader-wrapper { 130 | width: 100%; 131 | height: 100vh; 132 | padding: 50px; } 133 | 134 | .mask-loading { 135 | position: relative; 136 | top: 45%; 137 | transform: translateY(-50%); } 138 | 139 | .spinner { 140 | width: 50px; 141 | height: 50px; 142 | position: relative; 143 | margin: 0 auto; } 144 | 145 | .double-bounce1, 146 | .double-bounce2 { 147 | width: 100%; 148 | height: 100%; 149 | border-radius: 50%; 150 | background-color: #6772e5; 151 | opacity: 0.6; 152 | position: absolute; 153 | top: 0; 154 | left: 0; 155 | -webkit-animation: sk-bounce 2.0s infinite ease-in-out; 156 | animation: sk-bounce 2.0s infinite ease-in-out; } 157 | 158 | .double-bounce2 { 159 | -webkit-animation-delay: -1.0s; 160 | animation-delay: -1.0s; } 161 | 162 | @-webkit-keyframes sk-bounce { 163 | 0%, 164 | 100% { 165 | -webkit-transform: scale(0); } 166 | 50% { 167 | -webkit-transform: scale(1); } } 168 | 169 | @keyframes sk-bounce { 170 | 0%, 171 | 100% { 172 | transform: scale(0); 173 | -webkit-transform: scale(0); } 174 | 50% { 175 | transform: scale(1); 176 | -webkit-transform: scale(1); } } 177 | 178 | .auth-token { 179 | background: white; 180 | white-space: pre-wrap; 181 | /* css-3 */ 182 | white-space: -moz-pre-wrap; 183 | /* Mozilla, since 1999 */ 184 | white-space: -pre-wrap; 185 | /* Opera 4-6 */ 186 | white-space: -o-pre-wrap; 187 | /* Opera 7 */ 188 | word-wrap: break-word; 189 | /* Internet Explorer 5.5+ */ } 190 | -------------------------------------------------------------------------------- /scss/style.scss: -------------------------------------------------------------------------------- 1 | // VARIABLES 2 | $light-border-color: rgba(207, 215, 223, 0.35); 3 | $light-text-color: #8898aa; 4 | $placeholder-color: rgba(136, 152, 170, 0.3); 5 | $light-grey-bg: #f6f9fc; 6 | $accent-text-color:#6772e5; 7 | body { 8 | background: $light-grey-bg; 9 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; 10 | } 11 | 12 | input[type="email"]::-webkit-input-placeholder, 13 | input[type="text"]::-webkit-input-placeholder, 14 | input[type="password"]::-webkit-input-placeholder { 15 | /* Chrome/Opera/Safari */ 16 | color: $placeholder-color; 17 | } 18 | 19 | input[type="email"]::-moz-placeholder, 20 | input[type="text"]::-moz-placeholder, 21 | input[type="password"]::-moz-placeholder { 22 | /* Firefox 19+ */ 23 | color: $placeholder-color; 24 | } 25 | 26 | input[type="email"]:-ms-input-placeholder, 27 | input[type="text"]:-ms-input-placeholder, 28 | input[type="password"]:-ms-input-placeholder { 29 | /* IE 10+ */ 30 | color: $placeholder-color; 31 | } 32 | 33 | input[type="email"]:-moz-placeholder, 34 | input[type="password"]:-moz-placeholder { 35 | /* Firefox 18- */ 36 | color: $placeholder-color; 37 | } 38 | 39 | a { 40 | color: #6772e5; 41 | transition: all 0.15s ease; 42 | &:hover { 43 | color: #32325d; 44 | text-decoration: none; 45 | } 46 | } 47 | 48 | .btn-default { 49 | transition: all 0.15s ease; 50 | &, 51 | &:hover { 52 | background: #6772e5; 53 | color: white; 54 | border: 0; 55 | } 56 | &:hover { 57 | background: $light-text-color; 58 | } 59 | } 60 | 61 | label { 62 | font-weight: 400; 63 | font-size: 12px; 64 | text-transform: uppercase; 65 | color: $light-text-color; 66 | } 67 | 68 | .btn { 69 | padding-top: 10px; 70 | padding-bottom: 10px; 71 | .form & { 72 | margin-top: 20px; 73 | } 74 | } 75 | 76 | .panel-default { 77 | margin: 40px 0; 78 | border-radius: 4px; 79 | box-shadow: 0 15px 35px rgba(50, 50, 93, .1), 0 5px 15px rgba(0, 0, 0, .07); 80 | transition-property: color, background-color, box-shadow, transform; 81 | transition-duration: .15s; 82 | border: 0; 83 | &>.panel-heading { 84 | background: none; 85 | border-color: $light-border-color; 86 | text-align: center; 87 | h1 { 88 | margin: 13px 0 10px; 89 | font-size: 17px; 90 | line-height: 26px; 91 | font-weight: 600; 92 | text-transform: uppercase; 93 | letter-spacing: .025em; 94 | color: $accent-text-color; 95 | } 96 | } 97 | } 98 | 99 | .panel-body { 100 | padding: 35px 40px 40px; 101 | } 102 | 103 | .navbar { 104 | background: #232a40; 105 | border-radius: 0; 106 | border: 0; 107 | &, 108 | .navbar-text { 109 | color: white; 110 | } 111 | a { 112 | cursor: pointer; 113 | color: #d0deee; 114 | &:hover { 115 | color: $light-grey-bg; 116 | } 117 | } 118 | } 119 | 120 | .navbar-right { 121 | float: right !important; 122 | } 123 | 124 | .navbar-left { 125 | float: left !important; 126 | } 127 | 128 | ul.navbar-right { 129 | margin: 0 0 0 10px; 130 | padding: 0; 131 | li { 132 | list-style: none; 133 | a { 134 | display: inline-block; 135 | padding: 15px 20px; 136 | i { 137 | display: inline-block; 138 | margin-right: 7px; 139 | } 140 | } 141 | } 142 | } 143 | 144 | .brand { 145 | position: relative; 146 | display: block; 147 | width: 55px; 148 | height: 50px; 149 | margin-left: -15px; 150 | background: $accent-text-color; 151 | img { 152 | position: absolute; 153 | top: 50%; 154 | left: 50%; 155 | margin-top: -18px; 156 | margin-left: -16px; 157 | width: 31px; 158 | height: auto; 159 | } 160 | } 161 | 162 | .password-form-group { 163 | margin-bottom: 5px; 164 | } 165 | 166 | .loader-wrapper { 167 | width: 100%; 168 | height: 100vh; 169 | padding: 50px; 170 | } 171 | 172 | .mask-loading { 173 | position: relative; 174 | top: 45%; 175 | transform: translateY(-50%); 176 | } 177 | 178 | .spinner { 179 | width: 50px; 180 | height: 50px; 181 | position: relative; 182 | margin: 0 auto; 183 | } 184 | 185 | .double-bounce1, 186 | .double-bounce2 { 187 | width: 100%; 188 | height: 100%; 189 | border-radius: 50%; 190 | background-color: $accent-text-color; 191 | opacity: 0.6; 192 | position: absolute; 193 | top: 0; 194 | left: 0; 195 | -webkit-animation: sk-bounce 2.0s infinite ease-in-out; 196 | animation: sk-bounce 2.0s infinite ease-in-out; 197 | } 198 | 199 | .double-bounce2 { 200 | -webkit-animation-delay: -1.0s; 201 | animation-delay: -1.0s; 202 | } 203 | 204 | @-webkit-keyframes sk-bounce { 205 | 0%, 206 | 100% { 207 | -webkit-transform: scale(0.0) 208 | } 209 | 50% { 210 | -webkit-transform: scale(1.0) 211 | } 212 | } 213 | 214 | @keyframes sk-bounce { 215 | 0%, 216 | 100% { 217 | transform: scale(0.0); 218 | -webkit-transform: scale(0.0); 219 | } 220 | 50% { 221 | transform: scale(1.0); 222 | -webkit-transform: scale(1.0); 223 | } 224 | } 225 | 226 | .auth-token { 227 | background: white; 228 | white-space: pre-wrap; 229 | /* css-3 */ 230 | white-space: -moz-pre-wrap; 231 | /* Mozilla, since 1999 */ 232 | white-space: -pre-wrap; 233 | /* Opera 4-6 */ 234 | white-space: -o-pre-wrap; 235 | /* Opera 7 */ 236 | word-wrap: break-word; 237 | /* Internet Explorer 5.5+ */ 238 | } -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject bib.customer.client "0.0.1" 2 | :description "A ClojureScript/Reagent client for AWS Cognito" 3 | :url "http://github.com/atowndata/biblio" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | 7 | :min-lein-version "2.7.1" 8 | 9 | :dependencies [[org.clojure/clojure "1.8.0"] 10 | [org.clojure/clojurescript "1.9.229"] 11 | [org.clojure/core.async "0.3.442" 12 | :exclusions [org.clojure/tools.reader]] 13 | [mount "0.1.11"] 14 | [reagent "0.6.0"] 15 | [reagent-utils "0.2.1"] 16 | [historian "1.1.1"] 17 | [secretary "1.2.3"] 18 | [com.andrewmcveigh/cljs-time "0.4.0"] 19 | [cljsjs/jquery-ui "1.11.4-0"] 20 | [cljsjs/jquery "2.2.4-0"] 21 | [cljs-ajax "0.6.0"] 22 | [bouncer "1.0.1"]] 23 | 24 | :plugins [[lein-figwheel "0.5.10"] 25 | [lein-cljsbuild "1.1.5" :exclusions [[org.clojure/clojure]]] 26 | [lein-sassc "0.10.4"] 27 | [lein-auto "0.1.3"]] 28 | 29 | :sassc 30 | [{:src "scss/style.scss" 31 | :output-to "resources/public/css/style.css" 32 | :style "nested" 33 | :import-path "scss"}] 34 | 35 | :auto 36 | {"sassc" {:file-pattern #"\.(scss|sass)$" :paths ["scss"]}} 37 | 38 | :hooks [leiningen.sassc] 39 | 40 | :source-paths ["src"] 41 | 42 | :cljsbuild {:builds 43 | [{:id "dev" 44 | :source-paths ["src"] 45 | 46 | ;; the presence of a :figwheel configuration here 47 | ;; will cause figwheel to inject the figwheel client 48 | ;; into your build 49 | :figwheel {:on-jsload "bib.customer.client.core/on-js-reload" 50 | ;; :open-urls will pop open your application 51 | ;; in the default browser once Figwheel has 52 | ;; started and complied your application. 53 | ;; Comment this out once it no longer serves you. 54 | :open-urls ["http://localhost:3449/index.html"]} 55 | 56 | :compiler {:main bib.customer.client.core 57 | :asset-path "js/compiled/out" 58 | :output-to "resources/public/js/compiled/bib/customer/client.js" 59 | :output-dir "resources/public/js/compiled/out" 60 | :source-map-timestamp true 61 | ;; To console.log CLJS data-structures make sure you enable devtools in Chrome 62 | ;; https://github.com/binaryage/cljs-devtools 63 | :preloads [devtools.preload]}} 64 | ;; This next build is an compressed minified build for 65 | ;; production. You can build this with: 66 | ;; lein cljsbuild once min 67 | {:id "min" 68 | :source-paths ["src"] 69 | :compiler {:output-to "resources/public/js/compiled/bib/customer/client.js" 70 | :main bib.customer.client.core 71 | :optimizations :advanced 72 | :pretty-print false 73 | :externs ["externs/aws-cognito-sdk.externs.js" 74 | "externs/amazon-cognito-identity.externs.js"] 75 | }}]} 76 | 77 | :figwheel {;; :http-server-root "public" ;; default and assumes "resources" 78 | ;; :server-port 3449 ;; default 79 | ;; :server-ip "127.0.0.1" 80 | 81 | :css-dirs ["resources/public/css"] ;; watch and update CSS 82 | 83 | ;; Start an nREPL server into the running figwheel process 84 | ;; :nrepl-port 7888 85 | 86 | ;; Server Ring Handler (optional) 87 | ;; if you want to embed a ring handler into the figwheel http-kit 88 | ;; server, this is for simple ring servers, if this 89 | 90 | ;; doesn't work for you just run your own server :) (see lein-ring) 91 | 92 | ;; :ring-handler hello_world.server/handler 93 | 94 | ;; To be able to open files in your editor from the heads up display 95 | ;; you will need to put a script on your path. 96 | ;; that script will have to take a file path and a line number 97 | ;; ie. in ~/bin/myfile-opener 98 | ;; #! /bin/sh 99 | ;; emacsclient -n +$2 $1 100 | ;; 101 | ;; :open-file-command "myfile-opener" 102 | 103 | ;; if you are using emacsclient you can just use 104 | ;; :open-file-command "emacsclient" 105 | 106 | ;; if you want to disable the REPL 107 | ;; :repl false 108 | 109 | ;; to configure a different figwheel logfile path 110 | ;; :server-logfile "tmp/logs/figwheel-logfile.log" 111 | 112 | ;; to pipe all the output to the repl 113 | ;; :server-logfile false 114 | } 115 | 116 | 117 | ;; setting up nREPL for Figwheel and ClojureScript dev 118 | ;; Please see: 119 | ;; https://github.com/bhauman/lein-figwheel/wiki/Using-the-Figwheel-REPL-within-NRepl 120 | :profiles {:dev {:dependencies [[binaryage/devtools "0.9.2"] 121 | [figwheel-sidecar "0.5.10"] 122 | [com.cemerick/piggieback "0.2.1"]] 123 | ;; need to add dev source path here to get user.clj loaded 124 | :source-paths ["src" "dev"] 125 | ;; for CIDER 126 | ;; :plugins [[cider/cider-nrepl "0.12.0"]] 127 | :repl-options {:nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]} 128 | ;; need to add the compliled assets to the :clean-targets 129 | :clean-targets ^{:protect false} ["resources/public/js/compiled" 130 | :target-path]}}) 131 | -------------------------------------------------------------------------------- /src/bib/customer/client/views/auth/forgot.cljs: -------------------------------------------------------------------------------- 1 | (ns bib.customer.client.views.auth.forgot 2 | (:require 3 | [reagent.core :as reagent :refer [atom]] 4 | [bouncer.core :as b] 5 | [bouncer.validators :as v] 6 | [bib.customer.client.views.utils :as view-utils] 7 | [bib.customer.client.events.auth :as auth-events] 8 | [bib.customer.client.state.auth :as auth-state])) 9 | 10 | (def subscribe (partial auth-state/subscribe-form :forgot)) 11 | 12 | (defn trigger-recovery-view [state] 13 | (let [email-cursor (subscribe :email) 14 | flash-cursor (subscribe :flash) 15 | fields (subscribe) 16 | touched (atom {:email false}) 17 | has-verr (fn [vs field] 18 | (and 19 | (contains? vs field) 20 | (field @touched)))] 21 | (fn [] 22 | [:div.row 23 | [:div.col-sm-6.col-sm-offset-3.col-md-6.col-md-offset-3.col-lg-4.col-lg-offset-4 24 | [:div.user-forgot.auth-panel 25 | [:div.panel.panel-default 26 | [:div.panel-heading>h1 "Reset Your Password"] 27 | [:div.panel-body 28 | (into [:div] 29 | (map 30 | (fn [v] [:div.alert 31 | {:class (name (:type v))} 32 | (:message v)]) 33 | @flash-cursor)) 34 | (let [verrs (first (b/validate @fields 35 | :email [v/required v/email]) )] 36 | [:div.form 37 | [:div.form-group 38 | {:class (when (has-verr verrs :email) "has-error")} 39 | [:label {:for "email-input"} "Email Address"] 40 | [:input#email-input.form-control 41 | {:type "email" :placeholder "Email" 42 | :value @email-cursor 43 | :on-change 44 | (comp 45 | (fn [_] (swap! touched assoc :email true)) 46 | (view-utils/swap-on-value-changed! email-cursor)) 47 | :on-key-press 48 | (view-utils/on-enter-fn auth-events/trigger-password-recovery)}] 49 | (when (has-verr verrs :email) 50 | [:small.text-help (:email verrs)])] 51 | [:a.btn.btn-default.btn-block 52 | {:on-click #(auth-events/trigger-password-recovery) 53 | :class (when-not (empty? verrs) 54 | "disabled")} 55 | "Send Password Reset Email"] 56 | ])]]]]]))) 57 | 58 | (defn validate-password-reset [form-vals] 59 | (b/validate form-vals 60 | :email [v/required v/email] 61 | :code [v/required] 62 | :new-password v/required 63 | :new-password-confirmation [v/required [(fn [cp] 64 | (= cp (:new-password form-vals))) 65 | :message 66 | "Password and Confirm Password must match"]])) 67 | 68 | (defn input-recovery-view [state] 69 | (let [form-fields (subscribe) 70 | code-cursor (subscribe :code) 71 | email-cursor (subscribe :email) 72 | new-password-cursor (subscribe :new-password) 73 | new-password-confirmation-cursor (subscribe :new-password-confirmation) 74 | flash-cursor (auth-state/subscribe-form :reset-password :flash) 75 | fields (subscribe) 76 | touched (atom {:email false 77 | :code false 78 | :new-password false 79 | :new-password-confirmation false}) 80 | has-verr (fn [vs field] 81 | (and 82 | (contains? vs field) 83 | (field @touched)))] 84 | (fn [] 85 | (let [verrs (first (validate-password-reset @fields))] 86 | [:div.row>div.col-sm-6.col-sm-offset-3.col-md-6.col-md-offset-3.col-lg-4.col-lg-offset-4 87 | [:div.user-forgot.auth-panel 88 | [:div.panel.panel-default 89 | [:div.panel-heading>h1 "Reset Your Password"] 90 | [:div.panel-body 91 | (into [:div] 92 | (map 93 | (fn [v] [:div.alert 94 | {:class (name (:type v))} 95 | (:message v)]) 96 | @flash-cursor)) 97 | [:div.form 98 | [:div.form-group 99 | {:class (when (has-verr verrs :email) "has-error")} 100 | [:label {:for "email-input"} "Email Address"] 101 | [:input#email-input.form-control 102 | {:type "email" :placeholder "Email" 103 | :value @email-cursor 104 | :on-key-press 105 | (view-utils/on-enter-fn auth-events/confirm-password-recovery) 106 | :on-change 107 | (comp 108 | (fn [_] (swap! touched assoc :email true)) 109 | (view-utils/swap-on-value-changed! email-cursor))}] 110 | (when (has-verr verrs :email) 111 | [:small.text-help (:email verrs)])] 112 | [:div.form-group 113 | {:class (when (has-verr verrs :code) "has-error")} 114 | [:label {:for "code-input"} "Verification Code"] 115 | [:input#email-input.form-control 116 | {:type "text" :placeholder "Code" 117 | :on-key-press 118 | (view-utils/on-enter-fn auth-events/confirm-password-recovery) 119 | :value @code-cursor 120 | :on-change 121 | (comp (fn [_] (swap! touched assoc :code true)) 122 | (view-utils/swap-on-value-changed! code-cursor) )}] 123 | (when (has-verr verrs :code) 124 | [:small.text-help (:code verrs)])] 125 | [:div.form-group 126 | {:class (when (has-verr verrs :new-password) "has-error")} 127 | [:label {:for "password-input"} "Password"] 128 | [:input#password-input.form-control 129 | {:type "password" :placeholder "Password" 130 | :value @new-password-cursor 131 | :on-key-press 132 | (view-utils/on-enter-fn auth-events/confirm-password-recovery) 133 | :on-change 134 | (comp (fn [_] (swap! touched assoc :new-password true)) 135 | (view-utils/swap-on-value-changed! new-password-cursor))}] 136 | (when (has-verr verrs :new-password) 137 | [:small.text-help (:new-password verrs)])] 138 | [:div.form-group 139 | {:class (when (has-verr verrs :new-password-confirmation) "has-error")} 140 | [:label {:for "confirm-password-input"} "Confirm Password"] 141 | [:input#password-input.form-control 142 | {:type "password" :placeholder "Confirm Password" 143 | :value @new-password-confirmation-cursor 144 | :on-key-press 145 | (view-utils/on-enter-fn auth-events/confirm-password-recovery) 146 | :on-change 147 | (comp (fn [_] (swap! touched assoc :new-password-confirmation true)) 148 | (view-utils/swap-on-value-changed! new-password-confirmation-cursor))}] 149 | (when (has-verr verrs :new-password-confirmation) 150 | [:small.text-help (:new-password-confirmation verrs)])] 151 | [:a.btn.btn-default.btn-block 152 | {:on-click #(auth-events/confirm-password-recovery) 153 | :class (when-not (empty? verrs) 154 | "disabled")} 155 | "Reset Password"] 156 | ] ]] ] ])))) 157 | -------------------------------------------------------------------------------- /src/bib/customer/client/events/auth.cljs: -------------------------------------------------------------------------------- 1 | (ns bib.customer.client.events.auth 2 | (:require-macros [cljs.core.async.macros :refer [go go-loop]]) 3 | (:require [clojure.string :as string] 4 | [cljs.core.async :as async :refer [timeout chan ! alts! close!]] 5 | [bib.customer.client.store :refer [app-state nav!]] 6 | [bib.customer.client.state.auth :as auth-state])) 7 | 8 | (def CognitoUserPool (-> js/AmazonCognitoIdentity .-CognitoUserPool)) 9 | (def CognitoUserAttribute (-> js/AmazonCognitoIdentity .-CognitoUserAttribute)) 10 | (def CognitoUser (-> js/AmazonCognitoIdentity .-CognitoUser)) 11 | (def AuthenticationDetails (-> js/AmazonCognitoIdentity .-AuthenticationDetails)) 12 | (def AWSCognito js/AWSCognito) 13 | 14 | (defn to-username [email] 15 | (string/replace email "@" "-at-")) 16 | 17 | (defn to-email [username] 18 | (string/replace username "-at-" "@")) 19 | 20 | (defn get-user-pool [] 21 | (new CognitoUserPool 22 | (clj->js 23 | {:UserPoolId (get-in @app-state [:cognito :userPoolId]) 24 | :ClientId (get-in @app-state [:cognito :userPoolClientID])}))) 25 | 26 | (defonce user-pool (get-user-pool)) 27 | 28 | (defn get-authtoken [cb] 29 | (let [user (-> user-pool .getCurrentUser)] 30 | (if (some? user) 31 | (.getSession user 32 | (fn [err session] 33 | (cond (some? err) 34 | (cb err nil) 35 | (not (.isValid session)) 36 | (cb nil nil) 37 | :default 38 | (cb nil (-> session .getIdToken .getJwtToken))))) 39 | (cb nil nil)))) 40 | 41 | (defn- create-cognito-user [email] 42 | (new CognitoUser 43 | (clj->js 44 | {:Username (to-username email) 45 | :Pool user-pool}))) 46 | 47 | (defn update-user-info [] 48 | (let [u (.getCurrentUser user-pool)] 49 | (when (some? u) 50 | (go 51 | (let [field-ch (chan) 52 | err-ch (chan) 53 | email (to-email (.getUsername u))] 54 | (swap! app-state assoc-in [:cognito-user :email] email) 55 | (get-authtoken 56 | (fn [err t] 57 | (go (when (some? t) (>! field-ch t)) 58 | (when (some? err) (>! err-ch err))))) 59 | (let [[token ch] (alts! [field-ch err-ch (timeout 10000)])] 60 | (cond 61 | (= ch field-ch) (swap! app-state assoc-in [:cognito-user :jwt-token] token) 62 | (= ch err-ch) (do (println "Error Retrieving User JWT Token" token) 63 | (auth-state/reset-cognito-user {})) 64 | :default (do (println "Timeout retrieving User JWT Token") 65 | (auth-state/reset-cognito-user {})))) 66 | (.getSession u 67 | (fn [err t] 68 | (go (when (some? t) (>! field-ch (.isValid t))) 69 | (when (some? err) (>! err-ch err))))) 70 | (let [[session ch] (alts! [field-ch err-ch (timeout 10000)])] 71 | (cond 72 | (= ch field-ch) (swap! app-state assoc-in [:cognito-user :valid-session] 73 | session) 74 | (= ch err-ch) (do (println "Error Retrieving User Session" session)) 75 | :default (do (println "Timeout retrieving User Session")))) 76 | (close! field-ch) 77 | (close! err-ch))) ) )) 78 | 79 | (defn configure-aws-cognito [] 80 | (set! (-> AWSCognito .-config .-region) (get-in @app-state [:cognito :region]))) 81 | 82 | 83 | (def flash-register (partial auth-state/conj-form-flash :register)) 84 | (def flash-verify (partial auth-state/conj-form-flash :verify)) 85 | (def flash-signin (partial auth-state/conj-form-flash :signin)) 86 | (def flash-forgot (partial auth-state/conj-form-flash :forgot)) 87 | (def flash-reset-password (partial auth-state/conj-form-flash :reset-password)) 88 | 89 | (defmulti handle-register-error #(keyword (.-name %))) 90 | 91 | (defmethod handle-register-error :UsernameExistsException 92 | [e] 93 | (flash-register 94 | {:type :alert-danger 95 | :message "The username is unavailable. Please choose a different one."})) 96 | 97 | 98 | (defmethod handle-register-error :InvalidParameterException 99 | [e] 100 | (flash-register 101 | {:type :alert-danger 102 | :message (.-message e)})) 103 | 104 | (defmethod handle-register-error :InvalidPasswordException 105 | [e] 106 | (flash-register 107 | {:type :alert-danger 108 | :message (str 109 | "The password specified is invalid. Please make sure you have at" 110 | " least of each of uppercase letter, lowercase letter, and special character.")})) 111 | 112 | (defmethod handle-register-error :default 113 | [e] 114 | (flash-register 115 | {:type :alert-danger 116 | :message "Could no complete registraion. Please try again. If this persists, please contact system administrator"}) 117 | (println "Unhandled register error" e)) 118 | 119 | (defn -handle-register-error [e] 120 | (auth-state/clear-form-flash :register) 121 | (handle-register-error e)) 122 | 123 | (defn register-new-user [] 124 | (let [password (get-in @app-state [:auth-form :register :password]) 125 | email (get-in @app-state [:auth-form :register :email]) 126 | attr-email (new CognitoUserAttribute 127 | (clj->js {:Name "email" :Value email}))] 128 | (auth-state/clear-form-flash :register) 129 | (.signUp user-pool 130 | (to-username email) 131 | password 132 | (to-array [attr-email]) nil 133 | (fn [err result] 134 | (if (some? err) 135 | (-handle-register-error err) 136 | (do (flash-verify {:type :alert-success 137 | :message "Please check your email for a verification code."}) 138 | (nav! :verify)) 139 | ))))) 140 | 141 | (defn verify-user [] 142 | (let [email (get-in @app-state [:auth-form :verify :email]) 143 | code (get-in @app-state [:auth-form :verify :code]) 144 | u (create-cognito-user email)] 145 | (auth-state/clear-form-flash :verify) 146 | (.confirmRegistration u 147 | code true 148 | (fn [err result] 149 | (if (some? err) 150 | (do (flash-verify 151 | {:type :alert-danger 152 | :message (.-message err)})) 153 | (do (println "Verified") 154 | (flash-signin 155 | {:type :alert-success 156 | :message "Successfully Verified. Please sign in."}) 157 | (nav! :signin))))))) 158 | 159 | (defmulti handle-signin-error #(keyword (.-name %))) 160 | 161 | 162 | (defn- handle-invalid-pair [] 163 | (flash-signin {:type :alert-danger 164 | :message "Invalid Username/Password Pair"})) 165 | 166 | ;; handles Incorrect Pairs and attempts exceeded 167 | (defmethod handle-signin-error :NotAuthorizedException 168 | [e] 169 | (println "Not Authorized" (.-message e)) 170 | (cond 171 | (some? (string/index-of (.-message e) "Password attempts exceeded")) 172 | (flash-signin {:type :alert-danger :message "Password attempts exceeded"}) 173 | :default 174 | (handle-invalid-pair)) 175 | ) 176 | 177 | (defmethod handle-signin-error :UserNotFoundException 178 | [e] 179 | (handle-invalid-pair)) 180 | 181 | (defmethod handle-signin-error :UserNotConfirmedException 182 | [e] 183 | (flash-signin {:type :alert-danger 184 | :message "User is not confirmed. Please check your email for a verification code."})) 185 | 186 | (defmethod handle-signin-error :default 187 | [e] 188 | (println "Unhandled Signin Error" e) 189 | (flash-signin {:type :alert-danger 190 | :message "Could not complete login"})) 191 | 192 | (defn- clear-signin-form [] 193 | (auth-state/clear-form-flash :signin) 194 | (auth-state/clear-signin-password)) 195 | 196 | (defn -handle-signin-error [e] 197 | (clear-signin-form) 198 | (handle-signin-error e)) 199 | 200 | 201 | (defn signin-user [] 202 | (let [email (get-in @app-state [:auth-form :signin :email]) 203 | password (get-in @app-state [:auth-form :signin :password]) 204 | user (create-cognito-user email) 205 | auth-details (new AuthenticationDetails 206 | (clj->js 207 | {:Username (to-username email) 208 | :Password password}))] 209 | (.authenticateUser user 210 | auth-details 211 | (clj->js 212 | {:onSuccess (fn [] 213 | (clear-signin-form) 214 | (update-user-info) 215 | (nav! :home)) 216 | :onFailure -handle-signin-error})))) 217 | 218 | (defn signout-user [] 219 | (-> (get-user-pool) 220 | .getCurrentUser 221 | .signOut) 222 | (auth-state/reset-cognito-user {})) 223 | 224 | (defn trigger-password-recovery [] 225 | (let [email (get-in @app-state [:auth-form :forgot :email]) 226 | user (create-cognito-user email)] 227 | (auth-state/clear-form-flash :forgot) 228 | (.forgotPassword user 229 | (clj->js 230 | {:onSuccess (fn [] 231 | (flash-reset-password 232 | {:type :alert-success 233 | :message "Please check your Email for a recovery code."}) 234 | (nav! :forgot-password-complete)) 235 | :onFailure (fn [e] 236 | (flash-forgot 237 | {:type :alert-danger 238 | :message (.-message e)}))})) 239 | )) 240 | 241 | (defn confirm-password-recovery [] 242 | (let [email (get-in @app-state [:auth-form :forgot :email]) 243 | code (get-in @app-state [:auth-form :forgot :code]) 244 | new-password (get-in @app-state [:auth-form :forgot :new-password]) 245 | user (create-cognito-user email)] 246 | (auth-state/clear-form-flash :reset-password) 247 | (.confirmPassword user 248 | code 249 | new-password 250 | (clj->js 251 | {:onSuccess (fn [] 252 | (flash-signin 253 | {:type :alert-success 254 | :message 255 | "Your password has been reset"}) 256 | (nav! :signin)) 257 | :onFailure (fn [e] 258 | (flash-reset-password 259 | {:type :alert-danger 260 | :message 261 | (.-message e)}))})))) 262 | 263 | (defn resend-verification-code [] 264 | (let [email (get-in @app-state [:auth-form :verify :email])] 265 | (auth-state/clear-form-flash :verify) 266 | (.resendConfirmationCode (create-cognito-user email) 267 | (fn [err r] 268 | (if (some? err) 269 | (flash-verify 270 | {:type :alert-danger 271 | :message 272 | (.-message err)}) 273 | (flash-verify 274 | {:type :alert-success 275 | :message 276 | "New confirmation code has been sent. Please check your email."})) 277 | )))) 278 | -------------------------------------------------------------------------------- /resources/public/js/vendor/amazon-cognito-identity.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2016 Amazon.com, 3 | * Inc. or its affiliates. All Rights Reserved. 4 | * 5 | * Licensed under the Amazon Software License (the "License"). 6 | * You may not use this file except in compliance with the 7 | * License. A copy of the License is located at 8 | * 9 | * http://aws.amazon.com/asl/ 10 | * 11 | * or in the "license" file accompanying this file. This file is 12 | * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 13 | * CONDITIONS OF ANY KIND, express or implied. See the License 14 | * for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("aws-sdk/global"),require("aws-sdk/clients/cognitoidentityserviceprovider")):"function"==typeof define&&define.amd?define(["aws-sdk/global","aws-sdk/clients/cognitoidentityserviceprovider"],t):"object"==typeof exports?exports.AmazonCognitoIdentity=t(require("aws-sdk/global"),require("aws-sdk/clients/cognitoidentityserviceprovider")):e.AmazonCognitoIdentity=t(e.AWSCognito,e.AWSCognito.CognitoIdentityServiceProvider)}(this,function(e,t){return function(e){function t(i){if(n[i])return n[i].exports;var s=n[i]={exports:{},id:i,loaded:!1};return e[i].call(s.exports,s,s.exports,t),s.loaded=!0,s.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){"use strict";function i(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n]);return t.default=e,t}function s(e){return e&&e.__esModule?e:{default:e}}Object.defineProperty(t,"__esModule",{value:!0});var r=n(15);Object.keys(r).forEach(function(e){"default"!==e&&"__esModule"!==e&&Object.defineProperty(t,e,{enumerable:!0,get:function(){return r[e]}})});var o=n(12),a=s(o),u=i(r);Object.keys(u).forEach(function(e){a.default[e]=u[e]}),"undefined"!=typeof window&&!window.crypto&&window.msCrypto&&(window.crypto=window.msCrypto)},function(t,n){t.exports=e},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function s(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0});var r=function(){function e(e,t){for(var n=0;n=0;){var o=t*this[e++]+n[i]+s;s=Math.floor(o/67108864),n[i++]=67108863&o}return s}function r(e){return J.charAt(e)}function o(e,t){var n=G[e.charCodeAt(t)];return null==n?-1:n}function a(e){for(var t=this.t-1;t>=0;--t)e[t]=this[t];e.t=this.t,e.s=this.s}function u(e){this.t=1,this.s=e<0?-1:0,e>0?this[0]=e:e<-1?this[0]=e+this.DV:this.t=0}function l(e){var t=i();return t.fromInt(e),t}function c(e,t){var i;if(16==t)i=4;else if(8==t)i=3;else if(2==t)i=1;else if(32==t)i=5;else{if(4!=t)throw new Error("Only radix 2, 4, 8, 16, 32 are supported");i=2}this.t=0,this.s=0;for(var s=e.length,r=!1,a=0;--s>=0;){var u=o(e,s);u<0?"-"==e.charAt(s)&&(r=!0):(r=!1,0==a?this[this.t++]=u:a+i>this.DB?(this[this.t-1]|=(u&(1<>this.DB-a):this[this.t-1]|=u<=this.DB&&(a-=this.DB))}this.clamp(),r&&n.ZERO.subTo(this,this)}function h(){for(var e=this.s&this.DM;this.t>0&&this[this.t-1]==e;)--this.t}function f(e){if(this.s<0)return"-"+this.negate().toString();var t;if(16==e)t=4;else if(8==e)t=3;else if(2==e)t=1;else if(32==e)t=5;else{if(4!=e)throw new Error("Only radix 2, 4, 8, 16, 32 are supported");t=2}var n,i=(1<0)for(u>u)>0&&(s=!0,o=r(n));a>=0;)u>(u+=this.DB-t)):(n=this[a]>>(u-=t)&i,u<=0&&(u+=this.DB,--a)),n>0&&(s=!0),s&&(o+=r(n));return s?o:"0"}function d(){var e=i();return n.ZERO.subTo(this,e),e}function v(){return this.s<0?this.negate():this}function g(e){var t=this.s-e.s;if(0!=t)return t;var n=this.t;if(t=n-e.t,0!=t)return this.s<0?-t:t;for(;--n>=0;)if(0!=(t=this[n]-e[n]))return t;return 0}function m(e){var t,n=1;return 0!=(t=e>>>16)&&(e=t,n+=16),0!=(t=e>>8)&&(e=t,n+=8),0!=(t=e>>4)&&(e=t,n+=4),0!=(t=e>>2)&&(e=t,n+=2),0!=(t=e>>1)&&(e=t,n+=1),n}function p(){return this.t<=0?0:this.DB*(this.t-1)+m(this[this.t-1]^this.s&this.DM)}function y(e,t){var n;for(n=this.t-1;n>=0;--n)t[n+e]=this[n];for(n=e-1;n>=0;--n)t[n]=0;t.t=this.t+e,t.s=this.s}function S(e,t){for(var n=e;n=0;--n)t[n+o+1]=this[n]>>s|a,a=(this[n]&r)<=0;--n)t[n]=0;t[o]=a,t.t=this.t+o+1,t.s=this.s,t.clamp()}function w(e,t){t.s=this.s;var n=Math.floor(e/this.DB);if(n>=this.t)return void(t.t=0);var i=e%this.DB,s=this.DB-i,r=(1<>i;for(var o=n+1;o>i;i>0&&(t[this.t-n-1]|=(this.s&r)<>=this.DB;if(e.t>=this.DB;i+=this.s}else{for(i+=this.s;n>=this.DB;i-=e.s}t.s=i<0?-1:0,i<-1?t[n++]=this.DV+i:i>0&&(t[n++]=i),t.t=n,t.clamp()}function A(e,t){var i=this.abs(),s=e.abs(),r=i.t;for(t.t=r+s.t;--r>=0;)t[r]=0;for(r=0;r=0;)e[n]=0;for(n=0;n=t.DV&&(e[n+t.t]-=t.DV,e[n+t.t+1]=1)}e.t>0&&(e[e.t-1]+=t.am(n,t[n],e,2*n,0,1)),e.s=0,e.clamp()}function U(e,t,s){var r=e.abs();if(!(r.t<=0)){var o=this.abs();if(o.t0?(r.lShiftTo(c,a),o.lShiftTo(c,s)):(r.copyTo(a),o.copyTo(s));var h=a.t,f=a[h-1];if(0!=f){var d=f*(1<1?a[h-2]>>this.F2:0),v=this.FV/d,g=(1<=0&&(s[s.t++]=1,s.subTo(C,s)),n.ONE.dlShiftTo(h,C),C.subTo(a,a);a.t=0;){var w=s[--y]==f?this.DM:Math.floor(s[y]*v+(s[y-1]+p)*g);if((s[y]+=a.am(0,w,s,S,0,h))0&&s.rShiftTo(c,s),u<0&&n.ZERO.subTo(s,s)}}}function E(e){var t=i();return this.abs().divRemTo(e,null,t),this.s<0&&t.compareTo(n.ZERO)>0&&e.subTo(t,t),t}function D(){if(this.t<1)return 0;var e=this[0];if(0==(1&e))return 0;var t=3&e;return t=t*(2-(15&e)*t)&15,t=t*(2-(255&e)*t)&255,t=t*(2-((65535&e)*t&65535))&65535,t=t*(2-e*t%this.DV)%this.DV,t>0?this.DV-t:-t}function I(e){return 0==this.compareTo(e)}function b(e,t){for(var n=0,i=0,s=Math.min(e.t,this.t);n>=this.DB;if(e.t>=this.DB;i+=this.s}else{for(i+=this.s;n>=this.DB;i+=e.s}t.s=i<0?-1:0,i>0?t[n++]=i:i<-1&&(t[n++]=this.DV+i),t.t=n,t.clamp()}function P(e){var t=i();return this.addTo(e,t),t}function R(e){var t=i();return this.subTo(e,t),t}function F(e){var t=i();return this.multiplyTo(e,t),t}function _(e){var t=i();return this.divRemTo(e,t,null),t}function B(e){this.m=e,this.mp=e.invDigit(),this.mpl=32767&this.mp,this.mph=this.mp>>15,this.um=(1<0&&this.m.subTo(t,t),t}function M(e){var t=i();return e.copyTo(t),this.reduce(t),t}function N(e){for(;e.t<=this.mt2;)e[e.t++]=0;for(var t=0;t>15)*this.mpl&this.um)<<15)&e.DM;for(n=t+this.m.t,e[n]+=this.m.am(0,i,e,t,0,this.m.t);e[n]>=e.DV;)e[n]-=e.DV,e[++n]++}e.clamp(),e.drShiftTo(this.m.t,e),e.compareTo(this.m)>=0&&e.subTo(this.m,e)}function V(e,t){e.squareTo(t),this.reduce(t)}function K(e,t,n){e.multiplyTo(t,n),this.reduce(n)}function q(e,t){var n,s=e.bitLength(),r=l(1),o=new B(t);if(s<=0)return r;n=s<18?1:s<48?3:s<144?4:s<768?5:6;var a=new Array,u=3,c=n-1,h=(1<1){var f=i();for(o.sqrTo(a[1],f);u<=h;)a[u]=i(),o.mulTo(f,a[u-2],a[u]),u+=2}var d,v,g=e.t-1,p=!0,y=i();for(s=m(e[g])-1;g>=0;){for(s>=c?d=e[g]>>s-c&h:(d=(e[g]&(1<0&&(d|=e[g-1]>>this.DB+s-c)),u=n;0==(1&d);)d>>=1,--u;if((s-=u)<0&&(s+=this.DB,--g),p)a[d].copyTo(r),p=!1;else{for(;u>1;)o.sqrTo(r,y),o.sqrTo(y,r),u-=2;u>0?o.sqrTo(r,y):(v=r,r=y,y=v),o.mulTo(y,a[d],r)}for(;g>=0&&0==(e[g]&1<0&&void 0!==arguments[0]?arguments[0]:{},n=t.AccessToken;i(this,e),this.jwtToken=n||""}return s(e,[{key:"getJwtToken",value:function(){return this.jwtToken}},{key:"getExpiration",value:function(){var e=this.jwtToken.split(".")[1],t=JSON.parse(r.util.base64.decode(e).toString("utf8"));return t.exp}}]),e}();t.default=o},function(e,t,n){"use strict";function i(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0});var s=function(){function e(e,t){for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:{},n=t.IdToken;i(this,e),this.jwtToken=n||""}return s(e,[{key:"getJwtToken",value:function(){return this.jwtToken}},{key:"getExpiration",value:function(){var e=this.jwtToken.split(".")[1],t=JSON.parse(r.util.base64.decode(e).toString("utf8"));return t.exp}}]),e}();t.default=o},function(e,t){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0});var i=function(){function e(e,t){for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:{},i=t.RefreshToken;n(this,e),this.token=i||""}return i(e,[{key:"getToken",value:function(){return this.token}}]),e}();t.default=s},function(e,t,n){"use strict";function i(e){return e&&e.__esModule?e:{default:e}}function s(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0});var r=function(){function e(e,t){for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:{},i=t.Name,s=t.Value;n(this,e),this.Name=i||"",this.Value=s||""}return i(e,[{key:"getValue",value:function(){return this.Value}},{key:"setValue",value:function(e){return this.Value=e,this}},{key:"getName",value:function(){return this.Name}},{key:"setName",value:function(e){return this.Name=e,this}},{key:"toString",value:function(){return JSON.stringify(this)}},{key:"toJSON",value:function(){return{Name:this.Name,Value:this.Value}}}]),e}();t.default=s},function(e,t){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0});var i=function(){function e(e,t){for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:{},i=t.IdToken,s=t.RefreshToken,r=t.AccessToken;if(n(this,e),null==r||null==i)throw new Error("Id token and Access Token must be present.");this.idToken=i,this.refreshToken=s,this.accessToken=r}return i(e,[{key:"getIdToken",value:function(){return this.idToken}},{key:"getRefreshToken",value:function(){return this.refreshToken}},{key:"getAccessToken",value:function(){return this.accessToken}},{key:"isValid",value:function(){var e=Math.floor(new Date/1e3);return e