diff --git a/project.clj b/project.clj index e36c694..89b22f5 100644 --- a/project.clj +++ b/project.clj @@ -2,7 +2,8 @@ :description "FIXME: write description" :url "https://github.com/kalouantonis/channel" - :dependencies [[compojure "1.5.1"] + :dependencies [[buddy "1.3.0"] + [compojure "1.5.1"] [conman "0.6.2"] [cprop "0.1.9"] [luminus-immutant "0.2.2"] diff --git a/src/clj/channel/middleware.clj b/src/clj/channel/middleware.clj index 9d3cc62..a7116a0 100644 --- a/src/clj/channel/middleware.clj +++ b/src/clj/channel/middleware.clj @@ -1,11 +1,14 @@ (ns channel.middleware - (:require [channel.env :refer [defaults]] + (:require [buddy.auth.backends :as backends] + [buddy.auth.middleware :refer [wrap-authentication]] + [channel.env :refer [defaults]] [clojure.tools.logging :as log] [channel.layout :refer [*app-context* error-page]] [ring.middleware.anti-forgery :refer [wrap-anti-forgery]] [ring.middleware.webjars :refer [wrap-webjars]] [ring.middleware.format :refer [wrap-restful-format]] [channel.config :refer [env]] + [mount.core :as mount] [ring.middleware.defaults :refer [site-defaults wrap-defaults]]) (:import [javax.servlet ServletContext])) @@ -53,6 +56,7 @@ (defn wrap-base [handler] (-> ((:middleware defaults) handler) + (wrap-authentication (backends/jws {:secret (env :jwt-secret)})) wrap-webjars (wrap-defaults (-> site-defaults diff --git a/src/clj/channel/routes/services.clj b/src/clj/channel/routes/services.clj index 22757da..b23bacb 100644 --- a/src/clj/channel/routes/services.clj +++ b/src/clj/channel/routes/services.clj @@ -1,50 +1,59 @@ (ns channel.routes.services - (:require [channel.routes.services.songs :as songs] + (:require [channel.routes.services.auth :as auth] + [channel.routes.services.songs :as songs] [compojure.api.sweet :refer :all] [compojure.api.upload :as upload] - [ring.util.http-response :as ring-response] [schema.core :as s])) (defapi service-routes {:swagger {:ui "/swagger-ui" :spec "/swagger.json" :data {:info {:version "1.0.0" - :title "Sound file API" - :description "API for uploading sound files."}}}} + :title "Channel API" + :description "API for the Channel web app"}}}} + + (context "/api/auth" [] + :tags ["auth"] + + (POST "/login" req + :summary "Authenticate user" + :body-params [username :- s/Str, password :- s/Str] + :return s/Str + (auth/login username password req))) (context "/api/songs" [] :tags ["songs"] (GET "/" [] - :return [songs/Song] :summary "Retrieve all songs." + :return [songs/Song] (songs/all-songs)) ;; possible solution is to get the API to request ID3 data first, ;; then submit with the full required track data. (POST "/" [] - :return songs/Song - :multipart-params [file :- upload/TempFileUpload] - :middleware [upload/wrap-multipart-params] :summary "Create a new song using an MP3 file." :description "All song data is extracted from the ID3 metadata of the MP3" + :multipart-params [file :- upload/TempFileUpload] + :return songs/Song + :middleware [upload/wrap-multipart-params] (songs/create-song! file)) (GET "/:id" [] + :summary "Retrieve a specific song." :return (s/maybe songs/Song) :path-params [id :- Long] - :summary "Retrieve a specific song." (songs/get-song id)) (PUT "/:id" [] - :return songs/Song - :path-params [id :- Long] - :body [song songs/UpdatedSong] :summary "Update song details." + :path-params [id :- s/Int] + :body [song songs/UpdatedSong] + :return songs/Song (songs/update-song! id song)) (DELETE "/:id" [] - :return nil - :path-params [id :- Long] :summary "Delete a specific song." + :path-params [id :- s/Int] + :return nil (songs/delete-song! id)))) diff --git a/src/clj/channel/routes/services/auth.clj b/src/clj/channel/routes/services/auth.clj new file mode 100644 index 0000000..01ebe72 --- /dev/null +++ b/src/clj/channel/routes/services/auth.clj @@ -0,0 +1,30 @@ +(ns channel.routes.services.auth + (:require [buddy.hashers :as hashers] + [buddy.sign.jwt :as jwt] + [channel.config :refer [env]] + [channel.db.core :as db] + [clojure.tools.logging :as log] + [ring.util.http-response :as ring-response] + [schema.core :as s])) + +(s/defschema User {:id s/Int + :username s/Str + :email (s/maybe s/Str)}) + +(defn create-auth-token [user] + (jwt/sign (dissoc user :password) (env :jwt-secret))) + +(defn login + "Authenticate user using their `username` and `password`. Also + expects the request map." + [username password {:keys [remote-addr server-name]}] + (if-let [user (db/user-by-username {:username username})] + (if (hashers/check password (:password user)) + (ring-response/ok (create-auth-token user)) + (do + ;; Log failed logins to monitor against attacks. + ;; TODO: make this toggleable, some users may not + ;; TODO: want ANY addresses tracked + (log/info "login failed for" username remote-addr server-name) + (ring-response/unauthorized "Invalid login credentials"))) + (ring-response/not-found))) diff --git a/src/clj/channel/routes/services/songs.clj b/src/clj/channel/routes/services/songs.clj index 78941ad..073e183 100644 --- a/src/clj/channel/routes/services/songs.clj +++ b/src/clj/channel/routes/services/songs.clj @@ -7,13 +7,13 @@ [ring.util.http-response :as ring-response] [channel.db.core :as db])) -(s/defschema Song {:id Long - :title String - :artist (s/maybe String) - :album (s/maybe String) - :genre (s/maybe String) +(s/defschema Song {:id s/Int + :title s/Str + :artist (s/maybe s/Str) + :album (s/maybe s/Str) + :genre (s/maybe s/Str) :track s/Int - :file String}) + :file s/Str}) ;; Data required to update a song (s/defschema UpdatedSong (dissoc Song :id :file))