From 9e6804132633096d192197cad04af9cc1f3396bd Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 13 Jan 2020 23:52:31 +0100 Subject: [PATCH] :construction: Initial work on password recovery and register refactor. --- backend/deps.edn | 1 + .../emails/en/password-recovery.mustache | 42 +++ backend/src/uxbox/core.clj | 5 +- backend/src/uxbox/emails.clj | 29 +- backend/src/uxbox/http.clj | 9 +- backend/src/uxbox/http/handlers.clj | 68 +++-- backend/src/uxbox/http/session.clj | 17 +- backend/src/uxbox/jobs/sendmail.clj | 8 +- backend/src/uxbox/main.clj | 5 +- backend/src/uxbox/services/init.clj | 8 +- backend/src/uxbox/services/mutations.clj | 5 +- backend/src/uxbox/services/mutations/auth.clj | 48 ---- .../mutations/{users.clj => profile.clj} | 247 ++++++++---------- .../services/mutations/project_files.clj | 5 +- backend/tests/uxbox/tests/helpers.clj | 6 +- frontend/resources/locales.json | 66 ++++- frontend/src/uxbox/main.cljs | 3 + frontend/src/uxbox/main/data/auth.cljs | 103 +++----- frontend/src/uxbox/main/repo/core.cljs | 21 ++ frontend/src/uxbox/main/ui.cljs | 108 ++++---- frontend/src/uxbox/main/ui/auth.cljs | 16 -- frontend/src/uxbox/main/ui/auth/recovery.cljs | 82 ------ .../uxbox/main/ui/auth/recovery_request.cljs | 71 ----- .../src/uxbox/main/ui/{auth => }/login.cljs | 13 +- .../src/uxbox/main/ui/profile/recovery.cljs | 90 +++++++ .../main/ui/profile/recovery_request.cljs | 72 +++++ .../main/ui/{auth => profile}/register.cljs | 18 +- .../src/uxbox/main/ui/settings/header.cljs | 2 +- 28 files changed, 607 insertions(+), 561 deletions(-) create mode 100644 backend/resources/emails/en/password-recovery.mustache delete mode 100644 backend/src/uxbox/services/mutations/auth.clj rename backend/src/uxbox/services/mutations/{users.clj => profile.clj} (57%) delete mode 100644 frontend/src/uxbox/main/ui/auth.cljs delete mode 100644 frontend/src/uxbox/main/ui/auth/recovery.cljs delete mode 100644 frontend/src/uxbox/main/ui/auth/recovery_request.cljs rename frontend/src/uxbox/main/ui/{auth => }/login.cljs (86%) create mode 100644 frontend/src/uxbox/main/ui/profile/recovery.cljs create mode 100644 frontend/src/uxbox/main/ui/profile/recovery_request.cljs rename frontend/src/uxbox/main/ui/{auth => profile}/register.cljs (89%) diff --git a/backend/deps.edn b/backend/deps.edn index 273672f4b..acc9cd703 100644 --- a/backend/deps.edn +++ b/backend/deps.edn @@ -18,6 +18,7 @@ instaparse/instaparse {:mvn/version "1.4.10"} com.cognitect/transit-clj {:mvn/version "0.8.319"} + ;; TODO: vendorize pgclient under `vertx-clojure/vertx-pgclient` io.vertx/vertx-pg-client {:mvn/version "3.8.4"} vertx-clojure/vertx diff --git a/backend/resources/emails/en/password-recovery.mustache b/backend/resources/emails/en/password-recovery.mustache new file mode 100644 index 000000000..df68f236a --- /dev/null +++ b/backend/resources/emails/en/password-recovery.mustache @@ -0,0 +1,42 @@ +-- begin :subject +Password recovery. +-- end + +-- begin :body-text +Hello {{name}}! + +You have requested a password recovery. + +The token is: + +{{ token }} +-- end + +-- begin :body-html + + + + + title + {{> ../partials/inline_style }} + + + + + + + + + + +
+ +

TODO

+

{{ token }}

+
+ {{> ../partials/en/footer }} + + +-- end \ No newline at end of file diff --git a/backend/src/uxbox/core.clj b/backend/src/uxbox/core.clj index c5586f7b8..c259375ec 100644 --- a/backend/src/uxbox/core.clj +++ b/backend/src/uxbox/core.clj @@ -2,7 +2,10 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; Copyright (c) 2019 Andrey Antukh +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2019-2020 Andrey Antukh (ns uxbox.core (:require diff --git a/backend/src/uxbox/emails.clj b/backend/src/uxbox/emails.clj index 51d1c5436..dafedba3a 100644 --- a/backend/src/uxbox/emails.clj +++ b/backend/src/uxbox/emails.clj @@ -23,16 +23,6 @@ {:static media/resolve-asset :comment (constantly nil)}) -;; --- Register Email - -(s/def ::name ::us/string) -(s/def ::register - (s/keys :req-un [::name])) - -(def register - "A new profile registration welcome email." - (emails/build ::register default-context)) - ;; --- Public API (defn render @@ -56,3 +46,22 @@ values ($1, $2) returning *"] (-> (db/query-one db/pool [sql data priority]) (p/then' (constantly nil))))) + +;; --- Emails + +(s/def ::name ::us/string) +(s/def ::register + (s/keys :req-un [::name])) + +(def register + "A new profile registration welcome email." + (emails/build ::register default-context)) + +(s/def ::token ::us/string) +(s/def ::password-recovery + (s/keys :req-un [::name ::token])) + +(def password-recovery + "A password recovery notification email." + (emails/build ::password-recovery default-context)) + diff --git a/backend/src/uxbox/http.clj b/backend/src/uxbox/http.clj index 5a1701a35..ae58a3e23 100644 --- a/backend/src/uxbox/http.clj +++ b/backend/src/uxbox/http.clj @@ -51,12 +51,6 @@ :timeout 200 :name "login-handler"}) - echo-handler (rl/ratelimit handlers/echo-handler - {:limit 1 - :period 5000 - :timeout 10 - :name "echo-handler"}) - routes [["/sub/:file-id" {:interceptors [(vxi/cookies) (vxi/cors cors-opts) interceptors/format-response-body @@ -64,10 +58,9 @@ :get ws/handler}] ["/api" {:interceptors interceptors} - ["/echo" {:all echo-handler}] + ["/echo" {:all handlers/echo-handler}] ["/login" {:post login-handler}] ["/logout" {:post handlers/logout-handler}] - ["/register" {:post handlers/register-handler}] ["/debug" ["/emails" {:get debug/emails-list}] ["/emails/:id" {:get debug/email}]] diff --git a/backend/src/uxbox/http/handlers.clj b/backend/src/uxbox/http/handlers.clj index 25ee3cad4..734bf73fd 100644 --- a/backend/src/uxbox/http/handlers.clj +++ b/backend/src/uxbox/http/handlers.clj @@ -16,28 +16,49 @@ [vertx.web :as vw] [vertx.eventbus :as ve])) +(def mutation-types-hierarchy + (-> (make-hierarchy) + (derive :login ::unauthenticated) + (derive :logout ::unauthenticated) + (derive :register-profile ::unauthenticated) + (derive :request-profile-recovery ::unauthenticated) + (derive :recover-profile ::unauthenticated))) + +(def query-types-hierarchy + (make-hierarchy)) + (defn query-handler [req] - (let [type (get-in req [:path-params :type]) + (let [type (keyword (get-in req [:path-params :type])) data (merge (:params req) - {::sq/type (keyword type) + {::sq/type type :user (:user req)})] - (-> (sq/handle (with-meta data {:req req})) - (p/then' (fn [result] - {:status 200 - :body result}))))) + (if (or (:user req) + (isa? query-types-hierarchy type ::unauthenticated)) + (-> (sq/handle (with-meta data {:req req})) + (p/then' (fn [result] + {:status 200 + :body result}))) + {:status 403 + :body {:type :authentication + :code :unauthorized}}))) (defn mutation-handler [req] - (let [type (get-in req [:path-params :type]) + (let [type (keyword (get-in req [:path-params :type])) data (merge (:params req) (:body-params req) (:uploads req) - {::sm/type (keyword type) + {::sm/type type :user (:user req)})] - (-> (sm/handle (with-meta data {:req req})) - (p/then' (fn [result] - {:status 200 :body result}))))) + (if (or (:user req) + (isa? mutation-types-hierarchy type ::unauthenticated)) + (-> (sm/handle (with-meta data {:req req})) + (p/then' (fn [result] + {:status 200 :body result}))) + {:status 403 + :body {:type :authentication + :code :unauthorized}}))) (defn login-handler [req] @@ -60,23 +81,20 @@ :cookies {"auth-token" nil} :body ""}))))) -(defn register-handler - [req] - (let [data (merge (:body-params req) - {::sm/type :register-profile}) - user-agent (get-in req [:headers "user-agent"])] - (-> (sm/handle (with-meta data {:req req})) - (p/then (fn [{:keys [id] :as user}] - (session/create id user-agent))) - (p/then' (fn [token] - {:status 204 - :cookies {"auth-token" {:value token}} - :body ""}))))) +;; (defn register-handler +;; [req] +;; (let [data (merge (:body-params req) +;; {::sm/type :register-profile}) +;; user-agent (get-in req [:headers "user-agent"])] +;; (-> (sm/handle (with-meta data {:req req})) +;; (p/then (fn [{:keys [id] :as user}] +;; (session/create id user-agent))) +;; (p/then' (fn [token] +;; {:status 204 +;; :body ""}))))) (defn echo-handler [req] - ;; (locking echo-handler - ;; (prn "echo-handler" (Thread/currentThread))) {:status 200 :body {:params (:params req) :cookies (:cookies req) diff --git a/backend/src/uxbox/http/session.clj b/backend/src/uxbox/http/session.clj index 877138948..5dfd76827 100644 --- a/backend/src/uxbox/http/session.clj +++ b/backend/src/uxbox/http/session.clj @@ -17,9 +17,10 @@ (defn retrieve "Retrieves a user id associated with the provided auth token." [token] - (let [sql "select user_id from sessions where id = $1"] - (-> (db/query-one db/pool [sql token]) - (p/then' (fn [row] (when row (:user-id row))))))) + (when token + (let [sql "select user_id from sessions where id = $1"] + (-> (db/query-one db/pool [sql token]) + (p/then' (fn [row] (when row (:user-id row)))))))) (defn create [user-id user-agent] @@ -52,11 +53,5 @@ (p/then' (fn [user-id] (if user-id (update data :request assoc :user user-id) - (spx/terminate (assoc data ::unauthorized true))))) - (vc/handle-on-context)))) - :leave (fn [data] - (if (::unauthorized data) - (update data :response - assoc :status 403 :body {:type :authentication - :code :unauthorized}) - data))}) + data))) + (vc/handle-on-context))))}) diff --git a/backend/src/uxbox/jobs/sendmail.clj b/backend/src/uxbox/jobs/sendmail.clj index 6435133b5..9c8cd570f 100644 --- a/backend/src/uxbox/jobs/sendmail.clj +++ b/backend/src/uxbox/jobs/sendmail.clj @@ -75,7 +75,7 @@ :pass (:smtp-password config) :ssl (:smtp-ssl config) :tls (:smtp-tls config) - :noop (not (:smtp-enabled config))}) + :enabled (:smtp-enabled config)}) (defn- send-email-to-console [email] @@ -98,9 +98,9 @@ [email] (p/future (let [config (get-smtp-config cfg/config) - result (if (:noop config) - (send-email-to-console email) - (postal/send-message config email))] + result (if (:enabled config) + (postal/send-message config email) + (send-email-to-console email))] (when (not= (:error result) :SUCCESS) (ex/raise :type :sendmail-error :code :email-not-sent diff --git a/backend/src/uxbox/main.clj b/backend/src/uxbox/main.clj index 18494e4a5..3a9ec4ce4 100644 --- a/backend/src/uxbox/main.clj +++ b/backend/src/uxbox/main.clj @@ -2,7 +2,10 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; Copyright (c) 2016-2019 Andrey Antukh +;; 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 (ns uxbox.main (:require [mount.core :as mount] diff --git a/backend/src/uxbox/services/init.clj b/backend/src/uxbox/services/init.clj index bda9a1af8..6bc9e8963 100644 --- a/backend/src/uxbox/services/init.clj +++ b/backend/src/uxbox/services/init.clj @@ -2,7 +2,10 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; Copyright (c) 2019 Andrey Antukh +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2019-2020 Andrey Antukh (ns uxbox.services.init "A initialization of services." @@ -26,8 +29,7 @@ (require 'uxbox.services.mutations.projects) (require 'uxbox.services.mutations.project-files) (require 'uxbox.services.mutations.project-pages) - (require 'uxbox.services.mutations.auth) - (require 'uxbox.services.mutations.users) + (require 'uxbox.services.mutations.profile) (require 'uxbox.services.mutations.user-attrs)) (defstate query-services diff --git a/backend/src/uxbox/services/mutations.clj b/backend/src/uxbox/services/mutations.clj index 3aba95007..050492d1a 100644 --- a/backend/src/uxbox/services/mutations.clj +++ b/backend/src/uxbox/services/mutations.clj @@ -2,7 +2,10 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; Copyright (c) 2019 Andrey Antukh +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2019-2020 Andrey Antukh (ns uxbox.services.mutations (:require diff --git a/backend/src/uxbox/services/mutations/auth.clj b/backend/src/uxbox/services/mutations/auth.clj deleted file mode 100644 index d88af4c73..000000000 --- a/backend/src/uxbox/services/mutations/auth.clj +++ /dev/null @@ -1,48 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) 2019 Andrey Antukh - -(ns uxbox.services.mutations.auth - (:require - [clojure.spec.alpha :as s] - [sodi.pwhash :as pwhash] - [promesa.core :as p] - [uxbox.config :as cfg] - [uxbox.common.exceptions :as ex] - [uxbox.common.spec :as us] - [uxbox.db :as db] - [uxbox.services.mutations :as sm])) - -(def ^:private user-by-username-sql - "select id, password - from users - where username=$1 or email=$1 - and deleted_at is null") - -(s/def ::username ::us/string) -(s/def ::password ::us/string) -(s/def ::scope ::us/string) - -(s/def ::login - (s/keys :req-un [::username ::password] - :opt-un [::scope])) - -(sm/defmutation ::login - [{:keys [username password scope] :as params}] - (letfn [(check-password [user password] - (let [result (pwhash/verify password (:password user))] - (:valid result))) - - (check-user [user] - (when-not user - (ex/raise :type :validation - :code ::wrong-credentials)) - (when-not (check-password user password) - (ex/raise :type :validation - :code ::wrong-credentials)) - - {:id (:id user)})] - (-> (db/query-one db/pool [user-by-username-sql username]) - (p/then' check-user)))) diff --git a/backend/src/uxbox/services/mutations/users.clj b/backend/src/uxbox/services/mutations/profile.clj similarity index 57% rename from backend/src/uxbox/services/mutations/users.clj rename to backend/src/uxbox/services/mutations/profile.clj index 3156e29a6..7aa2b7ddf 100644 --- a/backend/src/uxbox/services/mutations/users.clj +++ b/backend/src/uxbox/services/mutations/profile.clj @@ -2,19 +2,24 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; Copyright (c) 2016 Andrey Antukh +;; 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 -(ns uxbox.services.mutations.users +(ns uxbox.services.mutations.profile (:require - [sodi.pwhash :as pwhash] [clojure.spec.alpha :as s] [datoteka.core :as fs] [datoteka.storages :as ds] [promesa.core :as p] [promesa.exec :as px] - [uxbox.config :as cfg] + [sodi.prng] + [sodi.pwhash] + [sodi.util] [uxbox.common.exceptions :as ex] [uxbox.common.spec :as us] + [uxbox.config :as cfg] [uxbox.db :as db] [uxbox.emails :as emails] [uxbox.images :as images] @@ -40,6 +45,46 @@ (s/def ::user ::us/uuid) (s/def ::username ::us/string) +;; --- Utilities + +(su/defstr sql:user-by-username-or-email + "select u.* + from users as u + where u.username=$1 or u.email=$1 + and u.deleted_at is null") + +(defn- retrieve-user + [conn username] + (db/query-one conn [sql:user-by-username-or-email username])) + +;; --- Mutation: Login + +(s/def ::username ::us/string) +(s/def ::password ::us/string) +(s/def ::scope ::us/string) + +(s/def ::login + (s/keys :req-un [::username ::password] + :opt-un [::scope])) + +(sm/defmutation ::login + [{:keys [username password scope] :as params}] + (letfn [(check-password [user password] + (let [result (sodi.pwhash/verify password (:password user))] + (:valid result))) + + (check-user [user] + (when-not user + (ex/raise :type :validation + :code ::wrong-credentials)) + (when-not (check-password user password) + (ex/raise :type :validation + :code ::wrong-credentials)) + + {:id (:id user)})] + (-> (retrieve-user db/pool username) + (p/then' check-user)))) + ;; --- Mutation: Update Profile (own) (defn- check-username-and-email! @@ -55,7 +100,7 @@ and id != $1 ) as val"] (p/let [res1 (db/query-one conn [sql1 id username]) - res2 (db/query-one conn [sql2 id email])] + res2 (db/query-one conn [sql2 id email])] (when (:val res1) (ex/raise :type :validation :code ::username-already-exists)) @@ -64,17 +109,21 @@ :code ::email-already-exists)) params))) +(su/defstr sql:update-profile + "update users + set username = $2, + email = $3, + fullname = $4, + metadata = $5 + where id = $1 + and deleted_at is null + returning *") + (defn- update-profile [conn {:keys [id username email fullname metadata] :as params}] - (let [sql "update users - set username = $2, - email = $3, - fullname = $4, - metadata = $5 - where id = $1 - and deleted_at is null - returning *"] - (-> (db/query-one conn [sql id username email fullname (blob/encode metadata)]) + (let [sqlv [sql:update-profile id username + email fullname (blob/encode metadata)]] + (-> (db/query-one conn sqlv) (p/then' su/raise-not-found-if-nil) (p/then' decode-profile-row) (p/then' strip-private-attrs)))) @@ -94,7 +143,7 @@ (defn- validate-password [conn {:keys [user old-password] :as params}] (p/let [profile (get-profile conn user) - result (pwhash/verify old-password (:password profile))] + result (sodi.pwhash/verify old-password (:password profile))] (when-not (:valid result) (ex/raise :type :validation :code ::old-password-not-match)) @@ -125,7 +174,6 @@ ;; --- Mutation: Update Photo - (s/def :uxbox$upload/name ::us/string) (s/def :uxbox$upload/size ::us/integer) (s/def :uxbox$upload/mtype ::us/string) @@ -194,7 +242,7 @@ [conn {:keys [id username fullname email password metadata] :as params}] (let [id (or id (uuid/next)) metadata (blob/encode metadata) - password (pwhash/derive password) + password (sodi.pwhash/derive password) sqlv [create-user-sql id fullname @@ -209,19 +257,15 @@ [conn params] (-> (create-profile conn params) (p/then' strip-private-attrs) - #_(p/then (fn [profile] - (-> (emails/send! {::emails/id :users/register - ::emails/to (:email params) - ::emails/priority :high - :name (:fullname params)}) - (p/then' (constantly profile))))))) + (p/then (fn [profile] + (-> (emails/send! emails/register {:to (:email params) + :name (:fullname params)}) + (p/then' (constantly profile))))))) (s/def ::register-profile (s/keys :req-un [::username ::email ::password ::fullname])) -(sm/defmutation :register-profile - {:doc "Register new user." - :spec ::register-profile} +(sm/defmutation ::register-profile [params] (when-not (:registration-enabled cfg/config) (ex/raise :type :restriction @@ -231,115 +275,56 @@ (p/then (partial check-profile-existence! conn)) (p/then (partial register-profile conn))))) -;; --- Password Recover +;; --- Mutation: Request Profile Recovery -;; (defn- recovery-token-exists? -;; "Checks if the token exists in the system. Just -;; return `true` or `false`." -;; [conn token] -;; (let [sqlv (sql/recovery-token-exists? {:token token}) -;; result (db/fetch-one conn sqlv)] -;; (:token_exists result))) +(s/def ::request-profile-recovery + (s/keys :req-un [::username])) -;; (defn- retrieve-user-for-recovery-token -;; "Retrieve a user id (uuid) for the given token. If -;; no user is found, an exception is raised." -;; [conn token] -;; (let [sqlv (sql/get-recovery-token {:token token}) -;; data (db/fetch-one conn sqlv)] -;; (or (:user data) -;; (ex/raise :type :validation -;; :code ::invalid-token)))) +(su/defstr sql:insert-recovery-token + "insert into tokens (user_id, token) values ($1, $2)") -;; (defn- mark-token-as-used -;; [conn token] -;; (let [sqlv (sql/mark-recovery-token-used {:token token})] -;; (pos? (db/execute conn sqlv)))) +(sm/defmutation ::request-profile-recovery + [{:keys [username] :as params}] + (letfn [(create-recovery-token [conn {:keys [id] :as user}] + (let [token (-> (sodi.prng/random-bytes 32) + (sodi.util/bytes->b64s)) + sql sql:insert-recovery-token] + (-> (db/query-one conn [sql id token]) + (p/then (constantly (assoc user :token token)))))) + (send-email-notification [conn user] + (emails/send! emails/password-recovery + {:to (:email user) + :token (:token user) + :name (:fullname user)}))] + (db/with-atomic [conn db/pool] + (-> (retrieve-user conn username) + (p/then' su/raise-not-found-if-nil) + (p/then #(create-recovery-token conn %)) + (p/then #(send-email-notification conn %)) + (p/then (constantly nil)))))) -;; (defn- recover-password -;; "Given a token and password, resets the password -;; to corresponding user or raise an exception." -;; [conn {:keys [token password]}] -;; (let [user (retrieve-user-for-recovery-token conn token)] -;; (update-password conn {:user user :password password}) -;; (mark-token-as-used conn token) -;; nil)) +;; --- Mutation: Recover Profile -;; (defn- create-recovery-token -;; "Creates a new recovery token for specified user and return it." -;; [conn userid] -;; (let [token (token/random) -;; sqlv (sql/create-recovery-token {:user userid -;; :token token})] -;; (db/execute conn sqlv) -;; token)) +(s/def ::token ::us/not-empty-string) +(s/def ::recover-profile + (s/keys :req-un [::token ::password])) -;; (defn- retrieve-user-for-password-recovery -;; [conn username] -;; (let [user (find-user-by-username-or-email conn username)] -;; (when-not user -;; (ex/raise :type :validation :code ::user-does-not-exists)) -;; user)) - -;; (defn- request-password-recovery -;; "Creates a new recovery password token and sends it via email -;; to the correspondig to the given username or email address." -;; [conn username] -;; (let [user (retrieve-user-for-password-recovery conn username) -;; token (create-recovery-token conn (:id user))] -;; (emails/send! {:email/name :users/password-recovery -;; :email/to (:email user) -;; :name (:fullname user) -;; :token token}) -;; token)) - -;; (defmethod core/query :validate-profile-password-recovery-token -;; [{:keys [token]}] -;; (us/assert ::us/token token) -;; (with-open [conn (db/connection)] -;; (recovery-token-exists? conn token))) - -;; (defmethod core/novelty :request-profile-password-recovery -;; [{:keys [username]}] -;; (us/assert ::us/username username) -;; (with-open [conn (db/connection)] -;; (db/atomic conn -;; (request-password-recovery conn username)))) - -;; (s/def ::recover-password -;; (s/keys :req-un [::us/token ::us/password])) - -;; (defmethod core/novelty :recover-profile-password -;; [params] -;; (us/assert ::recover-password params) -;; (with-open [conn (db/connection)] -;; (db/apply-atomic conn recover-password params))) - -;; --- Query Helpers - -;; (defn find-full-user-by-id -;; "Find user by its id. This function is for internal -;; use only because it returns a lot of sensitive information. -;; If no user is found, `nil` is returned." -;; [conn id] -;; (let [sqlv (sql/get-profile {:id id})] -;; (some-> (db/fetch-one conn sqlv) -;; (data/normalize-attrs)))) - -;; (defn find-user-by-id -;; "Find user by its id. If no user is found, `nil` is returned." -;; [conn id] -;; (let [sqlv (sql/get-profile {:id id})] -;; (some-> (db/fetch-one conn sqlv) -;; (data/normalize-attrs) -;; (trim-user-attrs) -;; (dissoc :password)))) - -;; (defn find-user-by-username-or-email -;; "Finds a user in the database by username and email. If no -;; user is found, `nil` is returned." -;; [conn username] -;; (let [sqlv (sql/get-profile-by-username {:username username})] -;; (some-> (db/fetch-one conn sqlv) -;; (trim-user-attrs)))) +(su/defstr sql:remove-recovery-token + "delete from tokenes where user_id=$1 and token=$2") +(sm/defmutation ::recover-profile + [{:keys [token password]}] + (letfn [(validate-token [conn token] + (let [sql "delete from tokens where token=$1 returning *" + sql "select * from tokens where token=$1"] + (-> (db/query-one conn [sql token]) + (p/then' :user-id) + (p/then' su/raise-not-found-if-nil)))) + (update-password [conn user-id] + (let [sql "update users set password=$2 where id=$1" + pwd (sodi.pwhash/derive password)] + (-> (db/query-one conn [sql user-id pwd]) + (p/then' (constantly nil)))))] + (db/with-atomic [conn db/pool] + (-> (validate-token conn token) + (p/then (fn [user-id] (update-password conn user-id))))))) diff --git a/backend/src/uxbox/services/mutations/project_files.clj b/backend/src/uxbox/services/mutations/project_files.clj index fb8283a87..96bfe9c59 100644 --- a/backend/src/uxbox/services/mutations/project_files.clj +++ b/backend/src/uxbox/services/mutations/project_files.clj @@ -2,7 +2,10 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; Copyright (c) 2019 Andrey Antukh +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2019-2020 Andrey Antukh (ns uxbox.services.mutations.project-files (:require diff --git a/backend/tests/uxbox/tests/helpers.clj b/backend/tests/uxbox/tests/helpers.clj index 195b33f81..1d5ba0e54 100644 --- a/backend/tests/uxbox/tests/helpers.clj +++ b/backend/tests/uxbox/tests/helpers.clj @@ -5,7 +5,7 @@ [cuerdas.core :as str] [mount.core :as mount] [datoteka.storages :as st] - [uxbox.services.mutations.users :as users] + [uxbox.services.mutations.profile :as profile] [uxbox.services.mutations.projects :as projects] [uxbox.services.mutations.project-files :as files] [uxbox.services.mutations.project-pages :as pages] @@ -61,11 +61,11 @@ [prefix & args] (uuid/namespaced uuid/oid (apply str prefix args))) -;; --- Users creation +;; --- Profile creation (defn create-user [conn i] - (users/create-profile conn {:id (mk-uuid "user" i) + (profile/create-profile conn {:id (mk-uuid "user" i) :fullname (str "User " i) :username (str "user" i) :email (str "user" i ".test@uxbox.io") diff --git a/frontend/resources/locales.json b/frontend/resources/locales.json index e9473b471..0335ecf19 100644 --- a/frontend/resources/locales.json +++ b/frontend/resources/locales.json @@ -13,14 +13,58 @@ "fr" : "Mot de passe oublié ?" } }, - "auth.message.password-recovered" : { + + "profile.recovery.password-changed" : { "used-in" : [ "src/uxbox/main/data/auth.cljs:178" ], "translations" : { - "en" : "Password successfully recovered.", - "fr" : "Mot de passe récupéré avec succès." + "en" : "Password successfully changed", + "fr" : "TODO" } }, - "auth.message.recovery-token-sent" : { + + "profile.recovery.username-or-email": { + "translations" : { + "en" : "Username or Email Address", + "fr" : "adresse email ou nom d'utilisateur" + } + }, + + "profile.recovery.token": { + "translations" : { + "en" : "Recovery token (sent by email)", + "fr" : null + } + }, + + "profile.recovery.password": { + "translations" : { + "en" : "Type a new password", + "fr" : null + } + }, + + "profile.recovery.submit-request": { + "translations" : { + "en" : "Recover Password", + "fr" : null + } + }, + + "profile.recovery.submit-recover": { + "translations" : { + "en" : "Change your password", + "fr" : null + } + }, + + "profile.recovery.go-to-login": { + "translations" : { + "en" : "Go back!", + "fr" : "Retour!" + } + }, + + "profile.recovery.recovery-token-sent" : { "used-in" : [ "src/uxbox/main/data/auth.cljs:141" ], "translations" : { "en" : "Password recovery link sent to your inbox.", @@ -398,7 +442,7 @@ "fr" : "Une erreur inattendue c'est produite" } }, - "errors.auth.invalid-recovery-token" : { + "profile.recovery.invalid-token" : { "used-in" : [ "src/uxbox/main/data/auth.cljs:174", "src/uxbox/main/data/auth.cljs:151" ], "translations" : { "en" : "The recovery token is invalid.", @@ -468,28 +512,28 @@ "fr" : "Envoyer un fichier" } }, - "register.already-have-account" : { + "profile.register.already-have-account" : { "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:131" ], "translations" : { "en" : "Already have an account?", "fr" : "Vous avez déjà un compte ?" } }, - "register.fullname.placeholder" : { + "profile.register.fullname" : { "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:72" ], "translations" : { "en" : "Full Name", "fr" : "Nom complet" } }, - "register.get-started" : { + "profile.register.get-started" : { "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:127" ], "translations" : { "en" : "Get started", "fr" : "Commencer" } }, - "register.password.placeholder" : { + "profile.register.password" : { "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:115" ], "translations" : { "en" : "Password", @@ -615,7 +659,7 @@ "fr" : "Votre avatar" } }, - "settings.profile.your-email" : { + "profile.register.email" : { "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:111", "src/uxbox/main/ui/auth/register.cljs:101" ], "translations" : { "en" : "Your email", @@ -629,7 +673,7 @@ "fr" : "Votre nom complet" } }, - "settings.profile.your-username" : { + "profile.register.username" : { "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:98", "src/uxbox/main/ui/auth/register.cljs:87" ], "translations" : { "en" : "Your username", diff --git a/frontend/src/uxbox/main.cljs b/frontend/src/uxbox/main.cljs index f792c0194..a0e8e1582 100644 --- a/frontend/src/uxbox/main.cljs +++ b/frontend/src/uxbox/main.cljs @@ -2,6 +2,9 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; ;; Copyright (c) 2015-2019 Andrey Antukh (ns ^:figwheel-hooks uxbox.main diff --git a/frontend/src/uxbox/main/data/auth.cljs b/frontend/src/uxbox/main/data/auth.cljs index bdac80ea1..d72369579 100644 --- a/frontend/src/uxbox/main/data/auth.cljs +++ b/frontend/src/uxbox/main/data/auth.cljs @@ -2,7 +2,10 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; Copyright (c) 2015-2016 Andrey Antukh +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2015-2019 Andrey Antukh (ns uxbox.main.data.auth (:require @@ -10,7 +13,7 @@ [beicon.core :as rx] [potok.core :as ptk] [uxbox.common.spec :as us] - [uxbox.main.repo :as rp] + [uxbox.main.repo.core :as rp] [uxbox.main.store :refer [initial-state]] [uxbox.main.data.users :as du] [uxbox.util.messages :as um] @@ -57,8 +60,7 @@ :password password :scope "webapp"} on-error #(rx/of (um/error (tr "errors.auth.unauthorized")))] - (->> (rp/req :auth/login params) - (rx/map :payload) + (->> (rp/mutation :login params) (rx/map logged-in) (rx/catch rp/client-error? on-error)))))) @@ -72,7 +74,7 @@ ptk/WatchEvent (watch [_ state s] - (->> (rp/req :auth/logout) + (->> (rp/mutation :logout) (rx/ignore))) ptk/EffectEvent @@ -84,12 +86,12 @@ (ptk/reify ::logout ptk/WatchEvent (watch [_ state s] - (rx/of (rt/nav :auth/login) + (rx/of (rt/nav :login) clear-user-data)))) ;; --- Register -(s/def ::register-params +(s/def ::register (s/keys :req-un [::fullname ::username ::password @@ -98,84 +100,53 @@ (defn register "Create a register event instance." [data on-error] - (us/assert ::register-params data) - (us/assert fn? on-error) + (s/assert ::register data) + (s/assert fn? on-error) (ptk/reify ::register ptk/WatchEvent (watch [_ state stream] (letfn [(handle-error [{payload :payload}] (on-error payload) (rx/empty))] - (rx/merge - (->> (rp/req :auth/register data) - (rx/map :payload) - (rx/map (constantly ::registered)) - (rx/catch rp/client-error? handle-error)) - (->> stream - (rx/filter #(= % ::registered)) - (rx/take 1) - (rx/map #(login data)))))))) + (->> (rp/mutation :register-profile data) + (rx/map (fn [_] (login data))) + (rx/catch rp/client-error? handle-error)))))) ;; --- Recovery Request -(s/def ::recovery-request-params +(s/def ::recovery-request (s/keys :req-un [::username])) -(defn recovery-request - [data] - (us/assert ::recovery-request-params data) - (ptk/reify ::recover-request +(defn request-profile-recovery + [data on-success] + (us/assert ::recovery-request data) + (us/assert fn? on-success) + (ptk/reify ::request-profile-recovery ptk/WatchEvent (watch [_ state stream] (letfn [(on-error [{payload :payload}] - (println "on-error" payload) (rx/empty))] - (rx/merge - (->> (rp/req :auth/recovery-request data) - (rx/map (constantly ::recovery-requested)) - (rx/catch rp/client-error? on-error)) - (->> stream - (rx/filter #(= % ::recovery-requested)) - (rx/take 1) - ;; TODO: this should be moved to the UI part - (rx/map #(um/info (tr "auth.message.recovery-token-sent"))))))))) - -;; --- Check Recovery Token - -(defrecord ValidateRecoveryToken [token] - ptk/WatchEvent - (watch [_ state stream] - (letfn [(on-error [{payload :payload}] - (rx/of - (rt/navigate :auth/login) - (um/error (tr "errors.auth.invalid-recovery-token"))))] - (->> (rp/req :auth/validate-recovery-token token) - (rx/ignore) - (rx/catch rp/client-error? on-error))))) - -(defn validate-recovery-token - [token] - {:pre [(string? token)]} - (ValidateRecoveryToken. token)) + (->> (rp/mutation :request-profile-recovery data) + (rx/tap on-success) + (rx/catch rp/client-error? on-error)))))) ;; --- Recovery (Password) (s/def ::token string?) -(s/def ::recovery-params - (s/keys :req-un [::username ::token])) +(s/def ::on-error fn?) +(s/def ::on-success fn?) -(defn recovery - [{:keys [token password] :as data}] - (us/assert ::recovery-params data) - (ptk/reify ::recovery +(s/def ::recover-profile + (s/keys :req-un [::password ::token ::on-error ::on-success])) + +(defn recover-profile + [{:keys [token password on-error on-success] :as data}] + (us/assert ::recover-profile data) + (ptk/reify ::recover-profile ptk/WatchEvent (watch [_ state stream] - (letfn [(on-error [{payload :payload}] - (rx/of (um/error (tr "errors.auth.invalid-recovery-token")))) - (on-success [{payload :payload}] - (rx/of - (rt/navigate :auth/login) - (um/info (tr "auth.message.password-recovered"))))] - (->> (rp/req :auth/recovery {:token token :password password}) - (rx/mapcat on-success) - (rx/catch rp/client-error? on-error)))))) + (->> (rp/mutation :recover-profile {:token token :password password}) + (rx/tap on-success) + (rx/catch (fn [err] + (on-error) + (rx/empty))))))) diff --git a/frontend/src/uxbox/main/repo/core.cljs b/frontend/src/uxbox/main/repo/core.cljs index bc5086d54..0f4ad716b 100644 --- a/frontend/src/uxbox/main/repo/core.cljs +++ b/frontend/src/uxbox/main/repo/core.cljs @@ -133,3 +133,24 @@ (.append form (name key) val)) (seq params)) (send-mutation! id form))) + +(defmethod mutation :login + [id params] + (let [url (str url "/login")] + (->> (impl-send {:method :post :url url :body params}) + (rx/map conditional-decode) + (rx/mapcat handle-response)))) + +(defmethod mutation :logout + [id params] + (let [url (str url "/logout")] + (->> (impl-send {:method :post :url url :body params :auth false}) + (rx/map conditional-decode) + (rx/mapcat handle-response)))) + +;; (defmethod mutation :register-profile +;; [id params] +;; (let [url (str url "/register")] +;; (->> (impl-send {:method :post :url url :body params :auth false}) +;; (rx/map conditional-decode) +;; (rx/mapcat handle-response)))) diff --git a/frontend/src/uxbox/main/ui.cljs b/frontend/src/uxbox/main/ui.cljs index 85a03fa99..01621c908 100644 --- a/frontend/src/uxbox/main/ui.cljs +++ b/frontend/src/uxbox/main/ui.cljs @@ -2,6 +2,9 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; ;; Copyright (c) 2015-2017 Juan de la Cruz ;; Copyright (c) 2015-2019 Andrey Antukh @@ -16,7 +19,11 @@ [uxbox.main.data.auth :refer [logout]] [uxbox.main.data.projects :as dp] [uxbox.main.store :as st] - [uxbox.main.ui.auth :as auth] + [uxbox.main.ui.login :refer [login-page]] + [uxbox.main.ui.profile.register :refer [profile-register-page]] + [uxbox.main.ui.profile.recovery-request :refer [profile-recovery-request-page]] + [uxbox.main.ui.profile.recovery :refer [profile-recovery-page]] + [uxbox.main.ui.dashboard :as dashboard] [uxbox.main.ui.settings :as settings] [uxbox.main.ui.shapes] @@ -29,14 +36,18 @@ [uxbox.util.router :as rt] [uxbox.util.timers :as ts])) +(def route-iref + (-> (l/key :route) + (l/derive st/state))) + ;; --- Routes (def routes - [["/auth" - ["/login" :auth/login] - ["/register" :auth/register] - ["/recovery/request" :auth/recovery-request] - ["/recovery/token/:token" :auth/recovery]] + [["/login" :login] + ["/profile" + ["/register" :profile-register] + ["/recovery/request" :profile-recovery-request] + ["/recovery" :profile-recovery]] ["/settings" ["/profile" :settings/profile] @@ -51,58 +62,15 @@ ["/workspace/:file-id" :workspace]]) -;; --- Error Handling - -(defn- on-error - "A default error handler." - [{:keys [type code] :as error}] - (reset! st/loader false) - (cond - (and (map? error) - (= :validation type) - (= :spec-validation code)) - (do - (println "============ SERVER RESPONSE ERROR ================") - (println (:explain error)) - (println "============ END SERVER RESPONSE ERROR ================")) - - ;; Unauthorized or Auth timeout - (and (map? error) - (= :authentication type) - (= :unauthorized code)) - (ts/schedule 0 #(st/emit! (rt/nav :auth/login))) - - ;; Network error - (and (map? error) - (= :unexpected type) - (= :abort code)) - (ts/schedule 100 #(st/emit! (uum/error (tr "errors.network")))) - - ;; Something else - :else - (do - (js/console.error error) - (ts/schedule 100 #(st/emit! (uum/error (tr "errors.generic"))))))) - -(set! st/*on-error* on-error) - -;; --- Main App (Component) - -(def route-iref - (-> (l/key :route) - (l/derive st/state))) - (mf/defc app [props] (let [route (mf/deref route-iref)] (case (get-in route [:data :name]) - :auth/login (mf/element auth/login-page) - :auth/register (mf/element auth/register-page) + :login (mf/element login-page) - ;; :auth/recovery-request (auth/recovery-request-page) - ;; :auth/recovery - ;; (let [token (get-in route [:params :path :token])] - ;; (auth/recovery-page token)) + :profile-register (mf/element profile-register-page) + :profile-recovery-request (mf/element profile-recovery-request-page) + :profile-recovery (mf/element profile-recovery-page) (:settings/profile :settings/password @@ -125,3 +93,37 @@ :key file-id}]) nil))) +;; --- Error Handling + +(defn- on-error + "A default error handler." + [{:keys [type code] :as error}] + (reset! st/loader false) + (cond + (and (map? error) + (= :validation type) + (= :spec-validation code)) + (do + (println "============ SERVER RESPONSE ERROR ================") + (println (:explain error)) + (println "============ END SERVER RESPONSE ERROR ================")) + + ;; Unauthorized or Auth timeout + (and (map? error) + (= :authentication type) + (= :unauthorized code)) + (ts/schedule 0 #(st/emit! (rt/nav :login))) + + ;; Network error + (and (map? error) + (= :unexpected type) + (= :abort code)) + (ts/schedule 100 #(st/emit! (uum/error (tr "errors.network")))) + + ;; Something else + :else + (do + (js/console.error error) + (ts/schedule 100 #(st/emit! (uum/error (tr "errors.generic"))))))) + +(set! st/*on-error* on-error) diff --git a/frontend/src/uxbox/main/ui/auth.cljs b/frontend/src/uxbox/main/ui/auth.cljs deleted file mode 100644 index 72359a9ca..000000000 --- a/frontend/src/uxbox/main/ui/auth.cljs +++ /dev/null @@ -1,16 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) 2016 Andrey Antukh - -(ns uxbox.main.ui.auth - (:require [uxbox.main.ui.auth.login :as login] - [uxbox.main.ui.auth.register :as register] - #_[uxbox.main.ui.auth.recovery-request :as recovery-request] - #_[uxbox.main.ui.auth.recovery :as recovery])) - -(def login-page login/login-page) -(def register-page register/register-page) -;; (def recovery-page recovery/recovery-page) -;; (def recovery-request-page recovery-request/recovery-request-page) diff --git a/frontend/src/uxbox/main/ui/auth/recovery.cljs b/frontend/src/uxbox/main/ui/auth/recovery.cljs deleted file mode 100644 index daa9b7a09..000000000 --- a/frontend/src/uxbox/main/ui/auth/recovery.cljs +++ /dev/null @@ -1,82 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) 2015-2017 Juan de la Cruz -;; Copyright (c) 2015-2019 Andrey Antukh - -(ns uxbox.main.ui.auth.recovery - (:require - [cljs.spec.alpha :as s] - [cuerdas.core :as str] - [lentes.core :as l] - [rumext.core :as mx :include-macros true] - [uxbox.builtins.icons :as i] - [uxbox.main.data.auth :as uda] - [uxbox.main.store :as st] - [uxbox.main.ui.messages :refer [messages-widget]] - [uxbox.main.ui.navigation :as nav] - [uxbox.util.dom :as dom] - [uxbox.util.forms :as fm] - [uxbox.util.i18n :refer (tr)] - [uxbox.util.router :as rt])) - -;; (def form-data (fm/focus-data :recovery st/state)) -;; (def form-errors (fm/focus-errors :recovery st/state)) - -;; (def assoc-value (partial fm/assoc-value :recovery)) -;; (def assoc-errors (partial fm/assoc-errors :recovery)) -;; (def clear-form (partial fm/clear-form :recovery)) - -;; ;; --- Recovery Form - -;; (s/def ::password ::fm/non-empty-string) -;; (s/def ::recovery-form -;; (s/keys :req-un [::password])) - -;; (mx/defc recovery-form -;; {:mixins [mx/static mx/reactive]} -;; [token] -;; (let [data (merge (mx/react form-data) {:token token}) -;; valid? (fm/valid? ::recovery-form data)] -;; (letfn [(on-change [field event] -;; (let [value (dom/event->value event)] -;; (st/emit! (assoc-value field value)))) -;; (on-submit [event] -;; (dom/prevent-default event) -;; (st/emit! (uda/recovery data) -;; (clear-form)))] -;; [:form {:on-submit on-submit} -;; [:div.login-content -;; [:input.input-text -;; {:name "password" -;; :value (:password data "") -;; :on-change (partial on-change :password) -;; :placeholder (tr "recover.password.placeholder") -;; :type "password"}] -;; [:input.btn-primary -;; {:name "login" -;; :class (when-not valid? "btn-disabled") -;; :disabled (not valid?) -;; :value (tr "recover.recover-password") -;; :type "submit"}] -;; [:div.login-links -;; [:a {:on-click #(st/emit! (rt/navigate :auth/login))} (tr "recover.go-back")]]]]))) - -;; ;; --- Recovery Page - -;; (defn- recovery-page-init -;; [own] -;; (let [[token] (::mx/args own)] -;; (st/emit! (uda/validate-recovery-token token)) -;; own)) - -;; (mx/defc recovery-page -;; {:mixins [mx/static (fm/clear-mixin st/store :recovery)] -;; :init recovery-page-init} -;; [token] -;; [:div.login -;; [:div.login-body -;; (messages-widget) -;; [:a i/logo] -;; (recovery-form token)]]) diff --git a/frontend/src/uxbox/main/ui/auth/recovery_request.cljs b/frontend/src/uxbox/main/ui/auth/recovery_request.cljs deleted file mode 100644 index 156c7df6b..000000000 --- a/frontend/src/uxbox/main/ui/auth/recovery_request.cljs +++ /dev/null @@ -1,71 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) 2015-2017 Andrey Antukh -;; Copyright (c) 2015-2017 Juan de la Cruz - -(ns uxbox.main.ui.auth.recovery-request - (:require [cljs.spec.alpha :as s :include-macros true] - [lentes.core :as l] - [cuerdas.core :as str] - [uxbox.builtins.icons :as i] - [uxbox.main.store :as st] - [uxbox.main.data.auth :as uda] - [uxbox.main.ui.messages :refer [messages-widget]] - [uxbox.main.ui.navigation :as nav] - [uxbox.util.i18n :refer (tr)] - [uxbox.util.dom :as dom] - [uxbox.util.forms :as fm] - [rumext.core :as mx :include-macros true] - [uxbox.util.router :as rt])) - -;; (def form-data (fm/focus-data :recovery-request st/state)) -;; (def form-errors (fm/focus-errors :recovery-request st/state)) - -;; (def assoc-value (partial fm/assoc-value :profile-password)) -;; (def assoc-errors (partial fm/assoc-errors :profile-password)) -;; (def clear-form (partial fm/clear-form :profile-password)) - -;; (s/def ::username ::fm/non-empty-string) -;; (s/def ::recovery-request-form (s/keys :req-un [::username])) - -;; (mx/defc recovery-request-form -;; {:mixins [mx/static mx/reactive]} -;; [] -;; (let [data (mx/react form-data) -;; valid? (fm/valid? ::recovery-request-form data)] -;; (letfn [(on-change [event] -;; (let [value (dom/event->value event)] -;; (st/emit! (assoc-value :username value)))) -;; (on-submit [event] -;; (dom/prevent-default event) -;; (st/emit! (uda/recovery-request data) -;; (clear-form)))] -;; [:form {:on-submit on-submit} -;; [:div.login-content -;; [:input.input-text -;; {:name "username" -;; :value (:username data "") -;; :on-change on-change -;; :placeholder (tr "recovery-request.username-or-email.placeholder") -;; :type "text"}] -;; [:input.btn-primary -;; {:name "login" -;; :class (when-not valid? "btn-disabled") -;; :disabled (not valid?) -;; :value (tr "recovery-request.recover-password") -;; :type "submit"}] -;; [:div.login-links -;; [:a {:on-click #(st/emit! (rt/navigate :auth/login))} (tr "recovery-request.go-back")]]]]))) - -;; ;; --- Recovery Request Page - -;; (mx/defc recovery-request-page -;; {:mixins [mx/static (fm/clear-mixin st/store :recovery-request)]} -;; [] -;; [:div.login -;; [:div.login-body -;; (messages-widget) -;; [:a i/logo] -;; (recovery-request-form)]]) diff --git a/frontend/src/uxbox/main/ui/auth/login.cljs b/frontend/src/uxbox/main/ui/login.cljs similarity index 86% rename from frontend/src/uxbox/main/ui/auth/login.cljs rename to frontend/src/uxbox/main/ui/login.cljs index 31c654871..970e89fdb 100644 --- a/frontend/src/uxbox/main/ui/auth/login.cljs +++ b/frontend/src/uxbox/main/ui/login.cljs @@ -2,10 +2,13 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; Copyright (c) 2015-2016 Andrey Antukh -;; Copyright (c) 2015-2016 Juan de la Cruz +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2015-2020 Andrey Antukh +;; Copyright (c) 2015-2020 Juan de la Cruz -(ns uxbox.main.ui.auth.login +(ns uxbox.main.ui.login (:require [cljs.spec.alpha :as s] [rumext.alpha :as mf] @@ -78,10 +81,10 @@ :type "submit"}] [:div.login-links - [:a {:on-click #(st/emit! (rt/nav :auth/recovery-request)) + [:a {:on-click #(st/emit! (rt/nav :profile-recovery-request)) :tab-index "5"} (tr "auth.forgot-password")] - [:a {:on-click #(st/emit! (rt/nav :auth/register)) + [:a {:on-click #(st/emit! (rt/nav :profile-register)) :tab-index "6"} (tr "auth.no-account")]]]])) diff --git a/frontend/src/uxbox/main/ui/profile/recovery.cljs b/frontend/src/uxbox/main/ui/profile/recovery.cljs new file mode 100644 index 000000000..b07387661 --- /dev/null +++ b/frontend/src/uxbox/main/ui/profile/recovery.cljs @@ -0,0 +1,90 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2015-2017 Andrey Antukh +;; Copyright (c) 2015-2017 Juan de la Cruz + +(ns uxbox.main.ui.profile.recovery + (:require + [cljs.spec.alpha :as s] + [cuerdas.core :as str] + [lentes.core :as l] + [rumext.alpha :as mf] + [uxbox.builtins.icons :as i] + [uxbox.common.spec :as us] + [uxbox.main.data.auth :as uda] + [uxbox.main.store :as st] + [uxbox.main.ui.messages :refer [messages-widget]] + [uxbox.main.ui.navigation :as nav] + [uxbox.util.messages :as um] + [uxbox.util.dom :as dom] + [uxbox.util.forms :as fm] + [uxbox.util.i18n :as i18n] + [uxbox.util.router :as rt])) + +(s/def ::token ::us/not-empty-string) +(s/def ::password ::us/not-empty-string) +(s/def ::recovery-form (s/keys :req-un [::token ::password])) + +(mf/defc recovery-form + [] + (let [{:keys [data] :as form} (fm/use-form ::recovery-form {}) + tr (i18n/use-translations) + on-success + (fn [] + (st/emit! (um/info (tr "profile.recovery.password-changed")) + (rt/nav :login))) + + on-error + (fn [] + (st/emit! (um/error (tr "profile.recovery.invalid-token")))) + + on-submit + (fn [event] + (dom/prevent-default event) + (st/emit! (uda/recover-profile (assoc (:clean-data form) + :on-error on-error + :on-success on-success))))] + [:form {:on-submit on-submit} + [:div.login-content + [:input.input-text + {:name "token" + :value (:token data "") + :class (fm/error-class form :token) + :on-blur (fm/on-input-blur form :token) + :on-change (fm/on-input-change form :token) + :placeholder (tr "profile.recovery.token") + :auto-complete "off" + :type "text"}] + [:input.input-text + {:name "password" + :value (:password data "") + :class (fm/error-class form :password) + :on-blur (fm/on-input-blur form :password) + :on-change (fm/on-input-change form :password) + :placeholder (tr "profile.recovery.password") + :type "password"}] + [:input.btn-primary + {:name "recover" + :class (when-not (:valid form) "btn-disabled") + :disabled (not (:valid form)) + :value (tr "profile.recovery.submit-recover") + :type "submit"}] + + [:div.login-links + [:a {:on-click #(st/emit! (rt/nav :login))} + (tr "profile.recovery.go-to-login")]]]])) + +;; --- Recovery Request Page + +(mf/defc profile-recovery-page + [] + [:div.login + [:div.login-body + [:& messages-widget] + [:a i/logo] + [:& recovery-form]]]) diff --git a/frontend/src/uxbox/main/ui/profile/recovery_request.cljs b/frontend/src/uxbox/main/ui/profile/recovery_request.cljs new file mode 100644 index 000000000..315484038 --- /dev/null +++ b/frontend/src/uxbox/main/ui/profile/recovery_request.cljs @@ -0,0 +1,72 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2015-2017 Andrey Antukh +;; Copyright (c) 2015-2017 Juan de la Cruz + +(ns uxbox.main.ui.profile.recovery-request + (:require + [cljs.spec.alpha :as s] + [cuerdas.core :as str] + [lentes.core :as l] + [rumext.alpha :as mf] + [uxbox.builtins.icons :as i] + [uxbox.common.spec :as us] + [uxbox.main.data.auth :as uda] + [uxbox.main.store :as st] + [uxbox.main.ui.messages :refer [messages-widget]] + [uxbox.main.ui.navigation :as nav] + [uxbox.util.messages :as um] + [uxbox.util.dom :as dom] + [uxbox.util.forms :as fm] + [uxbox.util.i18n :as i18n] + [uxbox.util.router :as rt])) + +(s/def ::username ::us/not-empty-string) +(s/def ::recovery-request-form (s/keys :req-un [::username])) + +(mf/defc recovery-form + [] + (let [{:keys [data] :as form} (fm/use-form ::recovery-request-form {}) + tr (i18n/use-translations) + on-success + (fn [] + (st/emit! (um/info (tr "profile.recovery.recovery-token-sent")))) + on-submit + (fn [event] + (dom/prevent-default event) + (st/emit! (uda/request-profile-recovery (:clean-data form) on-success)))] + [:form {:on-submit on-submit} + [:div.login-content + [:input.input-text + {:name "username" + :value (:username data "") + :class (fm/error-class form :username) + :on-blur (fm/on-input-blur form :username) + :on-change (fm/on-input-change form :username) + :placeholder (tr "profile.recovery.username-or-email") + :type "text"}] + [:input.btn-primary + {:name "login" + :class (when-not (:valid form) "btn-disabled") + :disabled (not (:valid form)) + :value (tr "profile.recovery.submit-request") + :type "submit"}] + + [:div.login-links + [:a {:on-click #(st/emit! (rt/nav :login))} + (tr "profile.recovery.go-to-login")]]]])) + +;; --- Recovery Request Page + +(mf/defc profile-recovery-request-page + [] + [:div.login + [:div.login-body + [:& messages-widget] + [:a i/logo] + [:& recovery-form]]]) diff --git a/frontend/src/uxbox/main/ui/auth/register.cljs b/frontend/src/uxbox/main/ui/profile/register.cljs similarity index 89% rename from frontend/src/uxbox/main/ui/auth/register.cljs rename to frontend/src/uxbox/main/ui/profile/register.cljs index 226ec6f39..a111d0ad0 100644 --- a/frontend/src/uxbox/main/ui/auth/register.cljs +++ b/frontend/src/uxbox/main/ui/profile/register.cljs @@ -5,7 +5,7 @@ ;; Copyright (c) 2015-2017 Andrey Antukh ;; Copyright (c) 2015-2017 Juan de la Cruz -(ns uxbox.main.ui.auth.register +(ns uxbox.main.ui.profile.register (:require [cljs.spec.alpha :as s] [cuerdas.core :as str] @@ -69,7 +69,7 @@ :class (fm/error-class form :fullname) :on-blur (fm/on-input-blur form :fullname) :on-change (fm/on-input-change form :fullname) - :placeholder (tr "register.fullname.placeholder") + :placeholder (tr "profile.register.fullname") :type "text"}] [:& fm/field-error {:form form @@ -84,7 +84,7 @@ :on-blur (fm/on-input-blur form :username) :on-change (fm/on-input-change form :username) :value (:username data "") - :placeholder (tr "settings.profile.your-username")}] + :placeholder (tr "profile.register.username")}] [:& fm/field-error {:form form :type #{::api} @@ -98,7 +98,7 @@ :on-blur (fm/on-input-blur form :email) :on-change (fm/on-input-change form :email) :value (:email data "") - :placeholder (tr "settings.profile.your-email")}] + :placeholder (tr "profile.register.email")}] [:& fm/field-error {:form form :type #{::api} @@ -112,7 +112,7 @@ :class (fm/error-class form :password) :on-blur (fm/on-input-blur form :password) :on-change (fm/on-input-change form :password) - :placeholder (tr "register.password.placeholder") + :placeholder (tr "profile.register.password") :type "password"}] [:& fm/field-error {:form form @@ -124,15 +124,15 @@ :tab-index "5" :class (when-not (:valid form) "btn-disabled") :disabled (not (:valid form)) - :value (tr "register.get-started")}] + :value (tr "profile.register.get-started")}] [:div.login-links - [:a {:on-click #(st/emit! (rt/nav :auth/login))} - (tr "register.already-have-account")]]]])) + [:a {:on-click #(st/emit! (rt/nav :login))} + (tr "profile.register.already-have-account")]]]])) ;; --- Register Page -(mf/defc register-page +(mf/defc profile-register-page [props] [:div.login [:div.login-body diff --git a/frontend/src/uxbox/main/ui/settings/header.cljs b/frontend/src/uxbox/main/ui/settings/header.cljs index c347c42fc..bad306381 100644 --- a/frontend/src/uxbox/main/ui/settings/header.cljs +++ b/frontend/src/uxbox/main/ui/settings/header.cljs @@ -42,7 +42,7 @@ [:& header-link {:section :settings/notifications :content (tr "settings.notifications")}]] #_[:li {:on-click #(st/emit! (da/logout))} - [:& header-link {:section :auth/login + [:& header-link {:section :logout :content (tr "settings.exit")}]]] [:& user]]))