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

Add user role checks, see #14

Squashed commit of the following:

commit 393c481a21fa97881be2b6859e9acaa8ab7abb7f
Author: Arne Schlüter <arne@schlueter.is>
Date:   Wed Sep 5 12:04:56 2018 +0200

    Consider user roles when building up the navigation

commit d631cba1174ecf42b682664bf57c41b88b7f5ed4
Author: Arne Schlüter <arne@schlueter.is>
Date:   Wed Sep 5 11:52:05 2018 +0200

    Save user roles on login

commit e68ced335ccc11a2daebbf12bb4061a53935c268
Author: Arne Schlüter <arne@schlueter.is>
Date:   Wed Sep 5 10:25:19 2018 +0200

    Rename dispatch to muted-dispatch for easier disambiguation
This commit is contained in:
Arne Schlüter 2018-09-05 12:05:43 +02:00
commit 5cbb83a22d
12 changed files with 180 additions and 53 deletions

View file

@ -1,6 +1,7 @@
(ns airsonic-ui.api.subs
(:require [clojure.string :as str]
[re-frame.core :refer [reg-sub]]))
[re-frame.core :refer [reg-sub]]
[airsonic-ui.helpers :refer [kebabify]]))
(defn response-for
"Returns the cached response for a single endpoint"
@ -18,9 +19,7 @@
[endpoint-str]
(-> (str/replace endpoint-str #"^(get|create|update|delete)" "")
(str/replace #"\d+$" "")
(str/replace #"([a-z])([A-Z])" (fn [[_ a b]] (str a "-" b)))
(str/lower-case)
(keyword)))
(kebabify)))
(defn route-data
"Given a list of event vectors, returns that responses for all API requests."

View file

@ -1,6 +1,6 @@
(ns airsonic-ui.components.audio-player.views
(:require [re-frame.core :refer [subscribe]]
[airsonic-ui.helpers :refer [add-classes dispatch]]
[airsonic-ui.helpers :refer [add-classes muted-dispatch]]
[airsonic-ui.views.cover :refer [cover]]
[airsonic-ui.views.icon :refer [icon]]))
@ -20,19 +20,19 @@
[:media-step-forward :audio-player/next-song]]]
(map (fn [[icon-glyph event]]
^{:key icon-glyph} [:p.control>button.button.is-light
{:on-click (dispatch [event])}
{:on-click (muted-dispatch [event])}
[icon icon-glyph]])
buttons))])
(defn- toggle-shuffle [playback-mode]
(dispatch [:audio-player/set-playback-mode (if (= playback-mode :shuffled)
(muted-dispatch [:audio-player/set-playback-mode (if (= playback-mode :shuffled)
:linear :shuffled)]))
(defn- toggle-repeat-mode [current-mode]
(let [modes (cycle '(:repeat-none :repeat-all :repeat-single))
next-mode (->> (drop-while (partial not= current-mode) modes)
(second))]
(dispatch [:audio-player/set-repeat-mode next-mode])))
(muted-dispatch [:audio-player/set-repeat-mode next-mode])))
(defn playback-mode-controls [playlist]
(let [{:keys [repeat-mode playback-mode]} playlist

View file

@ -37,6 +37,8 @@
[(re-frame/inject-cofx :store)]
initialize-app)
(re-frame/dispatch [:api/request "getUser" {:username "arne"}])
(defn verify-credentials
"Initializes the whole authentication chain when we have locally stored
credentials that look plausible."
@ -61,12 +63,13 @@
(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."
"Tries to authenticate a user by requesting info about the given user, saving
the credentials when the request was successful."
[cofx [_ credentials]]
(assoc cofx :http-xhrio {:method :get
:uri (api/url (:server credentials) "ping" (select-keys credentials [:u :p]))
:uri (api/url (:server credentials) "getUser"
(merge (select-keys credentials [:u :p])
{:username (:u credentials)}))
:response-format (ajax/json-response-format {:keywords? true})
:on-success [:credentials/authentication-response credentials]
:on-failure [:api/failed-response]})) ; <- we don't need endpoint and params here because the response is not cached
@ -79,7 +82,7 @@
[fx [_ credentials response]]
(assoc fx :dispatch (if (api/is-error? response)
[:credentials/authentication-failure response]
[:credentials/authentication-success (assoc credentials :verified? true)])))
[:credentials/authentication-success credentials response])))
(re-frame/reg-event-fx :credentials/authentication-response authentication-response)
@ -95,9 +98,10 @@
(defn authentication-success
"Gets called after the server indicates that the credentials entered by a user
are correct (see `credentials-verification-request`)"
[{:keys [db]} [_ credentials]]
[{:keys [db]} [_ credentials auth-response]]
{:store {:credentials credentials}
:db (assoc db :credentials (assoc credentials :verified? true))
:db (-> (assoc db :credentials (assoc credentials :verified? true))
(assoc :user (api/unwrap-response auth-response)))
:dispatch [::logged-in]})
(re-frame/reg-event-fx :credentials/authentication-success authentication-success)

View file

@ -1,6 +1,7 @@
(ns airsonic-ui.helpers
"Assorted helper functions"
(:require [re-frame.core :as rf]))
(:require [re-frame.core :as rf]
[clojure.string :as str]))
(defn find-where
"Returns the the first item in `coll` with its index for which `(p song)`
@ -10,7 +11,7 @@
(reduce (fn [_ [idx song]]
(when (p song) (reduced [idx song]))) nil)))
(defn dispatch
(defn muted-dispatch
"Dispatches a re-frame event while canceling default DOM behavior"
[ev]
(fn [e]
@ -22,3 +23,11 @@
[elem & classes]
(keyword (apply str (name elem) (->> (filter identity classes)
(map #(str "." (name %)))))))
(defn kebabify
"Turns camelCased strings and keywords into kebab-cased keywords"
[x]
(-> (if (keyword? x) (name x) x)
(str/replace #"([a-z])([A-Z])" (fn [[_ a b]] (str a "-" b)))
(str/lower-case)
(keyword)))

View file

@ -1,6 +1,9 @@
(ns airsonic-ui.subs
(:require [re-frame.core :refer [reg-sub subscribe]]
[airsonic-ui.api.helpers :as api]))
[airsonic-ui.api.helpers :as api]
[airsonic-ui.helpers :refer [kebabify]]
[debux.cs.core :refer-macros [dbg]]
[clojure.string :as str]))
(defn is-booting?
"The boot process starts with setting up routing and continues if we found
@ -16,11 +19,46 @@
(defn credentials [db _] (:credentials db))
(reg-sub ::credentials credentials)
;; ---
;; user info and roles
;; ---
(defn user-info
"Returns the response to getUser?username=$name; this isn't cached like the
other responses because it's not retrieved via :api/request"
[db _]
(:user db))
(reg-sub :user/info user-info)
(defn user-roles
"Takes only the roles out of a getUser response to make it easier to work with"
[user-info _]
(->>
(filter (fn [[k _]] (re-find #"Role$" (name k))) user-info)
(keep (fn [[role has-role?]]
(when has-role? (str/replace (name role) #"Role$" ""))))
(map kebabify)
(set)))
(reg-sub
::user
(fn [_ _] [(subscribe [::credentials])])
(fn [[credentials] _]
(when credentials {:name (:u credentials)})))
:user/roles
:<- [:user/info]
user-roles)
(defn user-role
"Can be used to determine whether a user is allowed to do certain things"
[user-roles [_ role]]
(or (user-roles role) (user-roles :admin)))
(reg-sub
:user/role
:<- [:user/roles]
user-role)
;; ---
;; misc
;; ---
(defn cover-url
"Provides a convenient way for views to get cover images so they don't have

View file

@ -21,12 +21,18 @@
(defn navbar-top
"Contains search, some navigational links and the logo"
[_]
[]
(let [active? (r/atom false)
toggle-active #(swap! active? not)
navbar-item (fn navbar-item [{:keys [href]} label]
[:a.navbar-item {:href href :on-click toggle-active} label])]
(fn [{:keys [user]}]
[:a.navbar-item {:href href :on-click toggle-active} label])
user @(subscribe [:user/info])
stream-role @(subscribe [:user/roles :stream])
podcast-role @(subscribe [:user/roles :podcast])
playlist-role @(subscribe [:user/roles :playlist])
share-role @(subscribe [:user/roles :share])
settings-role @(subscribe [:user/roles :settings])]
(fn []
[:nav.navbar.is-fixed-top.is-dark {:role "navigation", :aria-label "search and navigation"}
;; user is `nil` when we're not logged in, we can hide the extended navigation
[:div.navbar-brand
@ -37,25 +43,30 @@
[:div.navbar-start
[:div.navbar-item [search/form]]]
[:div.navbar-end
[:div.navbar-item.has-dropdown.is-hoverable
[:div.navbar-link "Library"]
[:div.navbar-dropdown
[navbar-item {:href (url-for ::routes/library {:criteria "recent"})} "Recently played"]
[navbar-item {:href (url-for ::routes/library {:criteria "newest"})} "Newest additions"]
[navbar-item {:href (url-for ::routes/library {:criteria "starred"})} "Starred"]]]
[navbar-item {} "Podcasts"]
[navbar-item {} "Playlists"]
[navbar-item {} "Shares"]
(when stream-role
[:div.navbar-item.has-dropdown.is-hoverable
[:div.navbar-link "Library"]
[:div.navbar-dropdown
[navbar-item {:href (url-for ::routes/library {:criteria "recent"})} "Recently played"]
[navbar-item {:href (url-for ::routes/library {:criteria "newest"})} "Newest additions"]
[navbar-item {:href (url-for ::routes/library {:criteria "starred"})} "Starred"]]])
(when podcast-role
[navbar-item {} "Podcasts"])
(when playlist-role
[navbar-item {} "Playlists"])
(when share-role
[navbar-item {} "Shares"])
[:div.navbar-item.has-dropdown.is-hoverable
[:div.navbar-link "More"]
[:div.navbar-dropdown.is-right
[navbar-item "Settings"]
(when settings-role
[navbar-item "Settings"])
[:a.navbar-item
{:on-click (fn [_]
(toggle-active)
(dispatch [::events/logout]))
:href "#"}
(str "Logout (" (:name user) ")")]]]]])])))
(str "Logout (" (:username user) ")")]]]]])])))
(defn media-content
"Provides the complete UI to browse the media library, interact with search
@ -77,14 +88,13 @@
(defn main-panel []
(let [notifications @(subscribe [::subs/notifications])
is-booting? @(subscribe [::subs/is-booting?])
[route-id params query] @(subscribe [:routes/current-route])
user @(subscribe [::subs/user])]
[route-id params query] @(subscribe [:routes/current-route])]
[(add-classes :div route-id)
[notification-list notifications]
(if is-booting?
[:div.app-loading>div.loader]
[:div
[navbar-top {:user user}]
[navbar-top]
(case route-id
::routes/login [login-form]
[media-content route-id params query])])]))

View file

@ -1,5 +1,5 @@
(ns airsonic-ui.views.song
(:require [airsonic-ui.helpers :refer [dispatch]]
(:require [airsonic-ui.helpers :refer [muted-dispatch]]
[airsonic-ui.routes :as routes :refer [url-for]]
[airsonic-ui.views.icon :refer [icon]]))
@ -11,7 +11,7 @@
(:artist song))
" - "
[:a
{:href "#" :on-click (dispatch [:audio-player/play-all songs idx])}
{:href "#" :on-click (muted-dispatch [:audio-player/play-all songs idx])}
(:title song)]]))
(defn listing [songs]
@ -19,12 +19,11 @@
(for [[idx song] (map-indexed vector songs)]
^{:key idx} [:tr
[:td.grow [item songs song idx]]
;; FIXME: Not implemented yet
[:td>a {:title "Play next"
:href "#"
:on-click (dispatch [:audio-player/enqueue-next song])}
:on-click (muted-dispatch [:audio-player/enqueue-next song])}
[icon :plus]]
[:td>a {:title "Play last"
:href "#"
:on-click (dispatch [:audio-player/enqueue-last song])}
:on-click (muted-dispatch [:audio-player/enqueue-last song])}
[icon :caret-right]]])])