From 7d9fdc34beba31c5ed4399958f494c952f359a20 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 21 Sep 2020 16:01:49 +0200 Subject: [PATCH] :recycle: Refactor email validations & tokens service. --- backend/src/app/config.clj | 3 + backend/src/app/http/auth/gitlab.clj | 37 ++-- backend/src/app/http/auth/google.clj | 33 ++-- backend/src/app/http/auth/ldap.clj | 2 +- backend/src/app/http/session.clj | 11 +- backend/src/app/migrations.clj | 7 + .../migrations/sql/0024-mod-profile-table.sql | 5 + .../sql/0025-del-generic-tokens-table.sql | 1 + backend/src/app/services/mutations/demo.clj | 2 +- .../src/app/services/mutations/profile.clj | 178 ++++++++---------- backend/src/app/services/tokens.clj | 99 +++++----- backend/src/app/util/time.clj | 22 ++- frontend/resources/locales.json | 154 +++++++-------- frontend/src/app/main/data/auth.cljs | 5 +- frontend/src/app/main/data/users.cljs | 5 +- frontend/src/app/main/ui/auth.cljs | 49 ++--- frontend/src/app/main/ui/auth/register.cljs | 34 ++-- frontend/src/app/main/ui/dashboard.cljs | 62 +++--- frontend/src/app/main/ui/modal.cljs | 8 +- .../app/main/ui/settings/change_email.cljs | 50 ++--- 20 files changed, 369 insertions(+), 398 deletions(-) create mode 100644 backend/src/app/migrations/sql/0024-mod-profile-table.sql create mode 100644 backend/src/app/migrations/sql/0025-del-generic-tokens-table.sql diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index 29c6b3cdf..68dcd5bc3 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -25,6 +25,7 @@ :database-uri "postgresql://127.0.0.1/uxbox" :database-username "uxbox" :database-password "uxbox" + :secret-key "default" :media-directory "resources/public/media" :assets-directory "resources/public/static" @@ -77,6 +78,7 @@ (s/def ::assets-directory ::us/string) (s/def ::media-uri ::us/string) (s/def ::media-directory ::us/string) +(s/def ::secret-key ::us/string) (s/def ::sendmail-backend ::us/string) (s/def ::sendmail-backend-apikey ::us/string) (s/def ::sendmail-reply-to ::us/email) @@ -133,6 +135,7 @@ ::assets-uri ::media-directory ::media-uri + ::secret-key ::sendmail-reply-to ::sendmail-from ::sendmail-backend diff --git a/backend/src/app/http/auth/gitlab.clj b/backend/src/app/http/auth/gitlab.clj index 7f26fa2f0..dade85309 100644 --- a/backend/src/app/http/auth/gitlab.clj +++ b/backend/src/app/http/auth/gitlab.clj @@ -9,24 +9,23 @@ (ns app.http.auth.gitlab (:require - [clojure.data.json :as json] - [clojure.tools.logging :as log] - [lambdaisland.uri :as uri] [app.common.exceptions :as ex] [app.config :as cfg] [app.db :as db] - [app.services.tokens :as tokens] - [app.services.mutations :as sm] [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 scope "read_user") - (defn- build-redirect-url [] (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) nil)))) - (defn auth [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) :redirect_uri (build-redirect-url) :response_type "code" @@ -115,31 +116,27 @@ {:status 200 :body {:redirect-uri (str uri)}})) - (defn callback [req] (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]) (get-access-token) (get-user-info))] - (when (not= :gitlab-oauth (:type tdata)) - (ex/raise :type :validation - :code ::tokens/invalid-token)) - (when-not info (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 :email (:email info) :fullname (:fullname info)}) uagent (get-in req [:headers "user-agent"]) - tdata {:type :authentication - :profile profile} - token (tokens/create! db/pool tdata {:valid {:minutes 10}}) + token (tokens/generate + {:iss :auth + :exp (dt/in-future "15m") + :profile-id (:id profile)}) uri (-> (uri/uri (:public-uri cfg/config)) (assoc :path "/#/auth/verify-token") diff --git a/backend/src/app/http/auth/google.clj b/backend/src/app/http/auth/google.clj index 67841dc0f..97b24a1b9 100644 --- a/backend/src/app/http/auth/google.clj +++ b/backend/src/app/http/auth/google.clj @@ -9,16 +9,17 @@ (ns app.http.auth.google (:require - [clojure.data.json :as json] - [clojure.tools.logging :as log] - [lambdaisland.uri :as uri] [app.common.exceptions :as ex] [app.config :as cfg] [app.db :as db] - [app.services.tokens :as tokens] - [app.services.mutations :as sm] [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") @@ -84,7 +85,8 @@ (defn auth [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 :access_type "offline" :include_granted_scopes true @@ -102,28 +104,24 @@ (defn callback [req] (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]) (get-access-token) (get-user-info))] - (when (not= :google-oauth (:type tdata)) - (ex/raise :type :validation - :code ::tokens/invalid-token)) - (when-not info (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 :email (:email info) :fullname (:fullname info)}) uagent (get-in req [:headers "user-agent"]) - tdata {:type :authentication - :profile profile} - token (tokens/create! db/pool tdata {:valid {:minutes 10}}) - + token (tokens/generate + {:iss :auth + :exp (dt/in-future "15m") + :profile-id (:id profile)}) uri (-> (uri/uri (:public-uri cfg/config)) (assoc :path "/#/auth/verify-token") (assoc :query (uri/map->query-string {:token token}))) @@ -133,4 +131,3 @@ :headers {"location" (str uri)} :cookies (session/cookies sid) :body ""}))) - diff --git a/backend/src/app/http/auth/ldap.clj b/backend/src/app/http/auth/ldap.clj index af74843fb..1018eb44f 100644 --- a/backend/src/app/http/auth/ldap.clj +++ b/backend/src/app/http/auth/ldap.clj @@ -51,7 +51,7 @@ (first))] (when-not (client/bind? conn (:dn user-entry) password) (ex/raise :type :authentication - :code ::wrong-credentials)) + :code :wrong-credentials)) (set/rename-keys user-entry {(keyword (:ldap-auth-avatar-attribute cfg/config)) :photo (keyword (:ldap-auth-fullname-attribute cfg/config)) :fullname (keyword (:ldap-auth-email-attribute cfg/config)) :email}))))) diff --git a/backend/src/app/http/session.clj b/backend/src/app/http/session.clj index de926da51..be3384364 100644 --- a/backend/src/app/http/session.clj +++ b/backend/src/app/http/session.clj @@ -10,7 +10,14 @@ (ns app.http.session (:require [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 [request] @@ -29,7 +36,7 @@ (defn create [profile-id user-agent] - (let [id (tokens/next-token)] + (let [id (next-token 64)] (db/insert! db/pool :http-session {:id id :profile-id profile-id :user-agent user-agent}) diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj index 8598d9a02..7f07dc67e 100644 --- a/backend/src/app/migrations.clj +++ b/backend/src/app/migrations.clj @@ -86,6 +86,13 @@ {:name "0023-adapt-old-pages-and-files" :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")} + ]}) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/backend/src/app/migrations/sql/0024-mod-profile-table.sql b/backend/src/app/migrations/sql/0024-mod-profile-table.sql new file mode 100644 index 000000000..dabecd04a --- /dev/null +++ b/backend/src/app/migrations/sql/0024-mod-profile-table.sql @@ -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; diff --git a/backend/src/app/migrations/sql/0025-del-generic-tokens-table.sql b/backend/src/app/migrations/sql/0025-del-generic-tokens-table.sql new file mode 100644 index 000000000..4c0f31c24 --- /dev/null +++ b/backend/src/app/migrations/sql/0025-del-generic-tokens-table.sql @@ -0,0 +1 @@ +DROP TABLE generic_token; diff --git a/backend/src/app/services/mutations/demo.clj b/backend/src/app/services/mutations/demo.clj index a2f03a349..839deae81 100644 --- a/backend/src/app/services/mutations/demo.clj +++ b/backend/src/app/services/mutations/demo.clj @@ -28,7 +28,7 @@ sem (System/currentTimeMillis) email (str "demo-" sem ".demo@nodomain.com") fullname (str "Demo User " sem) - password (-> (bn/random-bytes 12) + password (-> (bn/random-bytes 16) (bc/bytes->b64u) (bc/bytes->str)) params {:id id diff --git a/backend/src/app/services/mutations/profile.clj b/backend/src/app/services/mutations/profile.clj index 117cbf40d..efc2c1932 100644 --- a/backend/src/app/services/mutations/profile.clj +++ b/backend/src/app/services/mutations/profile.clj @@ -5,7 +5,7 @@ ;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; defined by the Mozilla Public License, v. 2.0. ;; -;; Copyright (c) 2016-2020 Andrey Antukh +;; Copyright (c) 2020 UXBOX Labs SL (ns app.services.mutations.profile (:require @@ -35,7 +35,6 @@ [cuerdas.core :as str] [datoteka.core :as fs])) - ;; --- Helpers & Specs (s/def ::email ::us/email) @@ -70,22 +69,22 @@ [params] (when-not (:registration-enabled cfg/config) (ex/raise :type :restriction - :code ::registration-disabled)) + :code :registration-disabled)) (when-not (email-domain-in-whitelist? (:registration-domain-whitelist cfg/config) (:email params)) (ex/raise :type :validation - :code ::email-domain-is-not-allowed)) + :code :email-domain-is-not-allowed)) (db/with-atomic [conn db/pool] (check-profile-existence! conn params) (let [profile (->> (create-profile conn params) (create-profile-relations conn)) - payload {:type :verify-email - :profile-id (:id profile) - :email (:email profile)} - - token (tokens/create! conn payload {:valid {:days 30}})] + token (tokens/generate + {:iss :verify-email + :exp (dt/in-future "48h") + :profile-id (:id profile) + :email (:email profile)})] (emails/send! conn emails/register {:to (:email profile) @@ -104,7 +103,7 @@ result (db/exec-one! conn [sql:profile-existence email])] (when (:val result) (ex/raise :type :validation - :code ::email-already-exists)) + :code :email-already-exists)) params)) (defn- derive-password @@ -119,16 +118,17 @@ "Create the profile entry on the database with limited input filling all the other fields with defaults." [conn {:keys [id fullname email password demo?] :as params}] - (let [id (or id (uuid/next)) - demo? (if (boolean? demo?) demo? false) - paswd (derive-password password)] + (let [id (or id (uuid/next)) + demo? (if (boolean? demo?) demo? false) + active? (if demo? true false) + password (derive-password password)] (db/insert! conn :profile {:id id :fullname fullname :email (str/lower email) - :pending-email (if demo? nil email) :photo "" - :password paswd + :password password + :is-active active? :is-demo demo?}))) (defn- create-profile-relations @@ -165,17 +165,21 @@ (letfn [(check-password [profile password] (when (= (:password profile) "!") (ex/raise :type :validation - :code ::account-without-password)) + :code :account-without-password)) (:valid (verify-password password (:password profile)))) (validate-profile [profile] + (when-not (:is-active profile) + (ex/raise :type :validation + :code :wrong-credentials)) (when-not profile (ex/raise :type :validation - :code ::wrong-credentials)) + :code :wrong-credentials)) (when-not (check-password profile password) (ex/raise :type :validation - :code ::wrong-credentials)) + :code :wrong-credentials)) profile)] + (db/with-atomic [conn db/pool] (let [prof (-> (retrieve-profile-by-email conn email) (validate-profile) @@ -185,8 +189,8 @@ (def sql:profile-by-email "select * from profile - where email=? and deleted_at is null - for update") + where email=? + and deleted_at is null") (defn- retrieve-profile-by-email [conn email] @@ -207,7 +211,7 @@ {:id (uuid/next) :fullname fullname :email (str/lower email) - :pending-email nil + :is-active true :photo "" :password "!" :is-demo false})) @@ -251,7 +255,7 @@ (let [profile (profile/retrieve-profile-data conn profile-id)] (when-not (:valid (verify-password old-password (:password profile))) (ex/raise :type :validation - :code ::old-password-not-match)))) + :code :old-password-not-match)))) (s/def ::update-profile-password (s/keys :req-un [::profile-id ::password ::old-password])) @@ -317,8 +321,6 @@ ;; --- Mutation: Request Email Change -(declare select-profile-for-update) - (s/def ::request-email-change (s/keys :req-un [::email])) @@ -326,20 +328,16 @@ [{:keys [profile-id email] :as params}] (db/with-atomic [conn db/pool] (let [email (str/lower email) - profile (select-profile-for-update conn profile-id) - payload {:type :change-email - :profile-id profile-id - :email email} - - token (tokens/create! conn payload)] + profile (db/get-by-id conn :profile profile-id) + token (tokens/generate + {:iss :change-email + :exp (dt/in-future "15m") + :profile-id profile-id + :email email})] (when (not= email (:email profile)) (check-profile-existence! conn params)) - (db/update! conn :profile - {:pending-email email} - {:id profile-id}) - (emails/send! conn emails/change-email {:to (:email profile) :name (:fullname profile) @@ -357,65 +355,51 @@ ;; Generic mutation for perform token based verification for auth ;; domain. +(defmulti process-token (fn [conn claims] (:iss claims))) + (s/def ::verify-profile-token (s/keys :req-un [::token])) (sm/defmutation ::verify-profile-token [{: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] - (let [profile (select-profile-for-update conn profile-id)] - (when (= (:email profile) - (:pending-email profile)) - (ex/raise :type :validation - :code ::unexpected-request)) + (let [claims (tokens/verify token)] + (process-token conn claims)))) + +(defmethod process-token :change-email + [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 @@ -425,9 +409,10 @@ (sm/defmutation ::request-profile-recovery [{:keys [email] :as params}] (letfn [(create-recovery-token [conn {:keys [id] :as profile}] - (let [payload {:type :password-recovery-token - :profile-id id} - token (tokens/create! conn payload)] + (let [token (tokens/generate + {:iss :password-recovery + :exp (dt/in-future "15m") + :profile-id id})] (assoc profile :token token))) (send-email-notification [conn profile] @@ -453,23 +438,16 @@ (sm/defmutation ::recover-profile [{:keys [token password]}] (letfn [(validate-token [conn token] - (let [tpayload (tokens/retrieve conn token)] - (when (not= (:type tpayload) :password-recovery-token) - (ex/raise :type :validation - :code ::tokens/invalid-token)) - (:profile-id tpayload))) + (let [tdata (tokens/verify token {:iss :password-recovery})] + (:profile-id tdata))) (update-password [conn profile-id] (let [pwd (derive-password password)] - (db/update! conn :profile {:password pwd} {:id profile-id}))) - - (delete-token [conn token] - (db/delete! conn :generic-token {:token token}))] + (db/update! conn :profile {:password pwd} {:id profile-id})))] (db/with-atomic [conn db/pool] (->> (validate-token conn token) (update-password conn)) - (delete-token conn token) nil))) @@ -515,6 +493,6 @@ (let [rows (db/exec! conn [sql:teams-ownership-check profile-id])] (when-not (empty? rows) (ex/raise :type :validation - :code ::owner-teams-with-people + :code :owner-teams-with-people :hint "The user need to transfer ownership of owned teams." :context {:teams (mapv :team-id rows)})))) diff --git a/backend/src/app/services/tokens.clj b/backend/src/app/services/tokens.clj index aa5a2189f..4c57a3da0 100644 --- a/backend/src/app/services/tokens.clj +++ b/backend/src/app/services/tokens.clj @@ -9,70 +9,59 @@ (ns app.services.tokens (: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.spec :as us] + [app.config :as cfg] + [app.db :as db] [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 - ([] (next-token 96)) - ([n] - (-> (bn/random-bytes n) - (bc/bytes->b64u) - (bc/bytes->str)))) +(defn- derive-tokens-secret + [key] + (when (= key "default") + (log/warn "Using default APP_SECRET_KEY, the system will generate insecure tokens.")) + (let [engine (bk/engine {:key key + :salt "tokens" + :alg :hkdf + :digest :blake2b-512})] + (bk/get-bytes engine 32))) -(def default-duration - (dt/duration {:hours 48})) +(def secret + (delay (derive-tokens-secret (:secret-key cfg/config)))) -(defn- decode-row - [{:keys [content] :as row}] - (when row - (cond-> row - (db/pgobject? content) - (assoc :content (db/decode-transit-pgobject content))))) +(defn generate + [claims] + (let [payload (t/encode claims)] + (jwe/encrypt payload @secret {:alg :a256kw :enc :a256gcm}))) -(defn create! - ([conn payload] (create! conn payload {})) - ([conn payload {:keys [valid] :or {valid default-duration}}] - (let [token (next-token) - until (dt/plus (dt/now) (dt/duration valid))] - (db/insert! conn :generic-token - {:content (db/tjson payload) - :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 +(defn verify + ([token] (verify token nil)) + ([token params] + (let [payload (jwe/decrypt token @secret {:alg :a256kw :enc :a256gcm}) + claims (t/decode payload)] + (when (and (dt/instant? (:exp claims)) + (dt/is-before? (:exp claims) (dt/now))) (ex/raise :type :validation - :code ::invalid-token)) - - ;; Validate the token expiration - (when (> (inst-ms (dt/now)) - (inst-ms (:valid-until row))) + :code :invalid-token + :reason :token-expired + :params params + :claims claims)) + (when (and (contains? params :iss) + (not= (:iss claims) + (:iss params))) (ex/raise :type :validation - :code ::invalid-token)) - - (when delete - (db/delete! conn :generic-token {:token token})) - - (-> row - (dissoc :content) - (merge (:content row)))))) + :code :invalid-token + :reason :invalid-issuer + :claims claims + :params params)) + claims))) + diff --git a/backend/src/app/util/time.clj b/backend/src/app/util/time.clj index bfe99127f..c855afd24 100644 --- a/backend/src/app/util/time.clj +++ b/backend/src/app/util/time.clj @@ -29,9 +29,17 @@ {:pre [(string? s)]} (Instant/parse s)) -(defn now - [] - (Instant/now)) +(defn instant? + [v] + (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 [d ta] @@ -65,6 +73,14 @@ :else (obj->duration ms-or-obj))) +(defn now + [] + (Instant/now)) + +(defn in-future + [v] + (plus (now) (duration v))) + (defn duration-between [t1 t2] (Duration/between t1 t2)) diff --git a/frontend/resources/locales.json b/frontend/resources/locales.json index 2c4b7b389..59c53d8df 100644 --- a/frontend/resources/locales.json +++ b/frontend/resources/locales.json @@ -1,6 +1,6 @@ { "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" : { "en" : "Already have an account?", "fr" : "Vous avez déjà un compte?", @@ -18,7 +18,7 @@ } }, "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" : { "en" : "Create demo account", "fr" : "Créer un compte de démonstration", @@ -27,7 +27,7 @@ } }, "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" : { "en" : "Just wanna try it?", "fr" : "Vous voulez juste essayer?", @@ -36,7 +36,7 @@ } }, "auth.demo-warning" : { - "used-in" : [ "src/app/main/ui/auth/register.cljs:32" ], + "used-in" : [ "src/app/main/ui/auth/register.cljs:33" ], "translations" : { "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.", @@ -45,7 +45,7 @@ } }, "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" : { "en" : "Email", "fr" : "Adresse email", @@ -63,7 +63,7 @@ } }, "auth.fullname-label" : { - "used-in" : [ "src/app/main/ui/auth/register.cljs:75" ], + "used-in" : [ "src/app/main/ui/auth/register.cljs:84" ], "translations" : { "en" : "Full Name", "fr" : "Nom complet", @@ -90,7 +90,7 @@ } }, "auth.login-here" : { - "used-in" : [ "src/app/main/ui/auth/register.cljs:109" ], + "used-in" : [ "src/app/main/ui/auth/register.cljs:118" ], "translations" : { "en" : "Login here", "fr" : "Se connecter ici", @@ -179,8 +179,14 @@ "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" : { - "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" : { "en" : "Password", "fr" : "Mot de passe", @@ -189,7 +195,7 @@ } }, "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" : { "en" : "At least 8 characters", "fr" : "Au moins 8 caractères", @@ -252,7 +258,7 @@ } }, "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" : { "en" : "Create an account", "fr" : "Créer un compte", @@ -261,7 +267,7 @@ } }, "auth.register-subtitle" : { - "used-in" : [ "src/app/main/ui/auth/register.cljs:98" ], + "used-in" : [ "src/app/main/ui/auth/register.cljs:107" ], "translations" : { "en" : "It's free, it's Open Source", "fr" : "C'est gratuit, c'est Open Source", @@ -270,7 +276,7 @@ } }, "auth.register-title" : { - "used-in" : [ "src/app/main/ui/auth/register.cljs:97" ], + "used-in" : [ "src/app/main/ui/auth/register.cljs:106" ], "translations" : { "en" : "Create an account", "fr" : "Créer un compte", @@ -729,7 +735,7 @@ } }, "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" : { "en" : "Email already used", "fr" : "Adresse e-mail déjà utilisée", @@ -737,8 +743,17 @@ "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" : { - "used-in" : [ "src/app/main/ui/settings/change_email.cljs:37" ], + "used-in" : [ "src/app/main/ui/settings/change_email.cljs:36" ], "translations" : { "en" : "Confirmation email must match", "fr" : "L'adresse e-mail de confirmation doit correspondre", @@ -747,7 +762,7 @@ } }, "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" : { "en" : "Something wrong has happened.", "fr" : "Quelque chose c'est mal passé.", @@ -819,7 +834,7 @@ } }, "errors.registration-disabled" : { - "used-in" : [ "src/app/main/ui/auth/register.cljs:47" ], + "used-in" : [ "src/app/main/ui/auth/register.cljs:49" ], "translations" : { "en" : "The registration is currently disabled.", "fr" : "L'enregistrement est actuellement désactivé.", @@ -828,7 +843,7 @@ } }, "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" : { "en" : "An unexpected error occurred.", "fr" : "Une erreur inattendue c'est produite", @@ -918,7 +933,7 @@ } }, "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" : { "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é.", @@ -926,23 +941,14 @@ "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" : { - "used-in" : [ "src/app/main/ui/settings/profile.cljs:78" ], "translations" : { - "en" : "There is a pending change of your email to “%s”.", - "fr" : "Il y a un changement en attente de votre adresse e-mail “%s”.", - "ru" : "Ваш email адреса будет сменен на “%s”.", - "es" : "Hay un cambio de correo pendiente a “%s”." - } + "en" : null, + "fr" : null, + "es" : null, + "ru" : null + }, + "used-in" : [ "src/app/main/ui/settings/profile.cljs:78" ] }, "settings.change-email-label" : { "used-in" : [ "src/app/main/ui/settings/profile.cljs:73" ], @@ -954,7 +960,7 @@ } }, "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" : { "en" : "Change email", "fr" : "Changer adresse e-mail", @@ -963,7 +969,7 @@ } }, "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" : { "en" : "Change your email", "fr" : "Changer adresse e-mail", @@ -971,17 +977,8 @@ "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" : { - "used-in" : [ "src/app/main/ui/settings/change_email.cljs:80" ], + "used-in" : [ "src/app/main/ui/settings/change_email.cljs:85" ], "translations" : { "en" : "Verify new email", "fr" : "Vérifier la nouvelle adresse e-mail", @@ -1071,7 +1068,7 @@ } }, "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" : { "en" : "New email", "fr" : "Nouvel e-mail", @@ -1089,7 +1086,7 @@ } }, "settings.notifications.email-changed-successfully" : { - "used-in" : [ "src/app/main/ui/auth.cljs:62" ], + "used-in" : [ "src/app/main/ui/auth.cljs:63" ], "translations" : { "en" : "Your email address has been updated successfully", "fr" : "Votre adresse e-mail a été mise à jour avec succès", @@ -1098,16 +1095,16 @@ } }, "settings.notifications.email-not-verified" : { - "used-in" : [ "src/app/main/ui/dashboard.cljs:100" ], "translations" : { - "en" : "Your email address has not been verified yet. Please check your inbox at “%s” for a confirmation email.", - "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.", - "ru" : "Ваш email адрес еще не подтвержден. Пожалуйста, проверьте наличие подтверждающего письма во входящих на “%s”.", - "es" : "Tu dirección de correo aún no ha sido verificada. Por favor, busca en tu correo “%s” un mensaje de confirmación." - } + "en" : null, + "fr" : null, + "es" : null, + "ru" : null + }, + "used-in" : [ "src/app/main/ui/dashboard.cljs:100" ] }, "settings.notifications.email-verified-successfully" : { - "used-in" : [ "src/app/main/ui/auth.cljs:55" ], + "used-in" : [ "src/app/main/ui/auth.cljs:57" ], "translations" : { "en" : "Your email address has been verified successfully", "fr" : "Votre adresse e-mail a été vérifiée avec succès", @@ -1125,7 +1122,7 @@ } }, "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" : { "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.", @@ -1241,15 +1238,6 @@ "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" : { "used-in" : [ "src/app/main/ui/settings/delete_account.cljs:43" ], "translations" : { @@ -1278,7 +1266,7 @@ } }, "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" : { "en" : "Don't show interactions", "fr" : "Ne pas afficher les interactions", @@ -1287,7 +1275,7 @@ } }, "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" : { "en" : "Edit page", "fr" : "Editer la page", @@ -1296,7 +1284,7 @@ } }, "viewer.header.fullscreen" : { - "used-in" : [ "src/app/main/ui/viewer/header.cljs:177" ], + "used-in" : [ "src/app/main/ui/viewer/header.cljs:179" ], "translations" : { "en" : "Full Screen", "fr" : "Plein écran", @@ -1305,7 +1293,7 @@ } }, "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" : { "en" : "Copy link", "fr" : "Copier lien", @@ -1314,7 +1302,7 @@ } }, "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" : { "en" : "Create link", "fr" : "Créer lien", @@ -1323,7 +1311,7 @@ } }, "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" : { "en" : "Share link will appear here", "fr" : "Le lien de partage apparaîtra ici", @@ -1332,7 +1320,7 @@ } }, "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" : { "en" : "Remove link", "fr" : "Supprimer le lien", @@ -1341,7 +1329,7 @@ } }, "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" : { "en" : "Anyone with the link will have access", "fr" : "Toute personne disposant du lien aura accès", @@ -1350,7 +1338,7 @@ } }, "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" : { "en" : "Share link", "fr" : "Lien de partage", @@ -1359,7 +1347,7 @@ } }, "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" : { "en" : "Show interactions", "fr" : "Afficher les interactions", @@ -1368,7 +1356,7 @@ } }, "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" : { "en" : "Show interactions on click", "fr" : "Afficher les interactions au clic", @@ -1377,7 +1365,7 @@ } }, "viewer.header.sitemap" : { - "used-in" : [ "src/app/main/ui/viewer/header.cljs:147" ], + "used-in" : [ "src/app/main/ui/viewer/header.cljs:149" ], "translations" : { "en" : "Sitemap", "fr" : "Plan du site", @@ -1761,7 +1749,7 @@ } }, "workspace.libraries.colors" : { - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:62" ], + "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:68" ], "translations" : { "en" : "%s colors", "fr" : "", @@ -1800,7 +1788,7 @@ } }, "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" : { "en" : "File library", "fr" : "", @@ -1809,7 +1797,7 @@ } }, "workspace.libraries.graphics" : { - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:59" ], + "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:65" ], "translations" : { "en" : "%s graphics", "fr" : "", @@ -1818,7 +1806,7 @@ } }, "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" : { "en" : "LIBRARIES IN THIS FILE", "fr" : "", @@ -1854,7 +1842,7 @@ } }, "workspace.libraries.remove" : { - "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:80" ], + "used-in" : [ "src/app/main/ui/workspace/libraries.cljs:82" ], "translations" : { "en" : "Remove", "fr" : "", @@ -1863,7 +1851,7 @@ } }, "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" : { "en" : "Search shared libraries", "fr" : "", @@ -1872,7 +1860,7 @@ } }, "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" : { "en" : "SHARED LIBRARIES", "fr" : "", diff --git a/frontend/src/app/main/data/auth.cljs b/frontend/src/app/main/data/auth.cljs index 2a235b79f..41bb2cbd3 100644 --- a/frontend/src/app/main/data/auth.cljs +++ b/frontend/src/app/main/data/auth.cljs @@ -146,10 +146,7 @@ on-success identity}} (meta data)] (->> (rp/mutation :register-profile data) (rx/tap on-success) - (rx/map #(login data)) - (rx/catch (fn [err] - (on-error err) - (rx/empty)))))))) + (rx/catch on-error)))))) ;; --- Request Account Deletion diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs index 1ade0ca6c..b068345bf 100644 --- a/frontend/src/app/main/data/users.cljs +++ b/frontend/src/app/main/data/users.cljs @@ -114,10 +114,7 @@ on-success identity}} (meta data)] (->> (rp/mutation :request-email-change data) (rx/tap on-success) - (rx/map (constantly fetch-profile)) - (rx/catch (fn [err] - (on-error err) - (rx/empty)))))))) + (rx/catch on-error)))))) ;; --- Cancel Email Change diff --git a/frontend/src/app/main/ui/auth.cljs b/frontend/src/app/main/ui/auth.cljs index 804b7abb4..8d636175e 100644 --- a/frontend/src/app/main/ui/auth.cljs +++ b/frontend/src/app/main/ui/auth.cljs @@ -9,23 +9,23 @@ (ns app.main.ui.auth (: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.users :as du] [app.main.data.messages :as dm] + [app.main.data.users :as du] + [app.main.repo :as rp] [app.main.store :as st] [app.main.ui.auth.login :refer [login-page]] [app.main.ui.auth.recovery :refer [recovery-page]] [app.main.ui.auth.recovery-request :refer [recovery-request-page]] [app.main.ui.auth.register :refer [register-page]] - [app.main.repo :as rp] - [app.util.timers :as ts] + [app.main.ui.icons :as i] [app.util.forms :as fm] [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 [{:keys [locale] :as props}] @@ -50,24 +50,30 @@ :auth-recovery [:& recovery-page {:locale locale :params (:query-params route)}])]])) -(defn- handle-email-verified +(defmulti handle-token (fn [token] (:iss token))) + +(defmethod handle-token :verify-email [data] (let [msg (tr "settings.notifications.email-verified-successfully")] (ts/schedule 100 #(st/emit! (dm/success msg))) - (st/emit! (rt/nav :settings-profile) - du/fetch-profile))) + (st/emit! (rt/nav :auth-login)))) -(defn- handle-email-changed +(defmethod handle-token :change-email [data] (let [msg (tr "settings.notifications.email-changed-successfully")] (ts/schedule 100 #(st/emit! (dm/success msg))) (st/emit! (rt/nav :settings-profile) du/fetch-profile))) -(defn- handle-authentication +(defmethod handle-token :auth [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 [{:keys [route] :as props}] (let [token (get-in route [:query-params :token])] @@ -76,21 +82,22 @@ (->> (rp/mutation :verify-profile-token {:token token}) (rx/subs (fn [tdata] - (case (:type tdata) - :verify-email (handle-email-verified tdata) - :change-email (handle-email-changed tdata) - :authentication (handle-authentication tdata) - nil)) + (handle-token tdata)) (fn [error] (case (:code error) - :app.services.mutations.profile/email-already-exists + :email-already-exists (let [msg (tr "errors.email-already-exists")] (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")] (ts/schedule 100 #(st/emit! (dm/error msg))) - (st/emit! (rt/nav :settings-profile))))))))) + (st/emit! (rt/nav :auth-login))))))))) [:div.verify-token i/loader-pencil])) diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs index b811fbca6..5511f3ab1 100644 --- a/frontend/src/app/main/ui/auth/register.cljs +++ b/frontend/src/app/main/ui/auth/register.cljs @@ -9,21 +9,22 @@ (ns app.main.ui.auth.register (:require - [cljs.spec.alpha :as s] - [cuerdas.core :as str] - [rumext.alpha :as mf] [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 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.icons :as i] [app.main.ui.messages :as msgs] [app.util.dom :as dom] [app.util.forms :as fm] [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 [_] @@ -43,14 +44,20 @@ (defn- on-error [form error] (case (:code error) - :app.services.mutations.profile/registration-disabled - (st/emit! (tr "errors.registration-disabled")) + :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] {: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 [data] @@ -61,7 +68,8 @@ (defn- on-submit [form event] (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)))) (mf/defc register-form diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs index 3dc6356db..e0c7ce10e 100644 --- a/frontend/src/app/main/ui/dashboard.cljs +++ b/frontend/src/app/main/ui/dashboard.cljs @@ -51,51 +51,33 @@ (= "drafts" project-id) (assoc :project-id (:default-project-id profile))))) -(declare global-notifications) - - (mf/defc dashboard [{:keys [route] :as props}] (let [profile (mf/deref refs/profile) - page (get-in route [:data :name]) - {:keys [search-term team-id project-id] :as params} - (parse-params route profile)] - [:* - [:& global-notifications {:profile profile}] - [:section.dashboard-layout - [:div.main-logo - [:a {:on-click #(st/emit! (rt/nav :dashboard-team {:team-id team-id}))} - i/logo-icon]] - [:& profile-section {:profile profile}] - [:& sidebar {:team-id team-id - :project-id project-id - :section page - :search-term search-term}] - [:div.dashboard-content - (case page - :dashboard-search - [:& search-page {:team-id team-id :search-term search-term}] + page (get-in route [:data :name]) + {:keys [search-term team-id project-id] :as params} (parse-params route profile)] + [:section.dashboard-layout + [:div.main-logo + [:a {:on-click #(st/emit! (rt/nav :dashboard-team {:team-id team-id}))} + i/logo-icon]] + [:& profile-section {:profile profile}] + [:& sidebar {:team-id team-id + :project-id project-id + :section page + :search-term search-term}] + [:div.dashboard-content + (case page + :dashboard-search + [:& search-page {:team-id team-id :search-term search-term}] - :dashboard-team - [:& recent-files-page {:team-id team-id}] + :dashboard-team + [:& recent-files-page {:team-id team-id}] - :dashboard-libraries - [:& libraries-page {:team-id team-id}] + :dashboard-libraries + [:& libraries-page {:team-id team-id}] - :dashboard-project - [:& project-page {:team-id team-id - :project-id project-id}])]]])) + :dashboard-project + [:& project-page {:team-id team-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))]]]))) - diff --git a/frontend/src/app/main/ui/modal.cljs b/frontend/src/app/main/ui/modal.cljs index a086af5f0..f8288d25f 100644 --- a/frontend/src/app/main/ui/modal.cljs +++ b/frontend/src/app/main/ui/modal.cljs @@ -23,9 +23,9 @@ (defonce components (atom {})) (defn show! - ([type props] - (let [id (random-uuid)] - (st/emit! (mdm/show-modal id type props))))) + [type props] + (let [id (random-uuid)] + (st/emit! (mdm/show-modal id type props)))) (defn allow-click-outside! [] (st/emit! (mdm/update-modal {:allow-click-outside true}))) @@ -37,6 +37,8 @@ [] (st/emit! (mdm/hide-modal))) +(def hide (mdm/hide-modal)) + (defn- on-esc-clicked [event] (when (k/esc? event) diff --git a/frontend/src/app/main/ui/settings/change_email.cljs b/frontend/src/app/main/ui/settings/change_email.cljs index bd51ed6e3..d1258f77e 100644 --- a/frontend/src/app/main/ui/settings/change_email.cljs +++ b/frontend/src/app/main/ui/settings/change_email.cljs @@ -9,9 +9,7 @@ (ns app.main.ui.settings.change-email (:require - [cljs.spec.alpha :as s] - [cuerdas.core :as str] - [rumext.alpha :as mf] + [app.common.spec :as us] [app.main.data.auth :as da] [app.main.data.messages :as dm] [app.main.data.users :as du] @@ -21,12 +19,13 @@ [app.main.ui.icons :as i] [app.main.ui.messages :as msgs] [app.main.ui.modal :as modal] - [app.util.dom :as dom] - [app.util.forms :as fm] - [app.util.i18n :as i18n :refer [tr t]])) + [app.util.i18n :as i18n :refer [tr t]] + [cljs.spec.alpha :as s] + [cuerdas.core :as str] + [rumext.alpha :as mf])) -(s/def ::email-1 ::fm/email) -(s/def ::email-2 ::fm/email) +(s/def ::email-1 ::us/email) +(s/def ::email-2 ::us/email) (defn- email-equality [data] @@ -42,7 +41,7 @@ (defn- on-error [form error] (cond - (= (:code error) :app.services.mutations.profile/email-already-exists) + (= (:code error) :email-already-exists) (swap! form (fn [data] (let [error {:message (tr "errors.email-already-exists")}] (assoc-in data [:errors :email-1] error)))) @@ -51,10 +50,16 @@ (let [msg (tr "errors.unexpected-error")] (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 - [form event] + [profile form event] (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)))) (mf/defc change-email-form @@ -66,7 +71,7 @@ {:type :info :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 :validators [email-equality] :initial {}} @@ -83,29 +88,14 @@ [:& submit-button {: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/register modal/components ::mf/register-as :change-email} [props] - (let [locale (mf/deref i18n/locale) + (let [locale (mf/deref i18n/locale) profile (mf/deref refs/profile)] [:div.modal-overlay [:div.generic-modal.change-email-modal [:span.close {:on-click #(modal/hide!)} i/close] - (if (:pending-email profile) - [:& change-email-confirmation {:locale locale :profile profile}] - [:& change-email-form {:locale locale :profile profile}])]])) + [:& change-email-form {:locale locale :profile profile}]]])) +