♻️ Refactor email validations & tokens service.

This commit is contained in:
Andrey Antukh 2020-09-21 16:01:49 +02:00 committed by Alonso Torres
parent dda6a96407
commit 7d9fdc34be
20 changed files with 369 additions and 398 deletions

View file

@ -25,6 +25,7 @@
:database-uri "postgresql://127.0.0.1/uxbox" :database-uri "postgresql://127.0.0.1/uxbox"
:database-username "uxbox" :database-username "uxbox"
:database-password "uxbox" :database-password "uxbox"
:secret-key "default"
:media-directory "resources/public/media" :media-directory "resources/public/media"
:assets-directory "resources/public/static" :assets-directory "resources/public/static"
@ -77,6 +78,7 @@
(s/def ::assets-directory ::us/string) (s/def ::assets-directory ::us/string)
(s/def ::media-uri ::us/string) (s/def ::media-uri ::us/string)
(s/def ::media-directory ::us/string) (s/def ::media-directory ::us/string)
(s/def ::secret-key ::us/string)
(s/def ::sendmail-backend ::us/string) (s/def ::sendmail-backend ::us/string)
(s/def ::sendmail-backend-apikey ::us/string) (s/def ::sendmail-backend-apikey ::us/string)
(s/def ::sendmail-reply-to ::us/email) (s/def ::sendmail-reply-to ::us/email)
@ -133,6 +135,7 @@
::assets-uri ::assets-uri
::media-directory ::media-directory
::media-uri ::media-uri
::secret-key
::sendmail-reply-to ::sendmail-reply-to
::sendmail-from ::sendmail-from
::sendmail-backend ::sendmail-backend

View file

@ -9,24 +9,23 @@
(ns app.http.auth.gitlab (ns app.http.auth.gitlab
(:require (:require
[clojure.data.json :as json]
[clojure.tools.logging :as log]
[lambdaisland.uri :as uri]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.config :as cfg] [app.config :as cfg]
[app.db :as db] [app.db :as db]
[app.services.tokens :as tokens]
[app.services.mutations :as sm]
[app.http.session :as session] [app.http.session :as session]
[app.util.http :as http])) [app.services.mutations :as sm]
[app.services.tokens :as tokens]
[app.util.http :as http]
[app.util.time :as dt]
[clojure.data.json :as json]
[clojure.tools.logging :as log]
[lambdaisland.uri :as uri]))
(def default-base-gitlab-uri "https://gitlab.com") (def default-base-gitlab-uri "https://gitlab.com")
(def scope "read_user") (def scope "read_user")
(defn- build-redirect-url (defn- build-redirect-url
[] []
(let [public (uri/uri (:public-uri cfg/config))] (let [public (uri/uri (:public-uri cfg/config))]
@ -100,10 +99,12 @@
(log/error "unexpected error on parsing response body from gitlab access token request" e) (log/error "unexpected error on parsing response body from gitlab access token request" e)
nil)))) nil))))
(defn auth (defn auth
[req] [req]
(let [token (tokens/create! db/pool {:type :gitlab-oauth}) (let [token (tokens/generate
{:iss :gitlab-oauth
:exp (dt/in-future "15m")})
params {:client_id (:gitlab-client-id cfg/config) params {:client_id (:gitlab-client-id cfg/config)
:redirect_uri (build-redirect-url) :redirect_uri (build-redirect-url)
:response_type "code" :response_type "code"
@ -115,31 +116,27 @@
{:status 200 {:status 200
:body {:redirect-uri (str uri)}})) :body {:redirect-uri (str uri)}}))
(defn callback (defn callback
[req] [req]
(let [token (get-in req [:params :state]) (let [token (get-in req [:params :state])
tdata (tokens/retrieve db/pool token) tdata (tokens/verify token {:iss :gitlab-oauth})
info (some-> (get-in req [:params :code]) info (some-> (get-in req [:params :code])
(get-access-token) (get-access-token)
(get-user-info))] (get-user-info))]
(when (not= :gitlab-oauth (:type tdata))
(ex/raise :type :validation
:code ::tokens/invalid-token))
(when-not info (when-not info
(ex/raise :type :authentication (ex/raise :type :authentication
:code ::unable-to-authenticate-with-gitlab)) :code :unable-to-authenticate-with-gitlab))
(let [profile (sm/handle {::sm/type :login-or-register (let [profile (sm/handle {::sm/type :login-or-register
:email (:email info) :email (:email info)
:fullname (:fullname info)}) :fullname (:fullname info)})
uagent (get-in req [:headers "user-agent"]) uagent (get-in req [:headers "user-agent"])
tdata {:type :authentication token (tokens/generate
:profile profile} {:iss :auth
token (tokens/create! db/pool tdata {:valid {:minutes 10}}) :exp (dt/in-future "15m")
:profile-id (:id profile)})
uri (-> (uri/uri (:public-uri cfg/config)) uri (-> (uri/uri (:public-uri cfg/config))
(assoc :path "/#/auth/verify-token") (assoc :path "/#/auth/verify-token")

View file

@ -9,16 +9,17 @@
(ns app.http.auth.google (ns app.http.auth.google
(:require (:require
[clojure.data.json :as json]
[clojure.tools.logging :as log]
[lambdaisland.uri :as uri]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.config :as cfg] [app.config :as cfg]
[app.db :as db] [app.db :as db]
[app.services.tokens :as tokens]
[app.services.mutations :as sm]
[app.http.session :as session] [app.http.session :as session]
[app.util.http :as http])) [app.services.mutations :as sm]
[app.services.tokens :as tokens]
[app.util.http :as http]
[app.util.time :as dt]
[clojure.data.json :as json]
[clojure.tools.logging :as log]
[lambdaisland.uri :as uri]))
(def base-goauth-uri "https://accounts.google.com/o/oauth2/v2/auth") (def base-goauth-uri "https://accounts.google.com/o/oauth2/v2/auth")
@ -84,7 +85,8 @@
(defn auth (defn auth
[req] [req]
(let [token (tokens/create! db/pool {:type :google-oauth}) (let [token (tokens/generate {:iss :google-oauth
:exp (dt/in-future "15m")})
params {:scope scope params {:scope scope
:access_type "offline" :access_type "offline"
:include_granted_scopes true :include_granted_scopes true
@ -102,28 +104,24 @@
(defn callback (defn callback
[req] [req]
(let [token (get-in req [:params :state]) (let [token (get-in req [:params :state])
tdata (tokens/retrieve db/pool token) tdata (tokens/verify token {:iss :google-oauth})
info (some-> (get-in req [:params :code]) info (some-> (get-in req [:params :code])
(get-access-token) (get-access-token)
(get-user-info))] (get-user-info))]
(when (not= :google-oauth (:type tdata))
(ex/raise :type :validation
:code ::tokens/invalid-token))
(when-not info (when-not info
(ex/raise :type :authentication (ex/raise :type :authentication
:code ::unable-to-authenticate-with-google)) :code :unable-to-authenticate-with-google))
(let [profile (sm/handle {::sm/type :login-or-register (let [profile (sm/handle {::sm/type :login-or-register
:email (:email info) :email (:email info)
:fullname (:fullname info)}) :fullname (:fullname info)})
uagent (get-in req [:headers "user-agent"]) uagent (get-in req [:headers "user-agent"])
tdata {:type :authentication token (tokens/generate
:profile profile} {:iss :auth
token (tokens/create! db/pool tdata {:valid {:minutes 10}}) :exp (dt/in-future "15m")
:profile-id (:id profile)})
uri (-> (uri/uri (:public-uri cfg/config)) uri (-> (uri/uri (:public-uri cfg/config))
(assoc :path "/#/auth/verify-token") (assoc :path "/#/auth/verify-token")
(assoc :query (uri/map->query-string {:token token}))) (assoc :query (uri/map->query-string {:token token})))
@ -133,4 +131,3 @@
:headers {"location" (str uri)} :headers {"location" (str uri)}
:cookies (session/cookies sid) :cookies (session/cookies sid)
:body ""}))) :body ""})))

View file

@ -51,7 +51,7 @@
(first))] (first))]
(when-not (client/bind? conn (:dn user-entry) password) (when-not (client/bind? conn (:dn user-entry) password)
(ex/raise :type :authentication (ex/raise :type :authentication
:code ::wrong-credentials)) :code :wrong-credentials))
(set/rename-keys user-entry {(keyword (:ldap-auth-avatar-attribute cfg/config)) :photo (set/rename-keys user-entry {(keyword (:ldap-auth-avatar-attribute cfg/config)) :photo
(keyword (:ldap-auth-fullname-attribute cfg/config)) :fullname (keyword (:ldap-auth-fullname-attribute cfg/config)) :fullname
(keyword (:ldap-auth-email-attribute cfg/config)) :email}))))) (keyword (:ldap-auth-email-attribute cfg/config)) :email})))))

View file

@ -10,7 +10,14 @@
(ns app.http.session (ns app.http.session
(:require (:require
[app.db :as db] [app.db :as db]
[app.services.tokens :as tokens])) [buddy.core.codecs :as bc]
[buddy.core.nonce :as bn]))
(defn next-token
[n]
(-> (bn/random-nonce n)
(bc/bytes->b64u)
(bc/bytes->str)))
(defn extract-auth-token (defn extract-auth-token
[request] [request]
@ -29,7 +36,7 @@
(defn create (defn create
[profile-id user-agent] [profile-id user-agent]
(let [id (tokens/next-token)] (let [id (next-token 64)]
(db/insert! db/pool :http-session {:id id (db/insert! db/pool :http-session {:id id
:profile-id profile-id :profile-id profile-id
:user-agent user-agent}) :user-agent user-agent})

View file

@ -86,6 +86,13 @@
{:name "0023-adapt-old-pages-and-files" {:name "0023-adapt-old-pages-and-files"
:fn mg0023/migrate} :fn mg0023/migrate}
{:name "0024-mod-profile-table"
:fn (mg/resource "app/migrations/sql/0024-mod-profile-table.sql")}
{:name "0025-del-generic-tokens-table"
:fn (mg/resource "app/migrations/sql/0025-del-generic-tokens-table.sql")}
]}) ]})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -0,0 +1,5 @@
ALTER TABLE profile ADD COLUMN is_active boolean NOT NULL DEFAULT false;
UPDATE profile SET is_active = true WHERE pending_email is null;
ALTER TABLE profile DROP COLUMN pending_email;

View file

@ -0,0 +1 @@
DROP TABLE generic_token;

View file

@ -28,7 +28,7 @@
sem (System/currentTimeMillis) sem (System/currentTimeMillis)
email (str "demo-" sem ".demo@nodomain.com") email (str "demo-" sem ".demo@nodomain.com")
fullname (str "Demo User " sem) fullname (str "Demo User " sem)
password (-> (bn/random-bytes 12) password (-> (bn/random-bytes 16)
(bc/bytes->b64u) (bc/bytes->b64u)
(bc/bytes->str)) (bc/bytes->str))
params {:id id params {:id id

View file

@ -5,7 +5,7 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0. ;; defined by the Mozilla Public License, v. 2.0.
;; ;;
;; Copyright (c) 2016-2020 Andrey Antukh <niwi@niwi.nz> ;; Copyright (c) 2020 UXBOX Labs SL
(ns app.services.mutations.profile (ns app.services.mutations.profile
(:require (:require
@ -35,7 +35,6 @@
[cuerdas.core :as str] [cuerdas.core :as str]
[datoteka.core :as fs])) [datoteka.core :as fs]))
;; --- Helpers & Specs ;; --- Helpers & Specs
(s/def ::email ::us/email) (s/def ::email ::us/email)
@ -70,22 +69,22 @@
[params] [params]
(when-not (:registration-enabled cfg/config) (when-not (:registration-enabled cfg/config)
(ex/raise :type :restriction (ex/raise :type :restriction
:code ::registration-disabled)) :code :registration-disabled))
(when-not (email-domain-in-whitelist? (:registration-domain-whitelist cfg/config) (when-not (email-domain-in-whitelist? (:registration-domain-whitelist cfg/config)
(:email params)) (:email params))
(ex/raise :type :validation (ex/raise :type :validation
:code ::email-domain-is-not-allowed)) :code :email-domain-is-not-allowed))
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(check-profile-existence! conn params) (check-profile-existence! conn params)
(let [profile (->> (create-profile conn params) (let [profile (->> (create-profile conn params)
(create-profile-relations conn)) (create-profile-relations conn))
payload {:type :verify-email token (tokens/generate
{:iss :verify-email
:exp (dt/in-future "48h")
:profile-id (:id profile) :profile-id (:id profile)
:email (:email profile)} :email (:email profile)})]
token (tokens/create! conn payload {:valid {:days 30}})]
(emails/send! conn emails/register (emails/send! conn emails/register
{:to (:email profile) {:to (:email profile)
@ -104,7 +103,7 @@
result (db/exec-one! conn [sql:profile-existence email])] result (db/exec-one! conn [sql:profile-existence email])]
(when (:val result) (when (:val result)
(ex/raise :type :validation (ex/raise :type :validation
:code ::email-already-exists)) :code :email-already-exists))
params)) params))
(defn- derive-password (defn- derive-password
@ -121,14 +120,15 @@
[conn {:keys [id fullname email password demo?] :as params}] [conn {:keys [id fullname email password demo?] :as params}]
(let [id (or id (uuid/next)) (let [id (or id (uuid/next))
demo? (if (boolean? demo?) demo? false) demo? (if (boolean? demo?) demo? false)
paswd (derive-password password)] active? (if demo? true false)
password (derive-password password)]
(db/insert! conn :profile (db/insert! conn :profile
{:id id {:id id
:fullname fullname :fullname fullname
:email (str/lower email) :email (str/lower email)
:pending-email (if demo? nil email)
:photo "" :photo ""
:password paswd :password password
:is-active active?
:is-demo demo?}))) :is-demo demo?})))
(defn- create-profile-relations (defn- create-profile-relations
@ -165,17 +165,21 @@
(letfn [(check-password [profile password] (letfn [(check-password [profile password]
(when (= (:password profile) "!") (when (= (:password profile) "!")
(ex/raise :type :validation (ex/raise :type :validation
:code ::account-without-password)) :code :account-without-password))
(:valid (verify-password password (:password profile)))) (:valid (verify-password password (:password profile))))
(validate-profile [profile] (validate-profile [profile]
(when-not (:is-active profile)
(ex/raise :type :validation
:code :wrong-credentials))
(when-not profile (when-not profile
(ex/raise :type :validation (ex/raise :type :validation
:code ::wrong-credentials)) :code :wrong-credentials))
(when-not (check-password profile password) (when-not (check-password profile password)
(ex/raise :type :validation (ex/raise :type :validation
:code ::wrong-credentials)) :code :wrong-credentials))
profile)] profile)]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(let [prof (-> (retrieve-profile-by-email conn email) (let [prof (-> (retrieve-profile-by-email conn email)
(validate-profile) (validate-profile)
@ -185,8 +189,8 @@
(def sql:profile-by-email (def sql:profile-by-email
"select * from profile "select * from profile
where email=? and deleted_at is null where email=?
for update") and deleted_at is null")
(defn- retrieve-profile-by-email (defn- retrieve-profile-by-email
[conn email] [conn email]
@ -207,7 +211,7 @@
{:id (uuid/next) {:id (uuid/next)
:fullname fullname :fullname fullname
:email (str/lower email) :email (str/lower email)
:pending-email nil :is-active true
:photo "" :photo ""
:password "!" :password "!"
:is-demo false})) :is-demo false}))
@ -251,7 +255,7 @@
(let [profile (profile/retrieve-profile-data conn profile-id)] (let [profile (profile/retrieve-profile-data conn profile-id)]
(when-not (:valid (verify-password old-password (:password profile))) (when-not (:valid (verify-password old-password (:password profile)))
(ex/raise :type :validation (ex/raise :type :validation
:code ::old-password-not-match)))) :code :old-password-not-match))))
(s/def ::update-profile-password (s/def ::update-profile-password
(s/keys :req-un [::profile-id ::password ::old-password])) (s/keys :req-un [::profile-id ::password ::old-password]))
@ -317,8 +321,6 @@
;; --- Mutation: Request Email Change ;; --- Mutation: Request Email Change
(declare select-profile-for-update)
(s/def ::request-email-change (s/def ::request-email-change
(s/keys :req-un [::email])) (s/keys :req-un [::email]))
@ -326,20 +328,16 @@
[{:keys [profile-id email] :as params}] [{:keys [profile-id email] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(let [email (str/lower email) (let [email (str/lower email)
profile (select-profile-for-update conn profile-id) profile (db/get-by-id conn :profile profile-id)
payload {:type :change-email token (tokens/generate
{:iss :change-email
:exp (dt/in-future "15m")
:profile-id profile-id :profile-id profile-id
:email email} :email email})]
token (tokens/create! conn payload)]
(when (not= email (:email profile)) (when (not= email (:email profile))
(check-profile-existence! conn params)) (check-profile-existence! conn params))
(db/update! conn :profile
{:pending-email email}
{:id profile-id})
(emails/send! conn emails/change-email (emails/send! conn emails/change-email
{:to (:email profile) {:to (:email profile)
:name (:fullname profile) :name (:fullname profile)
@ -357,65 +355,51 @@
;; Generic mutation for perform token based verification for auth ;; Generic mutation for perform token based verification for auth
;; domain. ;; domain.
(defmulti process-token (fn [conn claims] (:iss claims)))
(s/def ::verify-profile-token (s/def ::verify-profile-token
(s/keys :req-un [::token])) (s/keys :req-un [::token]))
(sm/defmutation ::verify-profile-token (sm/defmutation ::verify-profile-token
[{:keys [token] :as params}] [{:keys [token] :as params}]
(letfn [(handle-email-change [conn tdata]
(let [profile (select-profile-for-update conn (:profile-id tdata))]
(when (not= (:email tdata)
(:pending-email profile))
(ex/raise :type :validation
:code ::email-does-not-match))
(check-profile-existence! conn {:email (:pending-email profile)})
(db/update! conn :profile
{:pending-email nil
:email (:pending-email profile)}
{:id (:id profile)})
tdata))
(handle-email-verify [conn tdata]
(let [profile (select-profile-for-update conn (:profile-id tdata))]
(when (or (not= (:email profile)
(:pending-email profile))
(not= (:email profile)
(:email tdata)))
(ex/raise :type :validation
:code ::tokens/invalid-token))
(db/update! conn :profile
{:pending-email nil}
{:id (:id profile)})
tdata))]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(let [tdata (tokens/retrieve conn token {:delete true})] (let [claims (tokens/verify token)]
(tokens/delete! conn token) (process-token conn claims))))
(case (:type tdata)
:change-email (handle-email-change conn tdata)
:verify-email (handle-email-verify conn tdata)
:authentication tdata
(ex/raise :type :validation
:code ::tokens/invalid-token))))))
;; --- Mutation: Cancel Email Change (defmethod process-token :change-email
[conn {:keys [profile-id email] :as claims}]
(s/def ::cancel-email-change
(s/keys :req-un [::profile-id]))
(sm/defmutation ::cancel-email-change
[{:keys [profile-id] :as params}]
(db/with-atomic [conn db/pool]
(let [profile (select-profile-for-update conn profile-id)] (let [profile (select-profile-for-update conn profile-id)]
(when (= (:email profile) (check-profile-existence! conn {:email email})
(:pending-email profile)) (db/update! conn :profile
(ex/raise :type :validation {:email email}
:code ::unexpected-request)) {:id profile-id})
claims))
(defmethod process-token :verify-email
[conn {:keys [profile-id] :as claims}]
(let [profile (select-profile-for-update conn profile-id)]
(when (:is-active profile)
(ex/raise :type :validation
:code :email-already-validated))
(when (not= (:email profile)
(:email claims))
(ex/raise :type :validation
:code :invalid-token))
(db/update! conn :profile
{:is-active true}
{:id (:id profile)})
claims))
(defmethod process-token :auth
[conn claims]
claims)
(defmethod process-token :default
[conn claims]
(ex/raise :type :validation
:code :invalid-token))
(db/update! conn :profile {:pending-email nil} {:id profile-id})
nil)))
;; --- Mutation: Request Profile Recovery ;; --- Mutation: Request Profile Recovery
@ -425,9 +409,10 @@
(sm/defmutation ::request-profile-recovery (sm/defmutation ::request-profile-recovery
[{:keys [email] :as params}] [{:keys [email] :as params}]
(letfn [(create-recovery-token [conn {:keys [id] :as profile}] (letfn [(create-recovery-token [conn {:keys [id] :as profile}]
(let [payload {:type :password-recovery-token (let [token (tokens/generate
:profile-id id} {:iss :password-recovery
token (tokens/create! conn payload)] :exp (dt/in-future "15m")
:profile-id id})]
(assoc profile :token token))) (assoc profile :token token)))
(send-email-notification [conn profile] (send-email-notification [conn profile]
@ -453,23 +438,16 @@
(sm/defmutation ::recover-profile (sm/defmutation ::recover-profile
[{:keys [token password]}] [{:keys [token password]}]
(letfn [(validate-token [conn token] (letfn [(validate-token [conn token]
(let [tpayload (tokens/retrieve conn token)] (let [tdata (tokens/verify token {:iss :password-recovery})]
(when (not= (:type tpayload) :password-recovery-token) (:profile-id tdata)))
(ex/raise :type :validation
:code ::tokens/invalid-token))
(:profile-id tpayload)))
(update-password [conn profile-id] (update-password [conn profile-id]
(let [pwd (derive-password password)] (let [pwd (derive-password password)]
(db/update! conn :profile {:password pwd} {:id profile-id}))) (db/update! conn :profile {:password pwd} {:id profile-id})))]
(delete-token [conn token]
(db/delete! conn :generic-token {:token token}))]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(->> (validate-token conn token) (->> (validate-token conn token)
(update-password conn)) (update-password conn))
(delete-token conn token)
nil))) nil)))
@ -515,6 +493,6 @@
(let [rows (db/exec! conn [sql:teams-ownership-check profile-id])] (let [rows (db/exec! conn [sql:teams-ownership-check profile-id])]
(when-not (empty? rows) (when-not (empty? rows)
(ex/raise :type :validation (ex/raise :type :validation
:code ::owner-teams-with-people :code :owner-teams-with-people
:hint "The user need to transfer ownership of owned teams." :hint "The user need to transfer ownership of owned teams."
:context {:teams (mapv :team-id rows)})))) :context {:teams (mapv :team-id rows)}))))

View file

@ -9,70 +9,59 @@
(ns app.services.tokens (ns app.services.tokens
(:require (:require
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[buddy.core.codecs :as bc]
[buddy.core.nonce :as bn]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.spec :as us] [app.common.spec :as us]
[app.config :as cfg]
[app.db :as db]
[app.util.time :as dt] [app.util.time :as dt]
[app.db :as db])) [app.util.transit :as t]
[buddy.core.codecs :as bc]
[buddy.core.kdf :as bk]
[buddy.core.nonce :as bn]
[buddy.sign.jwe :as jwe]
[clojure.spec.alpha :as s]
[clojure.tools.logging :as log]))
(defn next-token (defn- derive-tokens-secret
([] (next-token 96)) [key]
([n] (when (= key "default")
(-> (bn/random-bytes n) (log/warn "Using default APP_SECRET_KEY, the system will generate insecure tokens."))
(bc/bytes->b64u) (let [engine (bk/engine {:key key
(bc/bytes->str)))) :salt "tokens"
:alg :hkdf
:digest :blake2b-512})]
(bk/get-bytes engine 32)))
(def default-duration (def secret
(dt/duration {:hours 48})) (delay (derive-tokens-secret (:secret-key cfg/config))))
(defn- decode-row (defn generate
[{:keys [content] :as row}] [claims]
(when row (let [payload (t/encode claims)]
(cond-> row (jwe/encrypt payload @secret {:alg :a256kw :enc :a256gcm})))
(db/pgobject? content)
(assoc :content (db/decode-transit-pgobject content)))))
(defn create! (defn verify
([conn payload] (create! conn payload {})) ([token] (verify token nil))
([conn payload {:keys [valid] :or {valid default-duration}}] ([token params]
(let [token (next-token) (let [payload (jwe/decrypt token @secret {:alg :a256kw :enc :a256gcm})
until (dt/plus (dt/now) (dt/duration valid))] claims (t/decode payload)]
(db/insert! conn :generic-token (when (and (dt/instant? (:exp claims))
{:content (db/tjson payload) (dt/is-before? (:exp claims) (dt/now)))
:token token
:valid-until until})
token)))
(defn delete!
[conn token]
(db/delete! conn :generic-token {:token token}))
(defn retrieve
([conn token] (retrieve conn token {}))
([conn token {:keys [delete] :or {delete false}}]
(let [row (->> (db/query conn :generic-token {:token token})
(map decode-row)
(first))]
(when-not row
(ex/raise :type :validation (ex/raise :type :validation
:code ::invalid-token)) :code :invalid-token
:reason :token-expired
;; Validate the token expiration :params params
(when (> (inst-ms (dt/now)) :claims claims))
(inst-ms (:valid-until row))) (when (and (contains? params :iss)
(not= (:iss claims)
(:iss params)))
(ex/raise :type :validation (ex/raise :type :validation
:code ::invalid-token)) :code :invalid-token
:reason :invalid-issuer
(when delete :claims claims
(db/delete! conn :generic-token {:token token})) :params params))
claims)))
(-> row
(dissoc :content)
(merge (:content row))))))

View file

@ -29,9 +29,17 @@
{:pre [(string? s)]} {:pre [(string? s)]}
(Instant/parse s)) (Instant/parse s))
(defn now (defn instant?
[] [v]
(Instant/now)) (instance? Instant v))
(defn is-after?
[da db]
(.isAfter ^Instant da ^Instant db))
(defn is-before?
[da db]
(.isBefore ^Instant da ^Instant db))
(defn plus (defn plus
[d ta] [d ta]
@ -65,6 +73,14 @@
:else :else
(obj->duration ms-or-obj))) (obj->duration ms-or-obj)))
(defn now
[]
(Instant/now))
(defn in-future
[v]
(plus (now) (duration v)))
(defn duration-between (defn duration-between
[t1 t2] [t1 t2]
(Duration/between t1 t2)) (Duration/between t1 t2))

View file

@ -1,6 +1,6 @@
{ {
"auth.already-have-account" : { "auth.already-have-account" : {
"used-in" : [ "src/app/main/ui/auth/register.cljs:106" ], "used-in" : [ "src/app/main/ui/auth/register.cljs:115" ],
"translations" : { "translations" : {
"en" : "Already have an account?", "en" : "Already have an account?",
"fr" : "Vous avez déjà un compte?", "fr" : "Vous avez déjà un compte?",
@ -18,7 +18,7 @@
} }
}, },
"auth.create-demo-profile" : { "auth.create-demo-profile" : {
"used-in" : [ "src/app/main/ui/auth/register.cljs:115", "src/app/main/ui/auth/login.cljs:135" ], "used-in" : [ "src/app/main/ui/auth/register.cljs:124", "src/app/main/ui/auth/login.cljs:135" ],
"translations" : { "translations" : {
"en" : "Create demo account", "en" : "Create demo account",
"fr" : "Créer un compte de démonstration", "fr" : "Créer un compte de démonstration",
@ -27,7 +27,7 @@
} }
}, },
"auth.create-demo-profile-label" : { "auth.create-demo-profile-label" : {
"used-in" : [ "src/app/main/ui/auth/register.cljs:112", "src/app/main/ui/auth/login.cljs:132" ], "used-in" : [ "src/app/main/ui/auth/register.cljs:121", "src/app/main/ui/auth/login.cljs:132" ],
"translations" : { "translations" : {
"en" : "Just wanna try it?", "en" : "Just wanna try it?",
"fr" : "Vous voulez juste essayer?", "fr" : "Vous voulez juste essayer?",
@ -36,7 +36,7 @@
} }
}, },
"auth.demo-warning" : { "auth.demo-warning" : {
"used-in" : [ "src/app/main/ui/auth/register.cljs:32" ], "used-in" : [ "src/app/main/ui/auth/register.cljs:33" ],
"translations" : { "translations" : {
"en" : "This is a DEMO service, DO NOT USE for real work, the projects will be periodicaly wiped.", "en" : "This is a DEMO service, DO NOT USE for real work, the projects will be periodicaly wiped.",
"fr" : "Il s'agit d'un service DEMO, NE PAS UTILISER pour un travail réel, les projets seront périodiquement supprimés.", "fr" : "Il s'agit d'un service DEMO, NE PAS UTILISER pour un travail réel, les projets seront périodiquement supprimés.",
@ -45,7 +45,7 @@
} }
}, },
"auth.email-label" : { "auth.email-label" : {
"used-in" : [ "src/app/main/ui/auth/register.cljs:81", "src/app/main/ui/auth/recovery_request.cljs:45", "src/app/main/ui/auth/login.cljs:81" ], "used-in" : [ "src/app/main/ui/auth/register.cljs:90", "src/app/main/ui/auth/recovery_request.cljs:45", "src/app/main/ui/auth/login.cljs:81" ],
"translations" : { "translations" : {
"en" : "Email", "en" : "Email",
"fr" : "Adresse email", "fr" : "Adresse email",
@ -63,7 +63,7 @@
} }
}, },
"auth.fullname-label" : { "auth.fullname-label" : {
"used-in" : [ "src/app/main/ui/auth/register.cljs:75" ], "used-in" : [ "src/app/main/ui/auth/register.cljs:84" ],
"translations" : { "translations" : {
"en" : "Full Name", "en" : "Full Name",
"fr" : "Nom complet", "fr" : "Nom complet",
@ -90,7 +90,7 @@
} }
}, },
"auth.login-here" : { "auth.login-here" : {
"used-in" : [ "src/app/main/ui/auth/register.cljs:109" ], "used-in" : [ "src/app/main/ui/auth/register.cljs:118" ],
"translations" : { "translations" : {
"en" : "Login here", "en" : "Login here",
"fr" : "Se connecter ici", "fr" : "Se connecter ici",
@ -179,8 +179,14 @@
"es" : "Hemos enviado a tu buzón un enlace para recuperar tu contraseña." "es" : "Hemos enviado a tu buzón un enlace para recuperar tu contraseña."
} }
}, },
"auth.notifications.validation-email-sent" : {
"used-in" : [ "src/app/main/ui/auth/register.cljs:59", "src/app/main/ui/settings/change_email.cljs:55" ],
"translations" : {
"en" : "Verification email sent to %s; check your email!"
}
},
"auth.password-label" : { "auth.password-label" : {
"used-in" : [ "src/app/main/ui/auth/register.cljs:85", "src/app/main/ui/auth/login.cljs:87" ], "used-in" : [ "src/app/main/ui/auth/register.cljs:94", "src/app/main/ui/auth/login.cljs:87" ],
"translations" : { "translations" : {
"en" : "Password", "en" : "Password",
"fr" : "Mot de passe", "fr" : "Mot de passe",
@ -189,7 +195,7 @@
} }
}, },
"auth.password-length-hint" : { "auth.password-length-hint" : {
"used-in" : [ "src/app/main/ui/auth/register.cljs:84" ], "used-in" : [ "src/app/main/ui/auth/register.cljs:93" ],
"translations" : { "translations" : {
"en" : "At least 8 characters", "en" : "At least 8 characters",
"fr" : "Au moins 8 caractères", "fr" : "Au moins 8 caractères",
@ -252,7 +258,7 @@
} }
}, },
"auth.register-submit-label" : { "auth.register-submit-label" : {
"used-in" : [ "src/app/main/ui/auth/register.cljs:89" ], "used-in" : [ "src/app/main/ui/auth/register.cljs:98" ],
"translations" : { "translations" : {
"en" : "Create an account", "en" : "Create an account",
"fr" : "Créer un compte", "fr" : "Créer un compte",
@ -261,7 +267,7 @@
} }
}, },
"auth.register-subtitle" : { "auth.register-subtitle" : {
"used-in" : [ "src/app/main/ui/auth/register.cljs:98" ], "used-in" : [ "src/app/main/ui/auth/register.cljs:107" ],
"translations" : { "translations" : {
"en" : "It's free, it's Open Source", "en" : "It's free, it's Open Source",
"fr" : "C'est gratuit, c'est Open Source", "fr" : "C'est gratuit, c'est Open Source",
@ -270,7 +276,7 @@
} }
}, },
"auth.register-title" : { "auth.register-title" : {
"used-in" : [ "src/app/main/ui/auth/register.cljs:97" ], "used-in" : [ "src/app/main/ui/auth/register.cljs:106" ],
"translations" : { "translations" : {
"en" : "Create an account", "en" : "Create an account",
"fr" : "Créer un compte", "fr" : "Créer un compte",
@ -729,7 +735,7 @@
} }
}, },
"errors.email-already-exists" : { "errors.email-already-exists" : {
"used-in" : [ "src/app/main/ui/auth.cljs:87", "src/app/main/ui/settings/change_email.cljs:47" ], "used-in" : [ "src/app/main/ui/auth.cljs:90", "src/app/main/ui/settings/change_email.cljs:46" ],
"translations" : { "translations" : {
"en" : "Email already used", "en" : "Email already used",
"fr" : "Adresse e-mail déjà utilisée", "fr" : "Adresse e-mail déjà utilisée",
@ -737,8 +743,17 @@
"es" : "Este correo ya está en uso" "es" : "Este correo ya está en uso"
} }
}, },
"errors.email-already-validated" : {
"used-in" : [ "src/app/main/ui/auth.cljs:95" ],
"translations" : {
"en" : "Email already validated.",
"fr" : "Adresse e-mail déjà validé.",
"ru" : "Электронная почта уже подтверждена.",
"es" : "Este correo ya está validado."
}
},
"errors.email-invalid-confirmation" : { "errors.email-invalid-confirmation" : {
"used-in" : [ "src/app/main/ui/settings/change_email.cljs:37" ], "used-in" : [ "src/app/main/ui/settings/change_email.cljs:36" ],
"translations" : { "translations" : {
"en" : "Confirmation email must match", "en" : "Confirmation email must match",
"fr" : "L'adresse e-mail de confirmation doit correspondre", "fr" : "L'adresse e-mail de confirmation doit correspondre",
@ -747,7 +762,7 @@
} }
}, },
"errors.generic" : { "errors.generic" : {
"used-in" : [ "src/app/main/ui/auth.cljs:91", "src/app/main/ui/settings/profile.cljs:36" ], "used-in" : [ "src/app/main/ui/auth.cljs:99", "src/app/main/ui/settings/profile.cljs:36" ],
"translations" : { "translations" : {
"en" : "Something wrong has happened.", "en" : "Something wrong has happened.",
"fr" : "Quelque chose c'est mal passé.", "fr" : "Quelque chose c'est mal passé.",
@ -819,7 +834,7 @@
} }
}, },
"errors.registration-disabled" : { "errors.registration-disabled" : {
"used-in" : [ "src/app/main/ui/auth/register.cljs:47" ], "used-in" : [ "src/app/main/ui/auth/register.cljs:49" ],
"translations" : { "translations" : {
"en" : "The registration is currently disabled.", "en" : "The registration is currently disabled.",
"fr" : "L'enregistrement est actuellement désactivé.", "fr" : "L'enregistrement est actuellement désactivé.",
@ -828,7 +843,7 @@
} }
}, },
"errors.unexpected-error" : { "errors.unexpected-error" : {
"used-in" : [ "src/app/main/data/media.cljs:64", "src/app/main/ui/workspace/sidebar/options/exports.cljs:66", "src/app/main/ui/auth/register.cljs:53", "src/app/main/ui/settings/change_email.cljs:51" ], "used-in" : [ "src/app/main/data/media.cljs:64", "src/app/main/ui/workspace/sidebar/options/exports.cljs:66", "src/app/main/ui/auth/register.cljs:55", "src/app/main/ui/settings/change_email.cljs:50" ],
"translations" : { "translations" : {
"en" : "An unexpected error occurred.", "en" : "An unexpected error occurred.",
"fr" : "Une erreur inattendue c'est produite", "fr" : "Une erreur inattendue c'est produite",
@ -918,7 +933,7 @@
} }
}, },
"settings.change-email-info" : { "settings.change-email-info" : {
"used-in" : [ "src/app/main/ui/settings/change_email.cljs:67" ], "used-in" : [ "src/app/main/ui/settings/change_email.cljs:72" ],
"translations" : { "translations" : {
"en" : "We'll send you an email to your current email “%s” to verify your identity.", "en" : "We'll send you an email to your current email “%s” to verify your identity.",
"fr" : "Nous vous enverrons un e-mail à votre adresse actuelle “%s” pour vérifier votre identité.", "fr" : "Nous vous enverrons un e-mail à votre adresse actuelle “%s” pour vérifier votre identité.",
@ -926,23 +941,14 @@
"es" : "Enviaremos un mensaje a tu correo actual “%s” para verificar tu identidad." "es" : "Enviaremos un mensaje a tu correo actual “%s” para verificar tu identidad."
} }
}, },
"settings.change-email-info2" : {
"used-in" : [ "src/app/main/ui/settings/change_email.cljs:94" ],
"translations" : {
"en" : "We have sent you an email to “%s”. Please follow the instructions to verify the email.",
"fr" : "Nous vous avons envoyé un e-mail à “%s”. Veuillez suivre les instructions pour vérifier l'e-mail.",
"ru" : "Мы отправили письмо на почту “%s”. Пожалуйста, следуйте инструкциям для подтверждения email адреса.",
"es" : "Te hemos enviado un mensaje a “%s”. Por favor sigue las instrucciones para verificar tu correo."
}
},
"settings.change-email-info3" : { "settings.change-email-info3" : {
"used-in" : [ "src/app/main/ui/settings/profile.cljs:78" ],
"translations" : { "translations" : {
"en" : "There is a pending change of your email to “%s”.", "en" : null,
"fr" : "Il y a un changement en attente de votre adresse e-mail “%s”.", "fr" : null,
"ru" : "Ваш email адреса будет сменен на “%s”.", "es" : null,
"es" : "Hay un cambio de correo pendiente a “%s”." "ru" : null
} },
"used-in" : [ "src/app/main/ui/settings/profile.cljs:78" ]
}, },
"settings.change-email-label" : { "settings.change-email-label" : {
"used-in" : [ "src/app/main/ui/settings/profile.cljs:73" ], "used-in" : [ "src/app/main/ui/settings/profile.cljs:73" ],
@ -954,7 +960,7 @@
} }
}, },
"settings.change-email-submit-label" : { "settings.change-email-submit-label" : {
"used-in" : [ "src/app/main/ui/settings/change_email.cljs:84" ], "used-in" : [ "src/app/main/ui/settings/change_email.cljs:89" ],
"translations" : { "translations" : {
"en" : "Change email", "en" : "Change email",
"fr" : "Changer adresse e-mail", "fr" : "Changer adresse e-mail",
@ -963,7 +969,7 @@
} }
}, },
"settings.change-email-title" : { "settings.change-email-title" : {
"used-in" : [ "src/app/main/ui/settings/change_email.cljs:63" ], "used-in" : [ "src/app/main/ui/settings/change_email.cljs:68" ],
"translations" : { "translations" : {
"en" : "Change your email", "en" : "Change your email",
"fr" : "Changer adresse e-mail", "fr" : "Changer adresse e-mail",
@ -971,17 +977,8 @@
"es" : "Cambiar tu correo" "es" : "Cambiar tu correo"
} }
}, },
"settings.close-modal-label" : {
"used-in" : [ "src/app/main/ui/settings/change_email.cljs:98" ],
"translations" : {
"en" : "Close",
"fr" : "Fermer",
"ru" : "Закрыть",
"es" : "Cerrar"
}
},
"settings.confirm-email-label" : { "settings.confirm-email-label" : {
"used-in" : [ "src/app/main/ui/settings/change_email.cljs:80" ], "used-in" : [ "src/app/main/ui/settings/change_email.cljs:85" ],
"translations" : { "translations" : {
"en" : "Verify new email", "en" : "Verify new email",
"fr" : "Vérifier la nouvelle adresse e-mail", "fr" : "Vérifier la nouvelle adresse e-mail",
@ -1071,7 +1068,7 @@
} }
}, },
"settings.new-email-label" : { "settings.new-email-label" : {
"used-in" : [ "src/app/main/ui/settings/change_email.cljs:75" ], "used-in" : [ "src/app/main/ui/settings/change_email.cljs:80" ],
"translations" : { "translations" : {
"en" : "New email", "en" : "New email",
"fr" : "Nouvel e-mail", "fr" : "Nouvel e-mail",
@ -1089,7 +1086,7 @@
} }
}, },
"settings.notifications.email-changed-successfully" : { "settings.notifications.email-changed-successfully" : {
"used-in" : [ "src/app/main/ui/auth.cljs:62" ], "used-in" : [ "src/app/main/ui/auth.cljs:63" ],
"translations" : { "translations" : {
"en" : "Your email address has been updated successfully", "en" : "Your email address has been updated successfully",
"fr" : "Votre adresse e-mail a été mise à jour avec succès", "fr" : "Votre adresse e-mail a été mise à jour avec succès",
@ -1098,16 +1095,16 @@
} }
}, },
"settings.notifications.email-not-verified" : { "settings.notifications.email-not-verified" : {
"used-in" : [ "src/app/main/ui/dashboard.cljs:100" ],
"translations" : { "translations" : {
"en" : "Your email address has not been verified yet. Please check your inbox at “%s” for a confirmation email.", "en" : null,
"fr" : "Votre adresse e-mail n'a pas encore été vérifiée. Veuillez vérifier votre boîte de réception à “%s” pour un e-mail de confirmation.", "fr" : null,
"ru" : "Ваш email адрес еще не подтвержден. Пожалуйста, проверьте наличие подтверждающего письма во входящих на “%s”.", "es" : null,
"es" : "Tu dirección de correo aún no ha sido verificada. Por favor, busca en tu correo “%s” un mensaje de confirmación." "ru" : null
} },
"used-in" : [ "src/app/main/ui/dashboard.cljs:100" ]
}, },
"settings.notifications.email-verified-successfully" : { "settings.notifications.email-verified-successfully" : {
"used-in" : [ "src/app/main/ui/auth.cljs:55" ], "used-in" : [ "src/app/main/ui/auth.cljs:57" ],
"translations" : { "translations" : {
"en" : "Your email address has been verified successfully", "en" : "Your email address has been verified successfully",
"fr" : "Votre adresse e-mail a été vérifiée avec succès", "fr" : "Votre adresse e-mail a été vérifiée avec succès",
@ -1125,7 +1122,7 @@
} }
}, },
"settings.notifications.profile-deletion-not-allowed" : { "settings.notifications.profile-deletion-not-allowed" : {
"used-in" : [ "src/app/main/data/auth.cljs:160" ], "used-in" : [ "src/app/main/data/auth.cljs:157" ],
"translations" : { "translations" : {
"en" : "You can't delete you profile. Reasign your teams before proceed.", "en" : "You can't delete you profile. Reasign your teams before proceed.",
"fr" : "Vous ne pouvez pas supprimer votre profil. Réassignez vos équipes avant de continuer.", "fr" : "Vous ne pouvez pas supprimer votre profil. Réassignez vos équipes avant de continuer.",
@ -1241,15 +1238,6 @@
"es" : "ACTUALIZAR" "es" : "ACTUALIZAR"
} }
}, },
"settings.verification-sent-title" : {
"used-in" : [ "src/app/main/ui/settings/change_email.cljs:89" ],
"translations" : {
"en" : "Verification email sent",
"fr" : "L'e-mail de vérification a été envoyé",
"ru" : "Письмо для подтверждения email адреса отправлено",
"es" : "Correo de verificación enviado"
}
},
"settings.yes-delete-my-account" : { "settings.yes-delete-my-account" : {
"used-in" : [ "src/app/main/ui/settings/delete_account.cljs:43" ], "used-in" : [ "src/app/main/ui/settings/delete_account.cljs:43" ],
"translations" : { "translations" : {
@ -1278,7 +1266,7 @@
} }
}, },
"viewer.header.dont-show-interactions" : { "viewer.header.dont-show-interactions" : {
"used-in" : [ "src/app/main/ui/viewer/header.cljs:67" ], "used-in" : [ "src/app/main/ui/viewer/header.cljs:68" ],
"translations" : { "translations" : {
"en" : "Don't show interactions", "en" : "Don't show interactions",
"fr" : "Ne pas afficher les interactions", "fr" : "Ne pas afficher les interactions",
@ -1287,7 +1275,7 @@
} }
}, },
"viewer.header.edit-page" : { "viewer.header.edit-page" : {
"used-in" : [ "src/app/main/ui/viewer/header.cljs:166" ], "used-in" : [ "src/app/main/ui/viewer/header.cljs:168" ],
"translations" : { "translations" : {
"en" : "Edit page", "en" : "Edit page",
"fr" : "Editer la page", "fr" : "Editer la page",
@ -1296,7 +1284,7 @@
} }
}, },
"viewer.header.fullscreen" : { "viewer.header.fullscreen" : {
"used-in" : [ "src/app/main/ui/viewer/header.cljs:177" ], "used-in" : [ "src/app/main/ui/viewer/header.cljs:179" ],
"translations" : { "translations" : {
"en" : "Full Screen", "en" : "Full Screen",
"fr" : "Plein écran", "fr" : "Plein écran",
@ -1305,7 +1293,7 @@
} }
}, },
"viewer.header.share.copy-link" : { "viewer.header.share.copy-link" : {
"used-in" : [ "src/app/main/ui/viewer/header.cljs:111" ], "used-in" : [ "src/app/main/ui/viewer/header.cljs:113" ],
"translations" : { "translations" : {
"en" : "Copy link", "en" : "Copy link",
"fr" : "Copier lien", "fr" : "Copier lien",
@ -1314,7 +1302,7 @@
} }
}, },
"viewer.header.share.create-link" : { "viewer.header.share.create-link" : {
"used-in" : [ "src/app/main/ui/viewer/header.cljs:120" ], "used-in" : [ "src/app/main/ui/viewer/header.cljs:122" ],
"translations" : { "translations" : {
"en" : "Create link", "en" : "Create link",
"fr" : "Créer lien", "fr" : "Créer lien",
@ -1323,7 +1311,7 @@
} }
}, },
"viewer.header.share.placeholder" : { "viewer.header.share.placeholder" : {
"used-in" : [ "src/app/main/ui/viewer/header.cljs:112" ], "used-in" : [ "src/app/main/ui/viewer/header.cljs:114" ],
"translations" : { "translations" : {
"en" : "Share link will appear here", "en" : "Share link will appear here",
"fr" : "Le lien de partage apparaîtra ici", "fr" : "Le lien de partage apparaîtra ici",
@ -1332,7 +1320,7 @@
} }
}, },
"viewer.header.share.remove-link" : { "viewer.header.share.remove-link" : {
"used-in" : [ "src/app/main/ui/viewer/header.cljs:118" ], "used-in" : [ "src/app/main/ui/viewer/header.cljs:120" ],
"translations" : { "translations" : {
"en" : "Remove link", "en" : "Remove link",
"fr" : "Supprimer le lien", "fr" : "Supprimer le lien",
@ -1341,7 +1329,7 @@
} }
}, },
"viewer.header.share.subtitle" : { "viewer.header.share.subtitle" : {
"used-in" : [ "src/app/main/ui/viewer/header.cljs:114" ], "used-in" : [ "src/app/main/ui/viewer/header.cljs:116" ],
"translations" : { "translations" : {
"en" : "Anyone with the link will have access", "en" : "Anyone with the link will have access",
"fr" : "Toute personne disposant du lien aura accès", "fr" : "Toute personne disposant du lien aura accès",
@ -1350,7 +1338,7 @@
} }
}, },
"viewer.header.share.title" : { "viewer.header.share.title" : {
"used-in" : [ "src/app/main/ui/viewer/header.cljs:97", "src/app/main/ui/viewer/header.cljs:99", "src/app/main/ui/viewer/header.cljs:105" ], "used-in" : [ "src/app/main/ui/viewer/header.cljs:99", "src/app/main/ui/viewer/header.cljs:101", "src/app/main/ui/viewer/header.cljs:107" ],
"translations" : { "translations" : {
"en" : "Share link", "en" : "Share link",
"fr" : "Lien de partage", "fr" : "Lien de partage",
@ -1359,7 +1347,7 @@
} }
}, },
"viewer.header.show-interactions" : { "viewer.header.show-interactions" : {
"used-in" : [ "src/app/main/ui/viewer/header.cljs:71" ], "used-in" : [ "src/app/main/ui/viewer/header.cljs:72" ],
"translations" : { "translations" : {
"en" : "Show interactions", "en" : "Show interactions",
"fr" : "Afficher les interactions", "fr" : "Afficher les interactions",
@ -1368,7 +1356,7 @@
} }
}, },
"viewer.header.show-interactions-on-click" : { "viewer.header.show-interactions-on-click" : {
"used-in" : [ "src/app/main/ui/viewer/header.cljs:75" ], "used-in" : [ "src/app/main/ui/viewer/header.cljs:76" ],
"translations" : { "translations" : {
"en" : "Show interactions on click", "en" : "Show interactions on click",
"fr" : "Afficher les interactions au clic", "fr" : "Afficher les interactions au clic",
@ -1377,7 +1365,7 @@
} }
}, },
"viewer.header.sitemap" : { "viewer.header.sitemap" : {
"used-in" : [ "src/app/main/ui/viewer/header.cljs:147" ], "used-in" : [ "src/app/main/ui/viewer/header.cljs:149" ],
"translations" : { "translations" : {
"en" : "Sitemap", "en" : "Sitemap",
"fr" : "Plan du site", "fr" : "Plan du site",
@ -1761,7 +1749,7 @@
} }
}, },
"workspace.libraries.colors" : { "workspace.libraries.colors" : {
"used-in" : [ "src/app/main/ui/workspace/libraries.cljs:62" ], "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:68" ],
"translations" : { "translations" : {
"en" : "%s colors", "en" : "%s colors",
"fr" : "", "fr" : "",
@ -1800,7 +1788,7 @@
} }
}, },
"workspace.libraries.file-library" : { "workspace.libraries.file-library" : {
"used-in" : [ "src/app/main/ui/workspace/libraries.cljs:69" ], "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:75" ],
"translations" : { "translations" : {
"en" : "File library", "en" : "File library",
"fr" : "", "fr" : "",
@ -1809,7 +1797,7 @@
} }
}, },
"workspace.libraries.graphics" : { "workspace.libraries.graphics" : {
"used-in" : [ "src/app/main/ui/workspace/libraries.cljs:59" ], "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:65" ],
"translations" : { "translations" : {
"en" : "%s graphics", "en" : "%s graphics",
"fr" : "", "fr" : "",
@ -1818,7 +1806,7 @@
} }
}, },
"workspace.libraries.in-this-file" : { "workspace.libraries.in-this-file" : {
"used-in" : [ "src/app/main/ui/workspace/libraries.cljs:66" ], "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:72" ],
"translations" : { "translations" : {
"en" : "LIBRARIES IN THIS FILE", "en" : "LIBRARIES IN THIS FILE",
"fr" : "", "fr" : "",
@ -1854,7 +1842,7 @@
} }
}, },
"workspace.libraries.remove" : { "workspace.libraries.remove" : {
"used-in" : [ "src/app/main/ui/workspace/libraries.cljs:80" ], "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:82" ],
"translations" : { "translations" : {
"en" : "Remove", "en" : "Remove",
"fr" : "", "fr" : "",
@ -1863,7 +1851,7 @@
} }
}, },
"workspace.libraries.search-shared-libraries" : { "workspace.libraries.search-shared-libraries" : {
"used-in" : [ "src/app/main/ui/workspace/libraries.cljs:87" ], "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:89" ],
"translations" : { "translations" : {
"en" : "Search shared libraries", "en" : "Search shared libraries",
"fr" : "", "fr" : "",
@ -1872,7 +1860,7 @@
} }
}, },
"workspace.libraries.shared-libraries" : { "workspace.libraries.shared-libraries" : {
"used-in" : [ "src/app/main/ui/workspace/libraries.cljs:84" ], "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:86" ],
"translations" : { "translations" : {
"en" : "SHARED LIBRARIES", "en" : "SHARED LIBRARIES",
"fr" : "", "fr" : "",

View file

@ -146,10 +146,7 @@
on-success identity}} (meta data)] on-success identity}} (meta data)]
(->> (rp/mutation :register-profile data) (->> (rp/mutation :register-profile data)
(rx/tap on-success) (rx/tap on-success)
(rx/map #(login data)) (rx/catch on-error))))))
(rx/catch (fn [err]
(on-error err)
(rx/empty))))))))
;; --- Request Account Deletion ;; --- Request Account Deletion

View file

@ -114,10 +114,7 @@
on-success identity}} (meta data)] on-success identity}} (meta data)]
(->> (rp/mutation :request-email-change data) (->> (rp/mutation :request-email-change data)
(rx/tap on-success) (rx/tap on-success)
(rx/map (constantly fetch-profile)) (rx/catch on-error))))))
(rx/catch (fn [err]
(on-error err)
(rx/empty))))))))
;; --- Cancel Email Change ;; --- Cancel Email Change

View file

@ -9,23 +9,23 @@
(ns app.main.ui.auth (ns app.main.ui.auth
(:require (:require
[cljs.spec.alpha :as s]
[beicon.core :as rx]
[rumext.alpha :as mf]
[app.main.ui.icons :as i]
[app.main.data.auth :as da] [app.main.data.auth :as da]
[app.main.data.users :as du]
[app.main.data.messages :as dm] [app.main.data.messages :as dm]
[app.main.data.users :as du]
[app.main.repo :as rp]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.auth.login :refer [login-page]] [app.main.ui.auth.login :refer [login-page]]
[app.main.ui.auth.recovery :refer [recovery-page]] [app.main.ui.auth.recovery :refer [recovery-page]]
[app.main.ui.auth.recovery-request :refer [recovery-request-page]] [app.main.ui.auth.recovery-request :refer [recovery-request-page]]
[app.main.ui.auth.register :refer [register-page]] [app.main.ui.auth.register :refer [register-page]]
[app.main.repo :as rp] [app.main.ui.icons :as i]
[app.util.timers :as ts]
[app.util.forms :as fm] [app.util.forms :as fm]
[app.util.i18n :as i18n :refer [tr t]] [app.util.i18n :as i18n :refer [tr t]]
[app.util.router :as rt])) [app.util.router :as rt]
[app.util.timers :as ts]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[rumext.alpha :as mf]))
(mf/defc goodbye-page (mf/defc goodbye-page
[{:keys [locale] :as props}] [{:keys [locale] :as props}]
@ -50,24 +50,30 @@
:auth-recovery [:& recovery-page {:locale locale :auth-recovery [:& recovery-page {:locale locale
:params (:query-params route)}])]])) :params (:query-params route)}])]]))
(defn- handle-email-verified (defmulti handle-token (fn [token] (:iss token)))
(defmethod handle-token :verify-email
[data] [data]
(let [msg (tr "settings.notifications.email-verified-successfully")] (let [msg (tr "settings.notifications.email-verified-successfully")]
(ts/schedule 100 #(st/emit! (dm/success msg))) (ts/schedule 100 #(st/emit! (dm/success msg)))
(st/emit! (rt/nav :settings-profile) (st/emit! (rt/nav :auth-login))))
du/fetch-profile)))
(defn- handle-email-changed (defmethod handle-token :change-email
[data] [data]
(let [msg (tr "settings.notifications.email-changed-successfully")] (let [msg (tr "settings.notifications.email-changed-successfully")]
(ts/schedule 100 #(st/emit! (dm/success msg))) (ts/schedule 100 #(st/emit! (dm/success msg)))
(st/emit! (rt/nav :settings-profile) (st/emit! (rt/nav :settings-profile)
du/fetch-profile))) du/fetch-profile)))
(defn- handle-authentication (defmethod handle-token :auth
[tdata] [tdata]
(st/emit! (da/login-from-token tdata))) (st/emit! (da/login-from-token tdata)))
(defmethod handle-token :default
[tdata]
(js/console.log "Unhandled token:" (pr-str tdata))
(st/emit! (rt/nav :auth-login)))
(mf/defc verify-token (mf/defc verify-token
[{:keys [route] :as props}] [{:keys [route] :as props}]
(let [token (get-in route [:query-params :token])] (let [token (get-in route [:query-params :token])]
@ -76,21 +82,22 @@
(->> (rp/mutation :verify-profile-token {:token token}) (->> (rp/mutation :verify-profile-token {:token token})
(rx/subs (rx/subs
(fn [tdata] (fn [tdata]
(case (:type tdata) (handle-token tdata))
:verify-email (handle-email-verified tdata)
:change-email (handle-email-changed tdata)
:authentication (handle-authentication tdata)
nil))
(fn [error] (fn [error]
(case (:code error) (case (:code error)
:app.services.mutations.profile/email-already-exists :email-already-exists
(let [msg (tr "errors.email-already-exists")] (let [msg (tr "errors.email-already-exists")]
(ts/schedule 100 #(st/emit! (dm/error msg))) (ts/schedule 100 #(st/emit! (dm/error msg)))
(st/emit! (rt/nav :settings-profile))) (st/emit! (rt/nav :auth-login)))
:email-already-validated
(let [msg (tr "errors.email-already-validated")]
(ts/schedule 100 #(st/emit! (dm/warn msg)))
(st/emit! (rt/nav :auth-login)))
(let [msg (tr "errors.generic")] (let [msg (tr "errors.generic")]
(ts/schedule 100 #(st/emit! (dm/error msg))) (ts/schedule 100 #(st/emit! (dm/error msg)))
(st/emit! (rt/nav :settings-profile))))))))) (st/emit! (rt/nav :auth-login)))))))))
[:div.verify-token [:div.verify-token
i/loader-pencil])) i/loader-pencil]))

View file

@ -9,21 +9,22 @@
(ns app.main.ui.auth.register (ns app.main.ui.auth.register
(:require (:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[rumext.alpha :as mf]
[app.config :as cfg] [app.config :as cfg]
[app.main.ui.icons :as i]
[app.main.data.auth :as uda]
[app.main.store :as st]
[app.main.data.auth :as da] [app.main.data.auth :as da]
[app.main.data.auth :as uda]
[app.main.data.messages :as dm]
[app.main.store :as st]
[app.main.ui.components.forms :refer [input submit-button form]] [app.main.ui.components.forms :refer [input submit-button form]]
[app.main.ui.icons :as i]
[app.main.ui.messages :as msgs] [app.main.ui.messages :as msgs]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.forms :as fm] [app.util.forms :as fm]
[app.util.i18n :refer [tr t]] [app.util.i18n :refer [tr t]]
[app.util.router :as rt])) [app.util.router :as rt]
[app.util.timers :as tm]
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[rumext.alpha :as mf]))
(mf/defc demo-warning (mf/defc demo-warning
[_] [_]
@ -43,14 +44,20 @@
(defn- on-error (defn- on-error
[form error] [form error]
(case (:code error) (case (:code error)
:app.services.mutations.profile/registration-disabled :registration-disabled
(st/emit! (tr "errors.registration-disabled")) (st/emit! (dm/error (tr "errors.registration-disabled")))
:app.services.mutations.profile/email-already-exists :email-already-exists
(swap! form assoc-in [:errors :email] (swap! form assoc-in [:errors :email]
{:message "errors.email-already-exists"}) {:message "errors.email-already-exists"})
(st/emit! (tr "errors.unexpected-error")))) (st/emit! (dm/error (tr "errors.unexpected-error")))))
(defn- on-success
[form data]
(let [msg (tr "auth.notifications.validation-email-sent" (:email data))]
(st/emit! (rt/nav :auth-login)
(dm/success msg))))
(defn- validate (defn- validate
[data] [data]
@ -61,7 +68,8 @@
(defn- on-submit (defn- on-submit
[form event] [form event]
(let [data (with-meta (:clean-data form) (let [data (with-meta (:clean-data form)
{:on-error (partial on-error form)})] {:on-error (partial on-error form)
:on-success (partial on-success form)})]
(st/emit! (uda/register data)))) (st/emit! (uda/register data))))
(mf/defc register-form (mf/defc register-form

View file

@ -51,17 +51,11 @@
(= "drafts" project-id) (= "drafts" project-id)
(assoc :project-id (:default-project-id profile))))) (assoc :project-id (:default-project-id profile)))))
(declare global-notifications)
(mf/defc dashboard (mf/defc dashboard
[{:keys [route] :as props}] [{:keys [route] :as props}]
(let [profile (mf/deref refs/profile) (let [profile (mf/deref refs/profile)
page (get-in route [:data :name]) page (get-in route [:data :name])
{:keys [search-term team-id project-id] :as params} {:keys [search-term team-id project-id] :as params} (parse-params route profile)]
(parse-params route profile)]
[:*
[:& global-notifications {:profile profile}]
[:section.dashboard-layout [:section.dashboard-layout
[:div.main-logo [:div.main-logo
[:a {:on-click #(st/emit! (rt/nav :dashboard-team {:team-id team-id}))} [:a {:on-click #(st/emit! (rt/nav :dashboard-team {:team-id team-id}))}
@ -84,18 +78,6 @@
:dashboard-project :dashboard-project
[:& project-page {:team-id team-id [:& project-page {:team-id team-id
:project-id project-id}])]]])) :project-id project-id}])]]))
(mf/defc global-notifications
[{:keys [profile] :as props}]
(let [locale (mf/deref i18n/locale)]
(when (and profile
(not= uuid/zero (:id profile))
(= (:pending-email profile)
(:email profile)))
[:section.banner.error.quick
[:div.content
[:div.icon i/msg-warning]
[:span (t locale "settings.notifications.email-not-verified" (:email profile))]]])))

View file

@ -23,9 +23,9 @@
(defonce components (atom {})) (defonce components (atom {}))
(defn show! (defn show!
([type props] [type props]
(let [id (random-uuid)] (let [id (random-uuid)]
(st/emit! (mdm/show-modal id type props))))) (st/emit! (mdm/show-modal id type props))))
(defn allow-click-outside! [] (defn allow-click-outside! []
(st/emit! (mdm/update-modal {:allow-click-outside true}))) (st/emit! (mdm/update-modal {:allow-click-outside true})))
@ -37,6 +37,8 @@
[] []
(st/emit! (mdm/hide-modal))) (st/emit! (mdm/hide-modal)))
(def hide (mdm/hide-modal))
(defn- on-esc-clicked (defn- on-esc-clicked
[event] [event]
(when (k/esc? event) (when (k/esc? event)

View file

@ -9,9 +9,7 @@
(ns app.main.ui.settings.change-email (ns app.main.ui.settings.change-email
(:require (:require
[cljs.spec.alpha :as s] [app.common.spec :as us]
[cuerdas.core :as str]
[rumext.alpha :as mf]
[app.main.data.auth :as da] [app.main.data.auth :as da]
[app.main.data.messages :as dm] [app.main.data.messages :as dm]
[app.main.data.users :as du] [app.main.data.users :as du]
@ -21,12 +19,13 @@
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.messages :as msgs] [app.main.ui.messages :as msgs]
[app.main.ui.modal :as modal] [app.main.ui.modal :as modal]
[app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr t]]
[app.util.forms :as fm] [cljs.spec.alpha :as s]
[app.util.i18n :as i18n :refer [tr t]])) [cuerdas.core :as str]
[rumext.alpha :as mf]))
(s/def ::email-1 ::fm/email) (s/def ::email-1 ::us/email)
(s/def ::email-2 ::fm/email) (s/def ::email-2 ::us/email)
(defn- email-equality (defn- email-equality
[data] [data]
@ -42,7 +41,7 @@
(defn- on-error (defn- on-error
[form error] [form error]
(cond (cond
(= (:code error) :app.services.mutations.profile/email-already-exists) (= (:code error) :email-already-exists)
(swap! form (fn [data] (swap! form (fn [data]
(let [error {:message (tr "errors.email-already-exists")}] (let [error {:message (tr "errors.email-already-exists")}]
(assoc-in data [:errors :email-1] error)))) (assoc-in data [:errors :email-1] error))))
@ -51,10 +50,16 @@
(let [msg (tr "errors.unexpected-error")] (let [msg (tr "errors.unexpected-error")]
(st/emit! (dm/error msg))))) (st/emit! (dm/error msg)))))
(defn- on-success
[profile data]
(let [msg (tr "auth.notifications.validation-email-sent" (:email profile))]
(st/emit! (dm/info msg) modal/hide)))
(defn- on-submit (defn- on-submit
[form event] [profile form event]
(let [data (with-meta {:email (get-in form [:clean-data :email-1])} (let [data (with-meta {:email (get-in form [:clean-data :email-1])}
{:on-error (partial on-error form)})] {:on-error (partial on-error form)
:on-success (partial on-success profile)})]
(st/emit! (du/request-email-change data)))) (st/emit! (du/request-email-change data))))
(mf/defc change-email-form (mf/defc change-email-form
@ -66,7 +71,7 @@
{:type :info {:type :info
:content (t locale "settings.change-email-info" (:email profile))}] :content (t locale "settings.change-email-info" (:email profile))}]
[:& form {:on-submit on-submit [:& form {:on-submit (partial on-submit profile)
:spec ::email-change-form :spec ::email-change-form
:validators [email-equality] :validators [email-equality]
:initial {}} :initial {}}
@ -83,20 +88,6 @@
[:& submit-button [:& submit-button
{:label (t locale "settings.change-email-submit-label")}]]]) {:label (t locale "settings.change-email-submit-label")}]]])
(mf/defc change-email-confirmation
[{:keys [locale profile] :as locale}]
[:section.modal-content.generic-form.confirmation
[:h2 (t locale "settings.verification-sent-title")]
[:& msgs/inline-banner
{:type :info
:content (t locale "settings.change-email-info2" (:email profile))}]
[:button.btn-primary.btn-large
{:on-click #(modal/hide!)}
(t locale "settings.close-modal-label")]])
(mf/defc change-email-modal (mf/defc change-email-modal
{::mf/register modal/components {::mf/register modal/components
::mf/register-as :change-email} ::mf/register-as :change-email}
@ -106,6 +97,5 @@
[:div.modal-overlay [:div.modal-overlay
[:div.generic-modal.change-email-modal [:div.generic-modal.change-email-modal
[:span.close {:on-click #(modal/hide!)} i/close] [:span.close {:on-click #(modal/hide!)} i/close]
(if (:pending-email profile) [:& change-email-form {:locale locale :profile profile}]]]))
[:& change-email-confirmation {:locale locale :profile profile}]
[:& change-email-form {:locale locale :profile profile}])]]))