From c0677d25e831440199653912367660d700657336 Mon Sep 17 00:00:00 2001 From: Timothy Pratley Date: Sun, 14 Jul 2024 12:38:40 -0700 Subject: [PATCH] add favicon to help find tabs to close --- HappyAPI.svg | 2 +- README.md | 10 ++++--- deps.edn | 2 +- docs/favicon.ico | 1 + docs/index.md | 1 + resources/favicon.ico | Bin 0 -> 15406 bytes src/happyapi/middleware.clj | 13 +++++++-- src/happyapi/oauth2/capture_redirect.clj | 27 +++++++++++------- src/happyapi/oauth2/client.clj | 9 ++++-- .../happyapi/oauth2/capture_redirect_test.clj | 16 +++++++---- test/happyapi/providers/twitter_test.clj | 27 +++++++++--------- 11 files changed, 66 insertions(+), 42 deletions(-) create mode 120000 docs/favicon.ico create mode 120000 docs/index.md create mode 100644 resources/favicon.ico 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 0000000000000000000000000000000000000000..538d0a95800832a04c4818cb9eecf6e6653dc30c GIT binary patch literal 15406 zcmeHOeQ;FO6<_-?w(2-KP94X;=nU;R{Na!UzXqxaq>_LkyF|z$V#x+5WlH_PFRYf$ z3epNHibf1y0+w1-U>B1}_y|HsAlW1ai6q!*I|IamU{O#B!k(Vr-FIK^`^tN}8~vwd z=FPqD?Y;N>?z!il^SkH0_)@n=5Y4cvyiaF673E?oa|w#>LJD;k=a9Sd#EiiJ*PM?-YeyALJCw^4T>POp}< zP?pmmInhvF*5=R=^hsXR=;rFjsGy;p#yU;kqs=Sb2cPZU3)jEZ_hJF<(ut=-YCU zMpwou_kq7t?)`6yYxK*_mj0}hi_k|_w5$%eqHx<>MpuoQK*8V&f z=rpP4X@vO#bM6DlzJdMU*!IzN30=gg8?vEiJjZ;28FJNjfu>#>`9zFHFYTMh6E$&a zes)#@>npKCSLhGSZv7h<{eikFf&|-;ZJ}rAO3oK@amn9h8QlLheH6I|{ZBy$^0~PL zW?bgye`M^CeW)T>hc&=jnA_kU^Ht}JXC?6jy|;9p=-(9%&V&uX7VP?)pP}#ab?cIS zD}qOQ!bO*%KWvdP>@v5P_n0p*>vAiC=ejB;=Ap0r!x#dGG8e27^sMPhmuv1T$3O{v z2%WNRV11z{=(#Hr+=}^1%xa%Z>hLpz_{!xA{wb5c!Ea2ylJ`Ri`=3{`qh-xvZ^S~0 zq(2GKuq_W>474WVtn1|^Z8XW>PH=G<|7C418^SVIv79qfmR&ibuF`CaX9KOYGVlSh z*G>IzNd2rUc%DmH)_FU5>a$^6W>eAr9INiSebsD$d=o6%Et0n8=KuJ6O!cD#=`>0tS?r*D|Px>n@4`K3}G zFZsYZP9=42o%gLZVH(re#d4DIL!DO|_2W)@(AHavk;zzj$M!WmS7(18;FtuT8ff;e z53GgR9)z8nYz?yQ=Iq~nmI_yXNTVWKDR<6W$$0qS2Kx1)-QIDqO{%t_?J<4s(7Acu zm}9|;4$6IqWoq%)Toy%-d9{aLxi{fk*uD(=JGtH(H{sY2XzZo@1=}6v3LLBh#A&ZK z0sjCU2*1YmEo~lluIe50wv#>bscIViYAY44Jw%1Cchb1`KJ_eH!*~{cl6gv{l#LR5 zOr1l)a@M(`@4QDg(%83|M{vwj+oe;7uV(qps(kEkIhKF-m%QE^bor2N$Ju22luM2m zNuHI|Km3~NTVUtv+{L`v9^B*vyy_;Nh_mhMaST3H9b4=%P4`%DSDmw5@XWk9Z&UvK zZRFqcm0>(N_Kf$B(OXqZsO8TyFRHqSKbLLbTg3j>Kr~MlWl#^eNwS zoZ6n5COM04_lRR^EP-EBxi;+FNv?eEMgF?gRl00&AaDo7Z*vUAzRTr3E!V$rb*HO* ztdE=i_bz^*Pktnr5WK~~rr=YhZ%Ji;JK5jeEf?s({HcBNng4+^N&T5i=m0*|*01qg zp>JHScB##8=B^{EY_69!!v~nt^b_5k8mzh<)8UDJo!o?pLr_x z?{ynP{T;rgfcG~pCqE!F6HZJYtxnCh=noDrxbTGiT zV1Hwe!Ol&#CiQ=`Gdy9iS*H0OdhoIqh$Zmn@Tn%hCgtfR=Iuqc!~%QJY|?gLAcm@O zOgm5O^7MPQHLQ1kI+rr-O7K+8KNx%z;@YRSH%v-=6 zbom^|xPA)$ygO3*3jCV%Endd{e{O?338!d%Q~Am?Pr^f3{sz~~zVxQ*sn-qNG<|6H z=CU&8dsTe3a9*{Cxwp%-W)p37%g}uZds1l`jQ$8k^ei&SG1&+9`UzQ&Wt$y_*;bSyK(K5 z%Nfh`oM?HwhS|WeB~JTt8r`|4NyV?$k;^&4*NO=p^z(t91MFm2^f$wEVrBK5cfVkd zukV~{{k@!qZ+)JMTinmUfj0V#=P@VHLD%|-F3EH*oU6Y+Cl-n@rW00N{b82pz8N&4 z?qwR=g#5VYb8Rp0j~brK9hu<2apRP08S@6ddc9+xj`uHgYi$MR;MP##-h=#};H+vZ zUh4_o8$BFjLh0;pV9$z`SG(aJ7Mn%4R6jwtZ(Bj5_cYVE=2O}^+q16S=JQ2KobJ!9 zr=NIT3)qaNcl``%o(ImPzp7itn41LV6l}u{ul4shox~ixtONF{X77$Rb4$&W{O+lP z?ZQ3>!0T?skomJOU`uXm06)XJw%w~avETAutYqA0yyIhzKc4dy;zB*MdGv@q?ckT- z-*aN+^=4baJ+SBQSkKtM>>mST^zS{#XXpRWn7{pl3Ritd1y${QeL#h)57J$mj*zGK zeD?F?oC5nL_*R=g#>uhC-oGdH-amGK1E2f&ec-5hwUqbRCdxxTR(U6idfua(Xv`bk zydG(<1!CPCGkkCY5m%LbsayxxdvQNDgBv;*EqU4iR%zB+|sNl7B@9z_w)`9sFI3Mf?99Zy3lkMbiY=VEc z(+BiEiu0~IE;DYn{gHE{VlU)BvxCI-bREB^I6!{3H@h)YeAWxzikOh(?}7sxYzJ`_ z@zgwSpz$2r%pQBa<9*G-oiygJhiTll&-fkEHzI$99F}_?E9JNm#rYv?^t~IiCLBc z`#R2Je>b=#E`B#ockx+-&wOV5)2+4>Ct<%5vtF{zK=0o%b}2WmHtD#vJ)6(=p0+>4z5u^SbKOe#JZ*Q@27fo(J~)BJcG%BgKcpkSsOE(5OL-5! zBT9_#K2CzBG<{ALu?E_>Y!Nj*JKL6L;_P9zeefB<2^3!m9|=EWZtHemyyLX+uj*Q2 zZHw#s=Z3%E`A>>fE;M`}W44`~*bH%QGGO5K1s5~$8J@?ec{TdCONRDNaYuU&E~jD_ zb^eaa{2i7$=Hz{6=$+S|qeGENGLI)pBmQ!6UbUS0m~VkYnq}rM*pZfBPY3^F?{(0@ zr>7XOBTv|ef11b>B+uHWSi*a3An_;dCuY!xu%BJd_OsUfu0YGJA}=2Kllk^fEQUfH{t97{tgZd9yvf<%vA?v4WCkHWU>{1IM{+1 z%=m4;N3_g~Z3S@*@e1!?Ex#*v0~?3U;X=K?wr;=8+tOsk&i3D35v&3S;{M2`W+X%H z58Fm;khV5*ZcNy_+Qw@g-(ap^S!u=3?hmVEru>`Q?kXJO>u zZSkZrCvY*rRhbir@4K1X2!590SaWOJsQsC83x1BfY&)gnfnEL~Cn56hDax>&Gp~W4 zxfu8iE za-@uv$b1$W^o4FH(`6Nn7|C>{Vxb7vJaf@*E-` z&n^@`#{s%{EMfw`GvNJjG80_=^+jAoY=VD>k96=e>K@q877kwDA8z$GytBc355_P+ z>yRMuNN}-vTN(u?KwPzpO~O{xZzsZ&syo5~+q}7o*<8muXtsp?kn*0!nGfeCoISwb z!GUdgq>9y8hs|tA2fI2K>}ky}dGxIb!dV;VRmZ((#HrdzvL4~R%J!a$&8V0Sb@(Or zt%wg^VjOZD$P<{)+7`R8nFIVg|L;tF*HaURd)cc7oE~lvrv=x*JAa2*hdpWfA0VrK z73|J$MXCR#8_*#IR9`a&V{H6RY)TAE%pQ;wtaTV*j4E!A&Sn|kpaeUwJ Kdkv(kf&T#~m$!ic literal 0 HcmV?d00001 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"}}))