diff --git a/HappyAPI.svg b/HappyAPI.svg
index 6d829e9..1f4ad53 100644
--- a/HappyAPI.svg
+++ b/HappyAPI.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/README.md b/README.md
index efbd463..0a6e043 100644
--- a/README.md
+++ b/README.md
@@ -135,16 +135,18 @@ and then from a file `happyapi.edn`.
When no port is specified (for example `:redirect_uri "http://localhost/redirect"`), HappyAPI listens on the default http port 80.
Port 80 is a privileged port that requires root permissions, which may be problematic for some users.
-Google allows the `redirect_uri` port to vary.
+Google and GitHub allow the `redirect_uri` port to vary.
Other providers do not.
A random port is a natural choice.
Configuring `:redirect_uri "http://localhost:0/redirect"` will listen on a random port.
-This is the default used for Google if not configured otherwise.
+This is the default used for Google and GitHub if not configured otherwise.
You can choose a port if you'd like.
If you want to listen on port 8080, configure `:redirect_uri "http://localhost:8080/redirect"`
-You need to update your provider settings to match.
-Most providers require an exact match between the provider side settings and client config,
+This is the default used for Twitter if not configured otherwise.
+
+You must update your provider settings to match either the default, or your own `redirect_uri`.
+Providers require an exact match between the provider side settings and client config,
so please check this carefully if you get an error.
### Instrumentation, logging, and metrics
diff --git a/deps.edn b/deps.edn
index 1e4d263..c280993 100644
--- a/deps.edn
+++ b/deps.edn
@@ -1,4 +1,4 @@
-{:paths ["src"]
+{:paths ["src" "resources"]
:deps {org.clojure/clojure {:mvn/version "1.11.3"}
buddy/buddy-sign {:mvn/version "3.5.351"}
diff --git a/docs/favicon.ico b/docs/favicon.ico
new file mode 120000
index 0000000..25b2b84
--- /dev/null
+++ b/docs/favicon.ico
@@ -0,0 +1 @@
+../resources/favicon.ico
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
new file mode 120000
index 0000000..32d46ee
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1 @@
+../README.md
\ No newline at end of file
diff --git a/resources/favicon.ico b/resources/favicon.ico
new file mode 100644
index 0000000..538d0a9
Binary files /dev/null and b/resources/favicon.ico differ
diff --git a/src/happyapi/middleware.clj b/src/happyapi/middleware.clj
index 6f9cf49..d6b40fe 100644
--- a/src/happyapi/middleware.clj
+++ b/src/happyapi/middleware.clj
@@ -168,10 +168,17 @@
keywordize-keys (wrap-keywordize-keys)))
;; TODO: surely there are other cases to consider?
+(defn remove-redundant-data-labels [x]
+ (if (map? x)
+ (cond (contains? x :data) (recur (get x :data))
+ (contains? x "data") (recur (get x "data"))
+ (seq (get x :items)) (get x :items)
+ (seq (get x "items")) (get x "items")
+ :else x)
+ x))
+
(defn extract-result [{:keys [body]}]
- (cond (and (map? body) (seq (get body :items))) (get body :items)
- (and (map? body) (seq (get body "items"))) (get body "items")
- :else body))
+ (remove-redundant-data-labels body))
(defn wrap-extract-result
"When we call an API, we want the logical result of the call, not the map containing body, and status.
diff --git a/src/happyapi/oauth2/capture_redirect.clj b/src/happyapi/oauth2/capture_redirect.clj
index 0355310..191493e 100644
--- a/src/happyapi/oauth2/capture_redirect.clj
+++ b/src/happyapi/oauth2/capture_redirect.clj
@@ -3,10 +3,12 @@
If you are making a web app, implement a route in your app that captures the code parameter.
If you use this namespace, add ring as a dependency in your project."
(:require [clojure.java.browse :as browse]
+ [clojure.java.io :as io]
[clojure.set :as set]
[happyapi.middleware :as middleware]
[happyapi.oauth2.auth :as oauth2]
- [ring.middleware.params :as params]))
+ [ring.middleware.params :as params])
+ (:import (java.io FileInputStream)))
(set! *warn-on-reflection* true)
@@ -18,6 +20,18 @@
(-> (oauth2/provider-login-url config scopes optional)
(browse/browse-url)))
+(defn make-redirect-handler [p]
+ (-> (fn redirect-handler [{:as req :keys [request-method uri params]}]
+ (case [request-method uri]
+ [:get "/favicon.ico"] {:body (io/file (io/resource "favicon.ico"))
+ :status 200}
+ (if (get @(deliver p params) "code")
+ {:status 200
+ :body "Code received, authentication successful."}
+ {:status 400
+ :body "No code in response."})))
+ (params/wrap-params)))
+
(defn fresh-credentials
"Opens a browser to authenticate, waits for a redirect, and returns a code.
Defaults access_type to offline,
@@ -39,15 +53,8 @@
port (if requested-port
(Integer/parseInt requested-port)
80)
- http-redirect-handler (fn [request]
- (if (get @(deliver p (get request :params)) "code")
- {:status 200
- :body "Code received, authentication successful."}
- {:status 400
- :body "No code in response."}))
- handler (params/wrap-params http-redirect-handler)
{:keys [run-server]} fns
- {:keys [port stop]} (run-server handler {:port port})
+ {:keys [port stop]} (run-server (make-redirect-handler p) {:port port})
;; The port may have changed when requesting a random port
config (if requested-port
(assoc config :redirect_uri (str protocol host ":" port path))
@@ -70,7 +77,7 @@
;; wait for the user to get redirected to localhost with a code
{:strs [code state] :as return-params} (deref p login-timeout nil)]
;; allow a bit of time to deliver the response before shutting down the server
- (stop)
+ (Thread. (fn [] (Thread/sleep 1000) (stop)))
(if code
(do
(when-not (= state state-and-challenge)
diff --git a/src/happyapi/oauth2/client.clj b/src/happyapi/oauth2/client.clj
index 886e556..06a255c 100644
--- a/src/happyapi/oauth2/client.clj
+++ b/src/happyapi/oauth2/client.clj
@@ -16,17 +16,20 @@
(defmethod endpoints :google [_]
{:auth_uri "https://accounts.google.com/o/oauth2/auth"
:token_uri "https://oauth2.googleapis.com/token"
- ;; port 0 indicates random port
+ ;; port 0 selects a random port
:redirect_uri "http://localhost:0/redirect"
:authorization_options {:access_type "offline"
:prompt "consent"
:include_granted_scopes true}})
(defmethod endpoints :github [_]
- {:auth_uri "https://github.com/login/oauth/authorize"
- :token_uri "https://github.com/login/oauth/access_token"})
+ {:auth_uri "https://github.com/login/oauth/authorize"
+ :token_uri "https://github.com/login/oauth/access_token"
+ ;; port 0 selects a random port
+ :redirect_uri "http://localhost:0/redirect"})
(defmethod endpoints :twitter [_]
{:auth_uri "https://twitter.com/i/oauth2/authorize"
:token_uri "https://api.twitter.com/2/oauth2/token"
+ :redirect_uri "http://localhost:8080/redirect"
:authorization_options {:code_challenge_method "plain"}})
(defn with-endpoints
diff --git a/test/happyapi/oauth2/capture_redirect_test.clj b/test/happyapi/oauth2/capture_redirect_test.clj
index 411ea8d..fc9dd9f 100644
--- a/test/happyapi/oauth2/capture_redirect_test.clj
+++ b/test/happyapi/oauth2/capture_redirect_test.clj
@@ -15,19 +15,23 @@
{:auth_uri "TEST"
:client_id "TEST"
:redirect_uri "http://localhost"}
- []
- {})))
+ [])))
(is (= {:access_token "TOKEN"}
(capture-redirect/fresh-credentials http/request
{:auth_uri "TEST"
:client_id "TEST"
:redirect_uri "http://localhost:8080/redirect"}
- []
- {})))
+ [])))
(is (thrown? Throwable
(capture-redirect/fresh-credentials http/request
{:auth_uri "TEST"
:client_id "TEST"
:redirect_uri "http://not.localhost"}
- []
- {})))))
+ [])))))
+
+(deftest make-redirect-handler-test
+ (let [p (promise)]
+
+ (capture-redirect/make-redirect-handler p)
+ ()
+ ))
diff --git a/test/happyapi/providers/twitter_test.clj b/test/happyapi/providers/twitter_test.clj
index c9a0cdc..c277d0e 100644
--- a/test/happyapi/providers/twitter_test.clj
+++ b/test/happyapi/providers/twitter_test.clj
@@ -1,20 +1,19 @@
(ns happyapi.providers.twitter-test
- (:require [clojure.edn :as edn]
- [clojure.test :refer :all]
+ (:require [clojure.test :refer :all]
[happyapi.providers.twitter :as twitter]))
(deftest api-request-test
- (twitter/setup! (assoc-in (edn/read-string (slurp "happyapi.edn"))
- [:twitter :redirect_uri]
- "http://localhost:8080/redirect"))
- (twitter/api-request {:method :get
- :url "https://api.twitter.com/2/users/me"
- :scopes ["tweet.read" "tweet.write" "users.read"]})
- (twitter/api-request {:method :delete
- :url "https://api.twitter.com/2/tweets/1811986925798195513"
- :scopes ["tweet.read" "tweet.write" "users.read"]})
+ (twitter/setup! nil)
+ (is (-> (twitter/api-request {:method :get
+ :url "https://api.twitter.com/2/users/me"
+ :scopes ["tweet.read" "tweet.write" "users.read"]})
+ :username))
+ (is (-> (twitter/api-request {:method :delete
+ :url "https://api.twitter.com/2/tweets/1811986925798195513"
+ :scopes ["tweet.read" "tweet.write" "users.read"]})
+ :deleted))
;; let's not post every time I run the tests...
#_(twitter/api-request {:method :post
- :url "https://api.twitter.com/2/tweets"
- :scopes ["tweet.read" "tweet.write" "users.read"]
- :body {:text "This is a test tweet from HappyAPI"}}))
+ :url "https://api.twitter.com/2/tweets"
+ :scopes ["tweet.read" "tweet.write" "users.read"]
+ :body {:text "This is a test tweet from HappyAPI"}}))