♻️ 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
:profile-id (:id profile) {:iss :verify-email
:email (:email profile)} :exp (dt/in-future "48h")
:profile-id (:id profile)
token (tokens/create! conn payload {:valid {:days 30}})] :email (:email profile)})]
(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
@ -119,16 +118,17 @@
"Create the profile entry on the database with limited input "Create the profile entry on the database with limited input
filling all the other fields with defaults." filling all the other fields with defaults."
[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
:profile-id profile-id {:iss :change-email
:email email} :exp (dt/in-future "15m")
:profile-id profile-id
token (tokens/create! conn payload)] :email email})]
(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]
(let [tdata (tokens/retrieve conn token {:delete true})]
(tokens/delete! conn token)
(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
(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] (db/with-atomic [conn db/pool]
(let [profile (select-profile-for-update conn profile-id)] (let [claims (tokens/verify token)]
(when (= (:email profile) (process-token conn claims))))
(:pending-email profile))
(ex/raise :type :validation (defmethod process-token :change-email
:code ::unexpected-request)) [conn {:keys [profile-id email] :as claims}]
(let [profile (select-profile-for-update conn profile-id)]
(check-profile-existence! conn {:email email})
(db/update! conn :profile
{:email email}
{: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,51 +51,33 @@
(= "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)] [:section.dashboard-layout
[:* [:div.main-logo
[:& global-notifications {:profile profile}] [:a {:on-click #(st/emit! (rt/nav :dashboard-team {:team-id team-id}))}
[:section.dashboard-layout i/logo-icon]]
[:div.main-logo [:& profile-section {:profile profile}]
[:a {:on-click #(st/emit! (rt/nav :dashboard-team {:team-id team-id}))} [:& sidebar {:team-id team-id
i/logo-icon]] :project-id project-id
[:& profile-section {:profile profile}] :section page
[:& sidebar {:team-id team-id :search-term search-term}]
:project-id project-id [:div.dashboard-content
:section page (case page
:search-term search-term}] :dashboard-search
[:div.dashboard-content [:& search-page {:team-id team-id :search-term search-term}]
(case page
:dashboard-search
[:& search-page {:team-id team-id :search-term search-term}]
:dashboard-team :dashboard-team
[:& recent-files-page {:team-id team-id}] [:& recent-files-page {:team-id team-id}]
:dashboard-libraries :dashboard-libraries
[:& libraries-page {:team-id team-id}] [:& libraries-page {:team-id team-id}]
: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,29 +88,14 @@
[:& 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}
[props] [props]
(let [locale (mf/deref i18n/locale) (let [locale (mf/deref i18n/locale)
profile (mf/deref refs/profile)] profile (mf/deref refs/profile)]
[: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}])]]))