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

Save login credentials in local storage

Squashed commit of the following:

commit b480676cef
Author: Arne Schlüter <arne@schlueter.is>
Date:   Wed May 30 18:38:40 2018 +0200

    Remember login credentials

commit ed060e55b6
Author: Arne Schlüter <arne@schlueter.is>
Date:   Wed May 30 14:45:11 2018 +0200

    Add tests for auth process

commit ca8972f8c3
Author: Arne Schlüter <arne@schlueter.is>
Date:   Wed May 30 13:34:38 2018 +0200

    Make sure to always run tests in development
This commit is contained in:
Arne Schlüter 2018-05-30 18:40:42 +02:00
commit 3376e01930
8 changed files with 111 additions and 46 deletions

View file

@ -21,10 +21,10 @@ To build the project make sure you have Node.js (v6.0.0), npm and Java 8 install
``` ```
# after cloning the project, first install all dependencies # after cloning the project, first install all dependencies
$ npm install $ npm install
# start a continuous build with hot-code-reloading; first build takes a while. open http://localhost:8080
# start a continuous build with hot-code-reloading and continuous testing
# first build takes a while. open http://localhost:8080
$ npm run dev $ npm run dev
# build and optimize the code once for production
$ npm run build
``` ```
**Note:** In dev mode this project comes with re-frame-10x. You can hit `Ctrl + h` to display the overlay and have a time traveling debugger. **Note:** In dev mode this project comes with re-frame-10x. You can hit `Ctrl + h` to display the overlay and have a time traveling debugger.
@ -36,17 +36,21 @@ This project uses [karma](https://karma-runner.github.io/) for tests. Make sure
``` ```
# run tests once # run tests once
$ npm test $ npm test
# run tests continuously, watching for changes
$ npm run test:watch
``` ```
**Note:** If you want nice console output in your tests, make sure to `(enable-console-print!)`. You can call `println` afterwards like you're used to.
## Build artifacts ## Build artifacts
Everything you need to serve the app can be found inside the `public` folder.
## Deploy to github ## Deployment
``` ```
# will build everything and publish everything in /public via gh-pages # build and optimize the code once for production
$ npm run build
# runs npm run build and publishes everything via gh-pages
$ npm run deploy $ npm run deploy
``` ```
All build artifacts will be output in `/public`. Don't change anything in there as changes will be overwritten.

View file

@ -4,21 +4,19 @@
"description": "Airsonic UI written with re-frame", "description": "Airsonic UI written with re-frame",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "run-s test:compile-once test:run-once",
"test:compile-once": "shadow-cljs compile test",
"test:run-once": "karma start --single-run",
"test:compile-watch": "shadow-cljs watch test",
"test:run-watch": "karma start --reporters growl,progress --auto-watch",
"test:watch": "npm-run-all test:compile-once -p test:compile-watch test:run-watch",
"build:cljs": "shadow-cljs release app", "build:cljs": "shadow-cljs release app",
"build:html": "sed 's/\"\\/app\\//\".\\/app\\//g' src/html/index.html > public/index.html", "build:html": "sed 's/\"\\/app\\//\".\\/app\\//g' src/html/index.html > public/index.html",
"build:sass": "node-sass --output-style compressed src/sass/app.sass public/app/style.css", "build:sass": "node-sass --output-style compressed src/sass/app.sass public/app/style.css",
"build": "rm -r public/*; run-p build:*; ", "build": "rm -r public/*; run-p build:*; ",
"deploy": "npm run build && gh-pages -d public", "deploy": "npm run build && gh-pages -d public",
"dev:cljs": "shadow-cljs watch app", "dev:cljs": "shadow-cljs watch app test",
"dev:html": "sed 's/\"\\.\\/app\\//\"\\/app\\//g' src/html/index.html > public/index.html", "dev:html": "sed 's/\"\\.\\/app\\//\"\\/app\\//g' src/html/index.html > public/index.html",
"dev:sass": "npm run build:sass; node-sass -w src/sass/app.sass public/app/style.css", "dev:sass": "npm run build:sass; node-sass -w src/sass/app.sass public/app/style.css",
"dev": "run-p dev:*" "dev:test": "karma start --reporters growl,progress --auto-watch",
"dev": "npm-run-all test:compile -p dev:*",
"test": "run-s test:compile test:run",
"test:compile": "shadow-cljs compile test",
"test:run": "karma start --single-run"
}, },
"author": "Arne Schlüter", "author": "Arne Schlüter",
"license": "ISC", "license": "ISC",

View file

@ -6,6 +6,7 @@
[[reagent "0.7.0"] [[reagent "0.7.0"]
[re-frame "0.10.5"] [re-frame "0.10.5"]
[day8.re-frame/http-fx "0.1.6"] [day8.re-frame/http-fx "0.1.6"]
[akiroz.re-frame/storage "0.1.2"]
[funcool/bide "1.6.0"] [funcool/bide "1.6.0"]
;; debugging ;; debugging
[day8.re-frame/re-frame-10x "0.3.2-react16"] [day8.re-frame/re-frame-10x "0.3.2-react16"]

View file

@ -1,8 +1,11 @@
(ns airsonic-ui.core (ns airsonic-ui.core
(:require [reagent.core :as reagent] (:require [reagent.core :as reagent]
[re-frame.core :as re-frame] [re-frame.core :as re-frame]
;; 3rd party effects / coeffects
[day8.re-frame.http-fx] [day8.re-frame.http-fx]
[airsonic-ui.audio] ; <- just registers effects [akiroz.re-frame.storage :as storage]
;; our app
[airsonic-ui.audio] ; <- just registers effects here
[airsonic-ui.routes :as routes] [airsonic-ui.routes :as routes]
[airsonic-ui.events :as events] [airsonic-ui.events :as events]
[airsonic-ui.views :as views] [airsonic-ui.views :as views]
@ -19,6 +22,9 @@
(defn ^:export init [] (defn ^:export init []
(routes/start-routing!) (routes/start-routing!)
(storage/reg-co-fx! :airsonic-ui {:fx :store
:cofx :store})
(re-frame/dispatch-sync [::events/initialize-db]) (re-frame/dispatch-sync [::events/initialize-db])
(re-frame/dispatch [::events/try-remember-user])
(dev-setup) (dev-setup)
(mount-root)) (mount-root))

View file

@ -2,6 +2,5 @@
(:require [airsonic-ui.routes :as routes])) (:require [airsonic-ui.routes :as routes]))
(def default-db (def default-db
{:active-requests 0 {;; because navigate! executes asynchronously we force to display the login screen first
;; because navigate! executes asynchronously we force to display the login screen first
:current-route [routes/default-route]}) :current-route [routes/default-route]})

View file

@ -18,31 +18,51 @@
(fn [_] (fn [_]
db/default-db)) db/default-db))
;; this is called with user and password to try and see if the credentials are ;; auth logic
;; correct; if yes, ::auth-success will be fired
(re-frame/reg-event-fx (defn authenticate
::authenticate "Tries to authenticate a user by pinging the server with credentials, saving
(fn [{:keys [db]} [_ user pass server]] them when the request was succesful. Bypasses the request when a user saved
{:db (-> (update db :active-requests inc) their credentials."
(assoc :server server)) [{:keys [db]} [_ user pass server]]
{:db (assoc-in db [:credentials :server] server)
:http-xhrio {:method :get :http-xhrio {:method :get
:uri (api/url server "ping" {:u user :p pass}) :uri (api/url server "ping" {:u user :p pass})
:response-format (ajax/json-response-format {:keywords? true}) :response-format (ajax/json-response-format {:keywords? true})
:on-success [::auth-success user pass] :on-success [::credentials-verified user pass]
:on-failure [::api-failure]}})) :on-failure [::api-failure]}})
;; TODO: Test that credentials are associated
(re-frame/reg-event-fx (re-frame/reg-event-fx
::auth-success ::authenticate authenticate)
(fn [{:keys [db]} [_ user pass response]]
;; TODO: Handle failures differently (defn try-remember-user
(let [login {:u user :p pass}] "Enables skipping the auth request when credentials are saved in the
{:routes/set-credentials login local storage; otherwise has no effect"
:db (-> (update db :active-requests #(max (dec %) 0)) [{:keys [db store]} [_]]
(assoc :login login)) (when-let [credentials (:credentials store)]
:dispatch [::logged-in]}))) {:db (assoc-in db [:credentials :server] (:server credentials))
:dispatch [::credentials-verified (:u credentials) (:p credentials) nil]}))
(re-frame/reg-event-fx
::try-remember-user
[(re-frame/inject-cofx :store)]
try-remember-user)
(defn credentials-verified
"Gets called after the server indicates that the credentials entered by a user
are correct (see `authenticate`)"
[{:keys [db store]} [_event user pass _response]]
(let [auth {:u user :p pass}
credentials (merge (:credentials db) auth)]
{:routes/set-credentials auth
:store {:credentials credentials}
:db (assoc db :credentials credentials)
:dispatch [::logged-in]}))
(re-frame/reg-event-fx
::credentials-verified
[(re-frame/inject-cofx :store)]
credentials-verified)
;; TODO: We have to find another solution for this once we have routes that ;; TODO: We have to find another solution for this once we have routes that
;; don't require a login but have the bottom controls ;; don't require a login but have the bottom controls
@ -63,11 +83,15 @@
;; TODO: Move these in the future? events.cljs should just do wiring. We could ;; TODO: Move these in the future? events.cljs should just do wiring. We could
;; implement api.cljs as a completely independent module. ;; implement api.cljs as a completely independent module.
(defn- api-url [db endpoint params]
(let [creds (:credentials db)]
(api/url (:server creds) endpoint (merge params (select-keys creds [:u :p])))))
(re-frame/reg-event-fx (re-frame/reg-event-fx
:api-request :api-request
(fn [{:keys [db]} [_ endpoint k params]] (fn [{:keys [db]} [_ endpoint k params]]
{:http-xhrio {:method :get {:http-xhrio {:method :get
:uri (api/url (:server db) endpoint (merge params (:login db))) :uri (api-url db endpoint params)
:response-format (ajax/json-response-format {:keywords? true}) :response-format (ajax/json-response-format {:keywords? true})
:on-success [::api-success k] :on-success [::api-success k]
:on-failure [::api-failure]}})) :on-failure [::api-failure]}}))
@ -88,11 +112,15 @@
; TODO: Make play, next and previous a bit prettier and more DRY ; TODO: Make play, next and previous a bit prettier and more DRY
(defn- song-url [db song]
(let [creds (:credentials db)]
(api/song-url (:server creds) (select-keys creds [:u :p]) song)))
(re-frame/reg-event-fx (re-frame/reg-event-fx
; sets up the db, starts to play a song and adds the rest to a playlist ; sets up the db, starts to play a song and adds the rest to a playlist
::play-songs ::play-songs
(fn [{:keys [db]} [_ songs song]] (fn [{:keys [db]} [_ songs song]]
{:play-song (api/song-url (:server db) (:login db) song) {:play-song (song-url db song)
:db (-> db :db (-> db
(assoc-in [:currently-playing :item] song) (assoc-in [:currently-playing :item] song)
(assoc-in [:currently-playing :playlist] songs))})) (assoc-in [:currently-playing :playlist] songs))}))
@ -104,7 +132,7 @@
current (-> db :currently-playing :item) current (-> db :currently-playing :item)
next (first (rest (drop-while #(not= % current) playlist)))] next (first (rest (drop-while #(not= % current) playlist)))]
(when next (when next
{:play-song (api/song-url (:server db) (:login db) next) {:play-song (song-url db next)
:db (assoc-in db [:currently-playing :item] next)})))) :db (assoc-in db [:currently-playing :item] next)}))))
(re-frame/reg-event-fx (re-frame/reg-event-fx
@ -114,7 +142,7 @@
current (-> db :currently-playing :item) current (-> db :currently-playing :item)
previous (last (take-while #(not= % current) playlist))] previous (last (take-while #(not= % current) playlist))]
(when previous (when previous
{:play-song (api/song-url (:server db) (:login db) previous) {:play-song (song-url db previous)
:db (assoc-in db [:currently-playing :item] previous)})))) :db (assoc-in db [:currently-playing :item] previous)}))))
(re-frame/reg-event-fx (re-frame/reg-event-fx

View file

@ -2,17 +2,17 @@
(:require [re-frame.core :as re-frame])) (:require [re-frame.core :as re-frame]))
;; can be used to query the user's credentials ;; can be used to query the user's credentials
;; TODO: Organize login credentials and server location differently (i.e. together)
;; FIXME: this is used for cover images and it's quite ugly tbh
(re-frame/reg-sub (re-frame/reg-sub
::login ::login
(fn [db] (fn [db]
(:login db))) (select-keys (:credentials db) [:u :p])))
(re-frame/reg-sub (re-frame/reg-sub
::server ::server
(fn [db] (fn [db]
(:server db))) (get-in db [:credentials :server])))
;; current hashbang ;; current hashbang

View file

@ -0,0 +1,29 @@
(ns airsonic-ui.events-test
(:require [cljs.test :refer [deftest testing is]]
[clojure.string :as str]
[airsonic-ui.events :as events]))
(enable-console-print!)
(deftest authentication
(testing "Credential verification"
(let [server "https://localhost"
fx (events/authenticate {:db {}} [:_ "user" "pass" server])
request (:http-xhrio fx)]
(testing "uses correct server url"
(is (str/starts-with? (:uri request) server))
(is (str/includes? (:uri request) "/ping")))
(testing "saves the given server location"
(is (= server (get-in fx [:db :credentials :server]))))
(testing "invokes correct success callback"
(is (= ::events/credentials-verified (first (:on-success request)))))))
(testing "On succesfull response"
(let [fx (events/credentials-verified {:db {}} [:_ "user" "pass"])
credentials {:u "user" :p "pass"}]
(testing "credentials are sent to the router for access rights"
(is (= credentials (:routes/set-credentials fx))))
(testing "credentials are saved in the global state"
(is (= credentials (-> (get-in fx [:db :credentials])
(select-keys [:u :p])))))
(testing "the login process is finalized"
(is (= [::events/logged-in] (:dispatch fx)))))))