user.cljs 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. (ns frontend.handler.user
  2. "Provides user related handler fns like login and logout"
  3. (:require-macros [frontend.handler.user])
  4. (:require [frontend.config :as config]
  5. [frontend.handler.config :as config-handler]
  6. [frontend.state :as state]
  7. [frontend.debug :as debug]
  8. [clojure.string :as string]
  9. [cljs-time.core :as t]
  10. [cljs-time.coerce :as tc]
  11. [cljs-http.client :as http]
  12. [cljs.core.async :as async :refer [go <!]]
  13. [goog.crypt.Sha256]
  14. [goog.crypt.Hmac]
  15. [goog.crypt :as crypt]
  16. [promesa.core :as p]
  17. [frontend.handler.notification :as notification]))
  18. (defn set-preferred-format!
  19. [format]
  20. (when format
  21. (config-handler/set-config! :preferred-format format)
  22. (state/set-preferred-format! format)))
  23. (defn set-preferred-workflow!
  24. [workflow]
  25. (when workflow
  26. (config-handler/set-config! :preferred-workflow workflow)
  27. (state/set-preferred-workflow! workflow)))
  28. ;;; userinfo, token, login/logout, ...
  29. (defn- decode-username
  30. [username]
  31. (let [arr (new js/Uint8Array (count username))]
  32. (doseq [i (range (count username))]
  33. (aset arr i (.charCodeAt username i)))
  34. (.decode (new js/TextDecoder "utf-8") arr)))
  35. (defn- parse-jwt [jwt]
  36. (some-> jwt
  37. (string/split ".")
  38. second
  39. (#(.decodeString ^js crypt/base64 % true))
  40. js/JSON.parse
  41. (js->clj :keywordize-keys true)
  42. (update :cognito:username decode-username)))
  43. (defn- expired? [parsed-jwt]
  44. (some->
  45. (* 1000 (:exp parsed-jwt))
  46. tc/from-long
  47. (t/before? (t/now))))
  48. (defn- almost-expired?
  49. "return true when jwt will expire after 1h"
  50. [parsed-jwt]
  51. (some->
  52. (* 1000 (:exp parsed-jwt))
  53. tc/from-long
  54. (t/before? (-> 1 t/hours t/from-now))))
  55. (defn- almost-expired-or-expired?
  56. [parsed-jwt]
  57. (or (almost-expired? parsed-jwt)
  58. (expired? parsed-jwt)))
  59. (defn get-auth-session []
  60. (try
  61. (some->
  62. (state/get-auth-id-token)
  63. parse-jwt)
  64. (catch js/Error e (js/console.error e))))
  65. (defn username []
  66. (:cognito:username (get-auth-session)))
  67. (defn email []
  68. (:email (get-auth-session)))
  69. (defn user-uuid []
  70. (:sub (get-auth-session)))
  71. (defn logged-in? []
  72. (some? (state/get-auth-refresh-token)))
  73. (defn- set-token-to-localstorage!
  74. ([id-token access-token]
  75. (prn :debug "set-token-to-localstorage!")
  76. (js/localStorage.setItem "id-token" id-token)
  77. (js/localStorage.setItem "access-token" access-token))
  78. ([id-token access-token refresh-token]
  79. (prn :debug "set-token-to-localstorage!")
  80. (js/localStorage.setItem "id-token" id-token)
  81. (js/localStorage.setItem "access-token" access-token)
  82. (js/localStorage.setItem "refresh-token" refresh-token)))
  83. (defn- clear-tokens
  84. ([]
  85. (state/set-auth-id-token nil)
  86. (state/set-auth-access-token nil)
  87. (state/set-auth-refresh-token nil)
  88. (set-token-to-localstorage! "" "" ""))
  89. ([except-refresh-token?]
  90. (state/set-auth-id-token nil)
  91. (state/set-auth-access-token nil)
  92. (when-not except-refresh-token?
  93. (state/set-auth-refresh-token nil))
  94. (if except-refresh-token?
  95. (set-token-to-localstorage! "" "")
  96. (set-token-to-localstorage! "" "" ""))))
  97. (defn- set-tokens!
  98. ([id-token access-token]
  99. (state/set-auth-id-token id-token)
  100. (state/set-auth-access-token access-token)
  101. (set-token-to-localstorage! id-token access-token))
  102. ([id-token access-token refresh-token]
  103. (state/set-auth-id-token id-token)
  104. (state/set-auth-access-token access-token)
  105. (state/set-auth-refresh-token refresh-token)
  106. (set-token-to-localstorage! id-token access-token refresh-token)))
  107. (defn- <refresh-tokens
  108. "return refreshed id-token, access-token"
  109. [refresh-token]
  110. (http/post (str "https://" config/OAUTH-DOMAIN "/oauth2/token")
  111. {:form-params {:grant_type "refresh_token"
  112. :client_id config/COGNITO-CLIENT-ID
  113. :refresh_token refresh-token}}))
  114. (defn <refresh-id-token&access-token
  115. "Refresh id-token and access-token"
  116. []
  117. (go
  118. (when-let [refresh-token (state/get-auth-refresh-token)]
  119. (let [resp (<! (<refresh-tokens refresh-token))]
  120. (cond
  121. (and (<= 400 (:status resp))
  122. (> 500 (:status resp)))
  123. ;; invalid refresh-token
  124. (clear-tokens)
  125. ;; e.g. api return 500, server internal error
  126. ;; we shouldn't clear tokens if they aren't expired yet
  127. ;; the `refresh-tokens-loop` will retry soon
  128. (and (not (http/unexceptional-status? (:status resp)))
  129. (not (-> (state/get-auth-id-token) parse-jwt expired?)))
  130. nil ; do nothing
  131. (not (http/unexceptional-status? (:status resp)))
  132. (notification/show! "exceptional status when refresh-token" :warning true)
  133. :else ; ok
  134. (when (and (:id_token (:body resp)) (:access_token (:body resp)))
  135. (set-tokens! (:id_token (:body resp)) (:access_token (:body resp)))))))))
  136. (defn restore-tokens-from-localstorage
  137. "Refresh id-token&access-token, pull latest repos, returns nil when tokens are not available."
  138. []
  139. (println "restore-tokens-from-localstorage")
  140. (let [refresh-token (js/localStorage.getItem "refresh-token")]
  141. (when refresh-token
  142. (go
  143. (<! (<refresh-id-token&access-token))
  144. ;; refresh remote graph list by pub login event
  145. (when (user-uuid) (state/pub-event! [:user/fetch-info-and-graphs]))))))
  146. (defn has-refresh-token?
  147. "Has refresh-token"
  148. []
  149. (boolean (js/localStorage.getItem "refresh-token")))
  150. (defn login-callback
  151. [session]
  152. (set-tokens!
  153. (:jwtToken (:idToken session))
  154. (:jwtToken (:accessToken session))
  155. (:token (:refreshToken session)))
  156. (notification/show! (str "Hi, " (username) " :)") :success)
  157. (state/pub-event! [:user/fetch-info-and-graphs]))
  158. (defn ^:export login-with-username-password-e2e
  159. [username password client-id client-secret]
  160. (let [text-encoder (new js/TextEncoder)
  161. key (.encode text-encoder client-secret)
  162. hasher (new crypt/Sha256)
  163. hmacer (new crypt/Hmac hasher key)
  164. secret-hash (.encodeByteArray ^js crypt/base64 (.getHmac hmacer (str username client-id)))
  165. payload {"AuthParameters" {"USERNAME" username,
  166. "PASSWORD" password,
  167. "SECRET_HASH" secret-hash}
  168. "AuthFlow" "USER_PASSWORD_AUTH",
  169. "ClientId" client-id}
  170. headers {"X-Amz-Target" "AWSCognitoIdentityProviderService.InitiateAuth",
  171. "Content-Type" "application/x-amz-json-1.1"}]
  172. (go
  173. (let [resp (<! (http/post config/COGNITO-IDP {:headers headers
  174. :body (js/JSON.stringify (clj->js payload))}))]
  175. (assert (= 200 (:status resp)))
  176. (let [body (js->clj (js/JSON.parse (:body resp)))
  177. access-token (get-in body ["AuthenticationResult" "AccessToken"])
  178. id-token (get-in body ["AuthenticationResult" "IdToken"])
  179. refresh-token (get-in body ["AuthenticationResult" "RefreshToken"])]
  180. (set-tokens! id-token access-token refresh-token)
  181. (state/pub-event! [:user/fetch-info-and-graphs])
  182. {:id-token id-token :access-token access-token :refresh-token refresh-token})))))
  183. (defn logout! []
  184. (state/set-state! [:ui/loading? :logging-out?] true)
  185. (-> (state/pub-event! [:user/logout])
  186. (p/then (fn []
  187. (clear-tokens)
  188. (state/clear-user-info!)))
  189. (p/finally #(state/set-state! [:ui/loading? :logging-out?] false))))
  190. (defn upgrade []
  191. (let [base-upgrade-url "https://logseqdemo.lemonsqueezy.com/checkout/buy/13e194b5-c927-41a8-af58-ed1a36d6000d"
  192. user-uuid (user-uuid)
  193. url (cond-> base-upgrade-url
  194. user-uuid (str "?checkout[custom][user_uuid]=" (name user-uuid)))]
  195. (println " ~~~ LEMON: " url " ~~~ ")
  196. (js/window.open url)))
  197. ; (js/window.open
  198. ; "https://logseqdemo.lemonsqueezy.com/checkout/buy/13e194b5-c927-41a8-af58-ed1a36d6000d"))
  199. (defn <ensure-id&access-token
  200. []
  201. (go
  202. (when (or (nil? (state/get-auth-id-token))
  203. (-> (state/get-auth-id-token) parse-jwt almost-expired-or-expired?))
  204. (debug/pprint (str "refresh tokens... " (tc/to-string (t/now))))
  205. (<! (<refresh-id-token&access-token))
  206. (when (or (nil? (state/get-auth-id-token))
  207. (-> (state/get-auth-id-token) parse-jwt expired?))
  208. (ex-info "empty or expired token and refresh failed" {:anom :expired-token})))))
  209. (defn <user-uuid
  210. []
  211. (go
  212. (if-some [exp (<! (<ensure-id&access-token))]
  213. exp
  214. (user-uuid))))
  215. ;;; user groups
  216. (defn alpha-user?
  217. []
  218. (or config/dev?
  219. (contains? (state/user-groups) "alpha-tester")))
  220. (defn beta-user?
  221. []
  222. (or config/dev?
  223. (contains? (state/user-groups) "beta-tester")))
  224. (defn alpha-or-beta-user?
  225. []
  226. (or (alpha-user?) (beta-user?)))
  227. (comment
  228. ;; We probably need this for some new features later
  229. (defonce feature-matrix {:file-sync :beta})
  230. (defn feature-available?
  231. [feature]
  232. (or config/dev?
  233. (when (logged-in?)
  234. (case (feature feature-matrix)
  235. :beta (alpha-or-beta-user?)
  236. :alpha (alpha-user?)
  237. false)))))