Skip to content

Commit 14bc7cc

Browse files
committed
feat(http): refactor API calling layer
This commit adds attempting to refresh the access token on 401s. If a request comes in unauthorized, we attempt to use the refresh token to retrieve a new access token (via calling the /api/auth/refresh endpoint) and retry once on success.
1 parent d102f68 commit 14bc7cc

File tree

5 files changed

+121
-60
lines changed

5 files changed

+121
-60
lines changed

src/main/parts/frontend/api/core.cljs

+21-46
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,30 @@
11
(ns parts.frontend.api.core
2+
"High level API functions that should be used to interact with the backend."
23
(:require [cljs.core.async :refer [<! go]]
3-
[cljs-http.client :as http]
4-
[parts.frontend.api.utils :as utils]))
4+
[parts.frontend.api.utils :as utils]
5+
[parts.frontend.api.http :as http]))
56

6-
(defn add-auth-header [req]
7-
(if-let [header (utils/get-auth-header)]
8-
(assoc-in req [:headers "Authorization"] header)
9-
req))
10-
11-
(defn GET [endpoint params]
12-
(go (<! (http/get (str "/api" endpoint)
13-
(-> {:query-params params
14-
:accept :transit+json}
15-
add-auth-header)))))
16-
17-
(defn POST [endpoint data]
18-
(go (<! (http/post (str "/api" endpoint)
19-
(-> {:transit-params data
20-
:accept :transit+json}
21-
add-auth-header)))))
22-
23-
(defn PUT [endpoint data]
24-
(go (<! (http/put (str "/api" endpoint)
25-
(-> {:transit-params data
26-
:accept :transit+json}
27-
add-auth-header)))))
28-
29-
(defn PATCH [endpoint data]
30-
(go (<! (http/patch (str "/api" endpoint)
31-
(-> {:transit-params data
32-
:accept :transit+json}
33-
add-auth-header)))))
34-
35-
(defn DELETE [endpoint]
36-
(go (<! (http/delete (str "/api" endpoint)
37-
(-> {:accept :transit+json}
38-
add-auth-header)))))
39-
40-
;; Auth
41-
42-
(defn login [credentials]
7+
;; Authentication-related functions
8+
(defn login
9+
"CREDENTIALS should be a map containing the keys :email and :password."
10+
[credentials]
4311
(go
44-
(let [response (<! (POST "/auth/login" credentials))]
12+
(let [response (<! (http/POST "/auth/login" credentials {:skip-auth true}))]
4513
(when (= 200 (:status response))
4614
(utils/save-tokens (:body response)))
4715
response)))
4816

4917
(defn logout []
50-
(js/localStorage.removeItem "auth-token"))
51-
52-
;; Account
53-
54-
(defn get-current-user []
55-
(GET "/account" {}))
18+
(utils/clear-tokens))
19+
20+
;; Account-related functions
21+
(defn get-current-user
22+
"Retrieve the information about the currently signed in user:
23+
24+
- id
25+
- email
26+
- username
27+
- display name
28+
- role"
29+
[]
30+
(http/GET "/account" {}))

src/main/parts/frontend/api/http.cljs

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
(ns parts.frontend.api.http
2+
"Low level functions for interacting with the backend."
3+
(:require
4+
[cljs-http.client :as http-client]
5+
[cljs.core.async :refer [<! go]]
6+
[parts.frontend.api.utils :as utils]))
7+
8+
(defn- add-auth-header
9+
"Add the Authorization header to REQ if OPTS includes :auth-header; if not
10+
leave REQ untouched."
11+
[req opts]
12+
(if-let [header (:auth-header opts)]
13+
(assoc-in req [:headers "Authorization"] header)
14+
req))
15+
16+
;; These functions wrap the cljs-http requests.
17+
;; They should not be used directly.
18+
(defn- raw-GET [endpoint params & [opts]]
19+
(go (<! (http-client/get (str "/api" endpoint)
20+
(-> {:query-params params
21+
:accept :transit+json}
22+
(add-auth-header opts))))))
23+
24+
(defn- raw-POST [endpoint data & [opts]]
25+
(go (<! (http-client/post (str "/api" endpoint)
26+
(-> {:transit-params data
27+
:accept :transit+json}
28+
(add-auth-header opts))))))
29+
30+
(defn- raw-PUT [endpoint data & [opts]]
31+
(go (<! (http-client/put (str "/api" endpoint)
32+
(-> {:transit-params data
33+
:accept :transit+json}
34+
(add-auth-header opts))))))
35+
36+
(defn- raw-PATCH [endpoint data & [opts]]
37+
(go (<! (http-client/patch (str "/api" endpoint)
38+
(-> {:transit-params data
39+
:accept :transit+json}
40+
(add-auth-header opts))))))
41+
42+
(defn- raw-DELETE [endpoint & [opts]]
43+
(go (<! (http-client/delete (str "/api" endpoint)
44+
(-> {:accept :transit+json}
45+
(add-auth-header opts))))))
46+
47+
(defn wrap-auth
48+
"Transparently handles auth token lifecycle for HTTP requests.
49+
50+
Wraps HTTP handlers with JWT auth machinery that:
51+
1. Augments outbound reqs with auth headers (when token exists)
52+
2. Intercepts 401s and attempts token refresh
53+
3. Retries failed reqs with fresh credentials
54+
55+
OPTIONS:
56+
:skip-auth - bypasses ALL auth logic (both header injection and refresh)"
57+
[handler]
58+
(fn [endpoint params & [opts]]
59+
(go
60+
(let [skip-auth? (:skip-auth opts)
61+
tokens (utils/get-tokens)
62+
auth-header (utils/auth-header tokens)
63+
auth-opts (if (and (not skip-auth?) auth-header)
64+
(assoc opts :auth-header auth-header)
65+
opts)
66+
resp (<! (handler endpoint params auth-opts))]
67+
(if (and (= 401 (:status resp))
68+
(not skip-auth?)
69+
(:refresh_token tokens))
70+
(let [refresh-resp (<! (raw-POST "/auth/refresh"
71+
{:refresh_token (:refresh_token tokens)}))]
72+
(if (= 200 (:status refresh-resp))
73+
(do
74+
(utils/save-tokens (:body refresh-resp))
75+
(let [new-tokens (utils/get-tokens)
76+
new-auth-header (utils/auth-header new-tokens)
77+
new-params (assoc-in params [:headers "Authorization"] new-auth-header)]
78+
(<! (handler endpoint new-params))))
79+
resp))
80+
resp)))))
81+
82+
;; Wrapping the raw HTTP handlers in the auth middleware
83+
;; Pass {:skip-auth true} to ignore authorization flows.
84+
(def GET (wrap-auth raw-GET))
85+
(def POST (wrap-auth raw-POST))
86+
(def PUT (wrap-auth raw-PUT))
87+
(def PATCH (wrap-auth raw-PATCH))
88+
(def DELETE (wrap-auth raw-DELETE))

src/main/parts/frontend/api/utils.cljs

+6-9
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
(ns parts.frontend.api.utils
2-
(:require [cognitect.transit :as transit]))
1+
(ns parts.frontend.api.utils)
32

43
(def ^:private token-storage-key "parts-auth-tokens")
5-
(def ^:private user-email-key "parts-user-email")
64

75
(defn save-tokens
86
"Save authentication tokens to local storage"
@@ -18,13 +16,12 @@
1816
(defn clear-tokens
1917
"Clear authentication tokens from local storage"
2018
[]
21-
(.removeItem js/localStorage token-storage-key)
22-
(.removeItem js/localStorage user-email-key))
19+
(.removeItem js/localStorage token-storage-key))
2320

24-
(defn get-auth-header
25-
"Get the Authorization header for authenticated requests"
26-
[]
27-
(when-let [tokens (get-tokens)]
21+
(defn auth-header
22+
"Get the Authorization header for authenticated requests from tokens"
23+
[tokens]
24+
(when (and (:token_type tokens) (:access_token tokens))
2825
(str (:token_type tokens) " " (:access_token tokens))))
2926

3027
(defn get-csrf-token

src/main/parts/frontend/components/system.cljs

+2-3
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,8 @@
6464
($ :span "(...) "))
6565
(if user
6666
($ :span
67-
($ :span {:class "user-email"} (str "🟢 " (:email user)))
68-
($ :button {:on-click (fn []
69-
(.then (logout) (fn [_] nil)))}
67+
($ :span {:class "user-email"} (str "🟢 " (:username user)))
68+
($ :button {:on-click (fn [] (logout))}
7069
"Log out"))
7170
($ :span
7271
($ :span "🔴")

src/main/parts/frontend/context.cljs

+4-2
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,11 @@
4343
(println "[auth-provider] checking for token")
4444
(if (utils/get-tokens)
4545
(do
46-
(println "[auth-provider] token found, already signed in")
46+
(println "[auth-provider] token found")
4747
(fetch-user!))
48-
(set-loading false)))
48+
(do
49+
(println "[auth-provider] no token")
50+
(set-loading false))))
4951
[fetch-user!])
5052

5153
($ auth-context.Provider {:value value}

0 commit comments

Comments
 (0)