1
0
Fork 0
mirror of https://github.com/heyarne/airsonic-ui.git synced 2026-05-07 02:33:39 +02:00

Move navigation to interceptor

Squashed commit of the following:

commit c8bf5e0cb4fd95935e06dc46dda38256f5bb970f
Author: Arne Schlüter <arne@schlueter.is>
Date:   Wed Aug 1 11:37:43 2018 +0200

    Start credential verification only if there are previous credentials

commit 61e6f2e7f2fb4d01e59c71c5980b1b761fa0bf83
Author: Arne Schlüter <arne@schlueter.is>
Date:   Wed Aug 1 10:22:31 2018 +0200

    Make `dispatches?` helper return a boolean

commit 4dc10acd5f1eae616d62c24e3cb9685e4e595f04
Author: Arne Schlüter <arne@schlueter.is>
Date:   Wed Aug 1 09:19:49 2018 +0200

    Add joker for linting

commit 7069febff0ed49be5c60e6787bfc9dc5b758917b
Author: Arne Schlüter <arne@schlueter.is>
Date:   Tue Jul 31 14:17:41 2018 +0200

    Implement navigation as interceptor

    FIXME: Unauthorized access doesn't redirect to `#/login?redirect=...`

commit 60f9f03dd86f48234133e76dd57c067afb7a74d4
Author: Arne Schlüter <arne@schlueter.is>
Date:   Wed Jul 18 19:35:47 2018 +0200

    Make booting explicit and prepare for :navigate interceptor
This commit is contained in:
Arne Schlüter 2018-08-01 11:39:24 +02:00
commit 727d454871
14 changed files with 257 additions and 221 deletions

View file

@ -6,7 +6,6 @@
[akiroz.re-frame.storage :as storage]
;; our app
[airsonic-ui.audio] ; <- just registers effects here
[airsonic-ui.routes :as routes]
[airsonic-ui.events :as events]
[airsonic-ui.views :as views]
[airsonic-ui.config :as config]))

View file

@ -1,4 +1,5 @@
(ns airsonic-ui.db)
(def default-db
{:notifications (sorted-map)})
{:is-booting? true
:notifications (sorted-map)})

View file

@ -24,85 +24,83 @@
;; * sending out the appropriate requests
;; ---
(defn initialize-app
[{{:keys [credentials]} :store} _]
(let [effects {:db db/default-db
:routes/start-routing nil}]
(if (not (empty? credentials))
(assoc effects :dispatch [:credentials/verify credentials])
effects)))
(re-frame/reg-event-fx
::initialize-app
(fn [_]
{:db db/default-db
:dispatch [:init-flow/restore-previous-session]}))
(defn restore-previous-session
"See comment above for different steps; what's important here is that we check
for a previous session before anything else, otherwise we might run into auth
troubles with our router."
[{:keys [db store]} _]
(let [credentials (:credentials store)]
{:dispatch-n [(if credentials
[:init-flow/credentials-found credentials]
[:init-flow/credentials-not-found])]
:routes/start-routing nil}))
(re-frame/reg-event-fx
:init-flow/restore-previous-session
[(re-frame/inject-cofx :store)]
restore-previous-session)
initialize-app)
(defn credentials-found [_ [_ {:keys [u p server]}]]
{:dispatch [:credentials/verification-request u p server]})
(defn verify-credentials
"Initializes the whole authentication chain when we have locally stored
credentials that look plausible."
[_ [_ credentials]]
;; TODO: spec this
(if (every? string? ((juxt :u :p :server) credentials))
{:dispatch [:credentials/send-authentication-request credentials]}))
(re-frame/reg-event-fx :init-flow/credentials-found credentials-found)
;; we don't do anything special here, it's just for the sake of clarity
(defn credentials-not-found
[cofx _]
(assoc-in cofx [:db :credentials] :credentials/not-found))
(re-frame/reg-event-fx :init-flow/credentials-not-found credentials-not-found)
(re-frame/reg-event-fx :credentials/verify verify-credentials)
;; ---
;; auth logic
;; ---
(defn credentials-verification-request
(defn user-login
"Gets called after the user clicked on the login button"
[cofx [_ user pass server]]
(let [credentials {:u user, :p pass, :server server, :verified? false}]
(-> (assoc-in cofx [:db :credentials] credentials)
(assoc :dispatch [:credentials/send-authentication-request credentials]))))
(re-frame/reg-event-fx :credentials/user-login user-login)
(defn authentication-request
"Tries to authenticate a user by pinging the server with credentials, saving
them when the request was successful. Bypasses the request when a user saved
their credentials."
[_ [_ user pass server]]
{:http-xhrio {:method :get
:uri (api/url server "ping" {:u user :p pass})
:response-format (ajax/json-response-format {:keywords? true})
:on-success [:credentials/verification-response user pass server]
:on-failure [:credentials/verification-failure]}})
[cofx [_ credentials]]
(assoc cofx :http-xhrio {:method :get
:uri (api/url (:server credentials) "ping" (select-keys credentials [:u :p]))
:response-format (ajax/json-response-format {:keywords? true})
:on-success [:credentials/authentication-response credentials]
:on-failure [:api/bad-response]}))
(re-frame/reg-event-fx :credentials/verification-request credentials-verification-request)
(re-frame/reg-event-fx :credentials/send-authentication-request authentication-request)
(defn credentials-verification-response
(defn authentication-response
"Since we don't get real status codes, we have to look into the server's
response and see whether we actually sent the correct credentials"
[fx [_ user pass server response]]
{:dispatch (if (api/is-error? response)
[:credentials/verification-failure response]
[:credentials/verified user pass server])})
[fx [_ credentials response]]
(assoc fx :dispatch (if (api/is-error? response)
[:credentials/authentication-failure response]
[:credentials/authentication-success (assoc credentials :verified? true)])))
(re-frame/reg-event-fx :credentials/verification-response credentials-verification-response)
(re-frame/reg-event-fx :credentials/authentication-response authentication-response)
(defn credentials-verification-failure [fx [_ response]]
(-> (assoc-in fx [:db :credentials] :credentials/verification-failure)
(assoc :dispatch [:notification/show :error (api/error-msg (api/->exception response))])))
(defn authentication-failure
"Removes all stored credentials and displays potential api errors to the user"
[fx [_ response]]
(-> (assoc fx :dispatch [:notification/show :error (api/error-msg (api/->exception response))])
(update :store dissoc :credentials)
(update :db dissoc :credentials)))
(re-frame/reg-event-fx :credentials/verification-failure credentials-verification-failure)
(re-frame/reg-event-fx :credentials/authentication-failure authentication-failure)
(defn credentials-verified
(defn authentication-success
"Gets called after the server indicates that the credentials entered by a user
are correct (see `credentials-verification-request`)"
[{:keys [db]} [_ user pass server]]
(let [credentials {:u user :p pass :server server}]
{:routes/set-credentials credentials
:store {:credentials credentials}
:db (assoc db :credentials credentials)
:dispatch [::logged-in]}))
[{:keys [db]} [_ credentials]]
{:store {:credentials credentials}
:db (assoc db :credentials (assoc credentials :verified? true))
:dispatch [::logged-in]})
(re-frame/reg-event-fx :credentials/verified credentials-verified)
(re-frame/reg-event-fx :credentials/authentication-success authentication-success)
;; TODO: We have to find another solution for this once we have routes that
;; don't require a login but have the bottom controls
@ -112,11 +110,11 @@
(fn [_]
(.. js/document -documentElement -classList (add "has-navbar-fixed-bottom"))))
(defn logged-in
[cofx _]
(let [redirect (or (get-in cofx [:routes/from-query-param :redirect]) [::routes/main])]
{:routes/navigate redirect
(let [redirect (or (get-in cofx [:routes/from-query-param :redirect])
[::routes/main])]
{:dispatch [:routes/do-navigation redirect]
:show-nav-bar nil}))
(re-frame/reg-event-fx
@ -128,12 +126,12 @@
"Clears all credentials and redirects the user to the login page"
[cofx [_ & args]]
(let [args (apply hash-map args)]
{:routes/navigate (if-let [redirect (:redirect-to args)]
[::routes/login {} {:redirect (routes/encode-route redirect)}]
[::routes/login])
:routes/unset-credentials nil
{:dispatch [:routes/do-navigation (if-let [redirect (:redirect-to args)]
[::routes/login {} {:redirect (routes/encode-route redirect)}]
[::routes/login])]
:store nil
:db (merge (:db cofx) db/default-db {:credentials :credentials/logged-out})}))
:db (-> (merge (:db cofx) db/default-db)
(dissoc :credentials))}))
(re-frame/reg-event-fx ::logout logout)
@ -223,8 +221,9 @@
;; ---
(re-frame/reg-event-fx
:routes/navigation
:routes/did-navigate
(fn [{:keys [db]} [_ route params query]]
;; FIXME: This leads to an ugly "unregistered event handler `nil`" error
;; all the naviagation logic is in routes.cljs; all we need to do here
;; is say what actually happens once we've navigated succesfully
{:db (assoc db :current-route [route params query])

View file

@ -7,7 +7,7 @@
(def router
(r/router [["/" ::login]
["/hello" ::main]
["/main" ::main]
["/artist/:id" ::artist-view]
["/album/:id" ::album-view]]))
@ -46,31 +46,39 @@
;; holding credentials, which is necessary to restrict certain routes, and the
;; last one is used for actual navigation
(def credentials (atom nil))
;; the event to initialize navigation is implemented so the coeffect map is
;; returned unaltered, we just need access to the current app database for
;; authentication, which we get with an interceptor
(re-frame/reg-fx
:routes/set-credentials
(fn [credentials']
(reset! credentials credentials')))
(def ^:private credentials (atom nil))
(re-frame/reg-fx
:routes/unset-credentials
(fn []
(reset! credentials nil)))
(def do-navigation
"An interceptor which performs the navigation after looking up current
credentials in the app database"
(re-frame.core/->interceptor
:id :routes/do-navigation
:after (fn do-navigation [context]
(let [[_ & [route]] (get-in context [:coeffects :event])
;; because :routes/do-navigation is both an event handler and
;; an interceptor, we know that when handling the event (see
;; below) the credentials aren't altered anymore
credentials'(get-in context [:coeffects :db :credentials])]
(println "calling do-navigation with" route credentials')
(reset! credentials credentials')
(apply r/navigate! router route)
context))))
(re-frame/reg-fx
:routes/navigate
(fn [[route-id params query]]
(println "calling ::navigate with" route-id params query)
(r/navigate! router route-id params query)))
(re-frame/reg-event-fx :routes/do-navigation do-navigation (fn [& _] nil))
(defn can-access? [route]
(or (not (protected-routes route)) @credentials))
(or (not (protected-routes route))
(:verified? @credentials)))
(defn on-navigate
[route-id params query]
(println "on-navigate is called" route-id params query credentials)
(if (can-access? route-id)
(re-frame/dispatch [:routes/navigation route-id params query])
(re-frame/dispatch [:routes/did-navigate route-id params query])
(re-frame/dispatch [:routes/unauthorized route-id params query])))
(defn encode-route
@ -89,11 +97,13 @@
[]
(r/match router (subs (.. js/window -location -hash) 1)))
;; add the current route to our coeffect map
(re-frame/reg-cofx
:routes/current-route
(fn [coeffects _]
(assoc coeffects :routes/current-route (current-route))))
;; add route into from a URL parameter to our coeffect map
(re-frame/reg-cofx
:routes/from-query-param
(fn [coeffects param]

View file

@ -3,19 +3,18 @@
[airsonic-ui.utils.api :as api]))
(defn is-booting?
"Predicate to tell whether our app is still in the process of initialization"
[{:keys [credentials]} _]
(and (not (map? credentials))
(not (#{:credentials/not-found :credentials/verification-failure :credentials/logged-out} credentials))))
"The boot process starts with setting up routing and continues if we found
previous credentials and ends when we receive a response from the server."
[db _]
;; so either we don't have any credentials or they are not verified
(or (empty? (:current-route db))
(and (not (empty? (:credentials db)))
(not (get-in db [:credentials :verified?])))))
(re-frame/reg-sub ::is-booting? is-booting?)
;; can be used to query the user's credentials
(re-frame/reg-sub
::credentials
(fn [db _]
(:credentials db)))
(defn credentials [db _] (:credentials db))
(re-frame/reg-sub ::credentials credentials)
(re-frame/reg-sub
::user
@ -65,7 +64,5 @@
;; user notifications
(re-frame/reg-sub
::notifications
(fn [db _]
(:notifications db)))
(defn notifications [db _] (:notifications db))
(re-frame/reg-sub ::notifications notifications)

View file

@ -1,12 +1,10 @@
(ns airsonic-ui.views
(:require [re-frame.core :refer [dispatch subscribe]]
[airsonic-ui.config :as config]
[airsonic-ui.routes :as routes :refer [url-for]]
[airsonic-ui.events :as events]
[airsonic-ui.subs :as subs]
[airsonic-ui.views.notifications :refer [notification-list]]
[airsonic-ui.views.loading-spinner :refer [loading-spinner]]
[airsonic-ui.views.breadcrumbs :refer [breadcrumbs]]
[airsonic-ui.views.bottom-bar :refer [bottom-bar]]
[airsonic-ui.views.login :refer [login-form]]
@ -49,30 +47,33 @@
;; putting everything together
(defn app [route params query]
(defn app [route-id params query]
(let [user @(subscribe [::subs/user])
content @(subscribe [::subs/current-content])]
(if (= route ::routes/login)
[login-form]
[:div
[:main.columns
[:div.column.is-2.sidebar
[sidebar user]]
[:div.column
[:section.section
[breadcrumbs content]
(case route
::routes/main [most-recent content]
::routes/artist-view [artist-detail content]
::routes/album-view [album-detail content])]]]
[bottom-bar]])))
[:div
[:main.columns
[:div.column.is-2.sidebar
[sidebar user]]
[:div.column
[:section.section
[breadcrumbs content]
(case route-id
::routes/main [most-recent content]
::routes/artist-view [artist-detail content]
::routes/album-view [album-detail content])]]]
[bottom-bar]]))
(defn main-panel []
(let [notifications @(subscribe [::subs/notifications])
is-booting? @(subscribe [::subs/is-booting?])
[route params query] @(subscribe [::subs/current-route])]
[route-id params query] @(subscribe [::subs/current-route])]
(println "route-id" route-id (case route-id
::routes/login "::routes/login"
"something else"))
[:div
[notification-list notifications]
(if is-booting?
[:div.app-loading>div.loader]
[app route params query] )]))
(case route-id
::routes/login [login-form]
[app route-id params query]))]))

View file

@ -16,7 +16,7 @@
server (r/atom (.. js/window -location -origin))
submit (fn [e]
(.preventDefault e)
(dispatch [:credentials/verification-request @user @pass @server]))]
(dispatch [:credentials/user-login @user @pass @server]))]
(fn []
[:section.hero.is-fullheight>div.hero-body
[:div.container.has-text-centered>div.column.is-4.is-offset-4