0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-13 16:21:57 -05:00

♻️ Refactor profile and login.

This commit is contained in:
Andrey Antukh 2020-02-04 16:05:51 +01:00
parent 841ace3aa8
commit 146faf74a9
26 changed files with 595 additions and 664 deletions

View file

@ -6,7 +6,6 @@ CREATE TABLE users (
deleted_at timestamptz NULL,
fullname text NOT NULL DEFAULT '',
username text NOT NULL,
email text NOT NULL,
photo text NOT NULL,
password text NOT NULL,
@ -15,10 +14,6 @@ CREATE TABLE users (
is_demo boolean NOT NULL DEFAULT false
);
CREATE UNIQUE INDEX users__username__idx
ON users (username)
WHERE deleted_at IS null;
CREATE UNIQUE INDEX users__email__idx
ON users (email)
WHERE deleted_at IS null;
@ -87,10 +82,9 @@ CREATE INDEX sessions__user_id__idx
-- Insert a placeholder system user.
INSERT INTO users (id, fullname, username, email, photo, password)
INSERT INTO users (id, fullname, email, photo, password)
VALUES ('00000000-0000-0000-0000-000000000000'::uuid,
'System User',
'00000000-0000-0000-0000-000000000000',
'system@uxbox.io',
'',
'!');

View file

@ -55,8 +55,8 @@
width 200
height 200}
:as opts}]
;; (us/verify ::thumbnail-opts opts)
(us/verify fs/path? input)
(us/assert ::thumbnail-opts opts)
(us/assert fs/path? input)
(let [ext (format->extension format)
tmp (fs/create-tempfile :suffix ext)
opr (doto (IMOperation.)
@ -80,6 +80,33 @@
(fs/delete tmp)
(ByteArrayInputStream. thumbnail-data)))))
(defn generate-thumbnail2
([input] (generate-thumbnail input nil))
([input {:keys [quality format width height]
:or {format "jpeg"
quality 92
width 200
height 200}
:as opts}]
(us/assert ::thumbnail-opts opts)
(us/assert fs/path? input)
(let [ext (format->extension format)
tmp (fs/create-tempfile :suffix ext)
opr (doto (IMOperation.)
(.addImage)
(.autoOrient)
(.strip)
(.thumbnail (int width) (int height) "^")
(.gravity "center")
(.extent (int width) (int height))
(.quality (double quality))
(.addImage))]
(doto (ConvertCmd.)
(.run opr (into-array (map str [input tmp]))))
(let [thumbnail-data (fs/slurp-bytes tmp)]
(fs/delete tmp)
(ByteArrayInputStream. thumbnail-data)))))
(defn info
[path]
(let [instance (Info. (str path))]
@ -96,3 +123,19 @@
row
(let [url (ust/public-uri media/media-storage value)]
(assoc-in row dst (str url))))))
(defn- resolve-uri
[storage row src dst]
(let [src (if (vector? src) src [src])
dst (if (vector? dst) dst [dst])
value (get-in row src)]
(if (empty? value)
row
(let [url (ust/public-uri media/media-storage value)]
(assoc-in row dst (str url))))))
(defn resolve-media-uris
[row & pairs]
(us/assert map? row)
(us/assert (s/coll-of vector?) pairs)
(reduce #(resolve-uri media/media-storage %1 (nth %2 0) (nth %2 1)) row pairs))

View file

@ -33,8 +33,8 @@
[vertx.core :as vc]))
(def sql:insert-user
"insert into users (id, fullname, username, email, password, photo, is_demo)
values ($1, $2, $3, $4, $5, '', true) returning *")
"insert into users (id, fullname, email, password, photo, is_demo)
values ($1, $2, $3, $4, '', true) returning *")
(def sql:insert-email
"insert into user_emails (user_id, email, is_main)
@ -44,14 +44,13 @@
[params]
(let [id (uuid/next)
sem (System/currentTimeMillis)
username (str "demo-" sem)
email (str username ".demo@uxbox.io")
email (str "demo-" sem ".demo@nodomain.com")
fullname (str "Demo User " sem)
password (-> (sodi.prng/random-bytes 12)
(sodi.util/bytes->b64s))
password' (sodi.pwhash/derive password)]
(db/with-atomic [conn db/pool]
(db/query-one conn [sql:insert-user id fullname username email password'])
(db/query-one conn [sql:insert-user id fullname email password'])
(db/query-one conn [sql:insert-email id email])
{:username username
{:email email
:password password})))

View file

@ -27,8 +27,10 @@
[uxbox.services.mutations :as sm]
[uxbox.services.util :as su]
[uxbox.services.queries.profile :as profile]
[uxbox.services.mutations.images :as imgs]
[uxbox.util.blob :as blob]
[uxbox.util.uuid :as uuid]
[uxbox.util.storage :as ust]
[vertx.core :as vc]))
;; --- Helpers & Specs
@ -36,36 +38,25 @@
(s/def ::email ::us/email)
(s/def ::fullname ::us/string)
(s/def ::lang ::us/string)
(s/def ::old-password ::us/string)
(s/def ::password ::us/string)
(s/def ::path ::us/string)
(s/def ::user ::us/uuid)
(s/def ::username ::us/string)
(s/def ::password ::us/string)
(s/def ::old-password ::us/string)
;; --- Utilities
(def 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)
(declare retrieve-user)
(s/def ::email ::us/email)
(s/def ::scope ::us/string)
(s/def ::login
(s/keys :req-un [::username ::password]
(s/keys :req-un [::email ::password]
:opt-un [::scope]))
(sm/defmutation ::login
[{:keys [username password scope] :as params}]
[{:keys [email password scope] :as params}]
(letfn [(check-password [user password]
(let [result (sodi.pwhash/verify password (:password user))]
(:valid result)))
@ -79,9 +70,20 @@
:code ::wrong-credentials))
{:id (:id user)})]
(-> (retrieve-user db/pool username)
(-> (retrieve-user db/pool email)
(p/then' check-user))))
(def sql:user-by-email
"select u.*
from users as u
where u.email=$1
and u.deleted_at is null")
(defn- retrieve-user
[conn email]
(db/query-one conn [sql:user-by-email email]))
;; --- Mutation: Add additional email
;; TODO
@ -93,65 +95,39 @@
;; --- Mutation: Update Profile (own)
(defn- check-username-and-email!
[conn {:keys [id username email] :as params}]
(let [sql1 "select exists
(select * from users
where username = $2
and id != $1
) as val"
sql2 "select exists
(select * from users
where email = $2
and id != $1
) as val"]
(p/let [res1 (db/query-one conn [sql1 id username])
res2 (db/query-one conn [sql2 id email])]
(when (:val res1)
(ex/raise :type :validation
:code ::username-already-exists))
(when (:val res2)
(ex/raise :type :validation
:code ::email-already-exists))
params)))
(def sql:update-profile
(def ^:private sql:update-profile
"update users
set username = $2,
fullname = $3,
lang = $4
set fullname = $2,
lang = $3
where id = $1
and deleted_at is null
returning *")
(defn- update-profile
[conn {:keys [id username fullname lang] :as params}]
(let [sqlv [sql:update-profile
id username fullname lang]]
[conn {:keys [id fullname lang] :as params}]
(let [sqlv [sql:update-profile id fullname lang]]
(-> (db/query-one conn sqlv)
(p/then' su/raise-not-found-if-nil)
(p/then' profile/strip-private-attrs))))
(s/def ::update-profile
(s/keys :req-un [::id ::username ::fullname ::lang]))
(s/keys :req-un [::id ::fullname ::lang]))
(sm/defmutation ::update-profile
[params]
(db/with-atomic [conn db/pool]
(-> (p/resolved params)
(p/then (partial check-username-and-email! conn))
(p/then (partial update-profile conn)))))
(update-profile conn params)))
;; --- Mutation: Update Password
(defn- validate-password
(defn- validate-password!
[conn {:keys [user old-password] :as params}]
(p/let [profile (profile/retrieve-profile conn user)
result (sodi.pwhash/verify old-password (:password profile))]
(when-not (:valid result)
(ex/raise :type :validation
:code ::old-password-not-match))
params))
:code ::old-password-not-match))))
(defn update-password
[conn {:keys [user password]}]
@ -159,99 +135,112 @@
set password = $2
where id = $1
and deleted_at is null
returning id"]
returning id"
password (sodi.pwhash/derive password)]
(-> (db/query-one conn [sql user password])
(p/then' su/raise-not-found-if-nil)
(p/then' su/constantly-nil))))
(s/def ::update-password
(s/keys :req-un [::user ::us/password ::old-password]))
(s/def ::update-profile-password
(s/keys :req-un [::user ::password ::old-password]))
(sm/defmutation :update-password
{:doc "Update self password."
:spec ::update-password}
(sm/defmutation ::update-profile-password
[params]
(db/with-atomic [conn db/pool]
(-> (p/resolved params)
(p/then (partial validate-password conn))
(p/then (partial update-password conn)))))
(validate-password! conn params)
(update-password conn params)))
;; --- Mutation: Update Photo
;; (s/def :uxbox$upload/name ::us/string)
;; (s/def :uxbox$upload/size ::us/integer)
;; (s/def :uxbox$upload/mtype ::us/string)
;; (s/def ::upload
;; (s/keys :req-un [:uxbox$upload/name
;; :uxbox$upload/size
;; :uxbox$upload/mtype]))
(declare upload-photo)
(declare update-profile-photo)
;; (s/def ::file ::upload)
;; (s/def ::update-profile-photo
;; (s/keys :req-un [::user ::file]))
(s/def ::file ::imgs/upload)
(s/def ::update-profile-photo
(s/keys :req-un [::user ::file]))
;; (def valid-image-types?
;; #{"image/jpeg", "image/png", "image/webp"})
(sm/defmutation ::update-profile-photo
[{:keys [user file] :as params}]
(db/with-atomic [conn db/pool]
;; TODO: send task for delete old photo
(-> (upload-photo conn params)
(p/then (partial update-profile-photo conn user)))))
;; (sm/defmutation ::update-profile-photo
;; [{:keys [user file] :as params}]
;; (letfn [(store-photo [{:keys [name path] :as upload}]
;; (let [filename (fs/name name)
;; storage media/media-storage]
;; (-> (ds/save storage filename path)
;; #_(su/handle-on-context))))
(defn- upload-photo
[conn {:keys [file user]}]
(when-not (imgs/valid-image-types? (:mtype file))
(ex/raise :type :validation
:code :image-type-not-allowed
:hint "Seems like you are uploading an invalid image."))
(vc/blocking
(let [thumb-opts {:width 256
:height 256
:quality 75
:format "webp"}
prefix (-> (sodi.prng/random-bytes 8)
(sodi.util/bytes->b64s))
name (str prefix ".webp")
photo (images/generate-thumbnail2 (fs/path (:path file)) thumb-opts)]
(ust/save! media/media-storage name photo))))
;; (update-user-photo [path]
;; (let [sql "update users
;; set photo = $1
;; where id = $2
;; and deleted_at is null
;; returning id, photo"]
;; (-> (db/query-one db/pool [sql (str path) user])
;; (p/then' su/raise-not-found-if-nil)
;; (p/then profile/resolve-thumbnail))))]
(defn- update-profile-photo
[conn user path]
(let [sql "update users set photo=$1 where id=$2 and deleted_at is null returning id"]
(-> (db/query-one conn [sql (str path) user])
(p/then' su/raise-not-found-if-nil))))
;; (when-not (valid-image-types? (:mtype file))
;; (ex/raise :type :validation
;; :code :image-type-not-allowed
;; :hint "Seems like you are uploading an invalid image."))
;; (-> (store-photo file)
;; (p/then update-user-photo))))
;; --- Mutation: Register Profile
(def sql:insert-user
"insert into users (id, fullname, username, email, password, photo)
values ($1, $2, $3, $4, $5, '') returning *")
(declare check-profile-existence!)
(declare register-profile)
(def sql:insert-email
(s/def ::register-profile
(s/keys :req-un [::email ::password ::fullname]))
(sm/defmutation ::register-profile
[params]
(when-not (:registration-enabled cfg/config)
(ex/raise :type :restriction
:code :registration-disabled))
(db/with-atomic [conn db/pool]
(check-profile-existence! conn params)
(register-profile conn params)))
(def ^:private sql:insert-user
"insert into users (id, fullname, email, password, photo)
values ($1, $2, $3, $4, '') returning *")
(def ^:private sql:insert-email
"insert into user_emails (user_id, email, is_main)
values ($1, $2, true)")
(def ^:private sql:profile-existence
"select exists (select * from users
where email = $1
and deleted_at is null) as val")
(defn- check-profile-existence!
[conn {:keys [username email] :as params}]
(let [sql "select exists
(select * from users
where username = $1
or email = $2
) as val"]
(-> (db/query-one conn [sql username email])
(p/then (fn [result]
(when (:val result)
(ex/raise :type :validation
:code ::username-or-email-already-exists))
params)))))
[conn {:keys [email] :as params}]
(-> (db/query-one conn [sql:profile-existence email])
(p/then' (fn [result]
(when (:val result)
(ex/raise :type :validation
:code ::email-already-exists))
params))))
(defn create-profile
"Create the user entry on the database with limited input
filling all the other fields with defaults."
[conn {:keys [id username fullname email password] :as params}]
[conn {:keys [id fullname email password] :as params}]
(let [id (or id (uuid/next))
password (sodi.pwhash/derive password)
sqlv1 [sql:insert-user id
fullname username
email password]
sqlv1 [sql:insert-user
id
fullname
email
password]
sqlv2 [sql:insert-email id email]]
(p/let [profile (db/query-one conn sqlv1)]
(db/query-one conn sqlv2)
@ -263,34 +252,22 @@
(p/then' profile/strip-private-attrs)
(p/then (fn [profile]
;; TODO: send a correct link for email verification
(p/let [data {:to (:email params)
:name (:fullname params)}]
(emails/send! emails/register data)
profile)))))
(s/def ::register-profile
(s/keys :req-un [::username ::email ::password ::fullname]))
(sm/defmutation ::register-profile
[params]
(when-not (:registration-enabled cfg/config)
(ex/raise :type :restriction
:code :registration-disabled))
(db/with-atomic [conn db/pool]
(-> (p/resolved params)
(p/then (partial check-profile-existence! conn))
(p/then (partial register-profile conn)))))
(let [data {:to (:email params)
:name (:fullname params)}]
(p/do!
(emails/send! emails/register data)
profile))))))
;; --- Mutation: Request Profile Recovery
(s/def ::request-profile-recovery
(s/keys :req-un [::username]))
(s/keys :req-un [::email]))
(def sql:insert-recovery-token
"insert into tokens (user_id, token) values ($1, $2)")
(sm/defmutation ::request-profile-recovery
[{:keys [username] :as params}]
[{:keys [email] :as params}]
(letfn [(create-recovery-token [conn {:keys [id] :as user}]
(let [token (-> (sodi.prng/random-bytes 32)
(sodi.util/bytes->b64s))
@ -298,12 +275,13 @@
(-> (db/query-one conn [sql id token])
(p/then (constantly (assoc user :token token))))))
(send-email-notification [conn user]
(emails/send! emails/password-recovery
(emails/send! conn
emails/password-recovery
{:to (:email user)
:token (:token user)
:name (:fullname user)}))]
(db/with-atomic [conn db/pool]
(-> (retrieve-user conn username)
(-> (retrieve-user conn email)
(p/then' su/raise-not-found-if-nil)
(p/then #(create-recovery-token conn %))
(p/then #(send-email-notification conn %))

View file

@ -31,16 +31,6 @@
;; --- Query: Profile (own)
;; (defn resolve-thumbnail
;; [user]
;; (let [opts {:src :photo
;; :dst :photo
;; :size [100 100]
;; :quality 90
;; :format "jpg"}]
;; (-> (px/submit! #(images/populate-thumbnails user opts))
;; (su/handle-on-context))))
(defn retrieve-profile
[conn id]
(let [sql "select * from users where id=$1 and deleted_at is null"]
@ -52,12 +42,12 @@
(sq/defquery ::profile
[{:keys [user] :as params}]
(-> (retrieve-profile db/pool user)
(p/then' strip-private-attrs)))
(p/then' strip-private-attrs)
(p/then' #(images/resolve-media-uris % [:photo :photo-uri]))))
;; --- Attrs Helpers
(defn strip-private-attrs
"Only selects a publicy visible user attrs."
[profile]
(select-keys profile [:id :username :fullname :metadata
:email :created-at :photo]))
(select-keys profile [:id :fullname :lang :email :created-at :photo]))

View file

@ -79,10 +79,10 @@
(p/then' (constantly nil))))
(defn- handle-task
[handlers {:keys [name] :as task}]
(let [task-fn (get handlers name)]
[tasks {:keys [name] :as item}]
(let [task-fn (get tasks name)]
(if task-fn
(task-fn task)
(task-fn item)
(do
(log/warn "no task handler found for" (pr-str name))
nil))))
@ -103,7 +103,7 @@
props (assoc :props (blob/decode props)))))
(defn- event-loop
[{:keys [handlers] :as options}]
[{:keys [tasks] :as options}]
(let [queue (:queue options "default")
max-retries (:max-retries options 3)]
(db/with-atomic [conn db/pool]
@ -111,7 +111,7 @@
(p/then decode-task-row)
(p/then (fn [item]
(when item
(-> (p/do! (handle-task handlers item))
(-> (p/do! (handle-task tasks item))
(p/handle (fn [v e]
(if e
(if (>= (:retry-num item) max-retries)

View file

@ -66,11 +66,10 @@
(defn create-user
[conn 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")
:password "123123"
:metadata {}}))
:fullname (str "User " i)
:email (str "user" i ".test@nodomain.com")
:password "123123"
:metadata {}}))
(defn create-project
[conn user-id i]

View file

@ -1,46 +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 <niwi@niwi.nz>
(ns uxbox.tests.test-services-auth
(:require
[clojure.test :as t]
[promesa.core :as p]
[uxbox.db :as db]
[uxbox.services.mutations :as sm]
[uxbox.tests.helpers :as th]))
(t/use-fixtures :once th/state-init)
(t/use-fixtures :each th/database-reset)
(t/deftest failed-auth
(let [user @(th/create-user db/pool 1)
event {:username "user1"
::sm/type :login
:password "foobar"
:metadata "1"
:scope "foobar"}
out (th/try-on! (sm/handle event))]
;; (th/print-result! out)
(let [error (:error out)]
(t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :service-error)))
(let [error (ex-cause (:error out))]
(t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :validation))
(t/is (th/ex-of-code? error :uxbox.services.mutations.profile/wrong-credentials)))))
(t/deftest success-auth
(let [user @(th/create-user db/pool 1)
event {:username "user1"
::sm/type :login
:password "123123"
:metadata "1"
:scope "foobar"}
out (th/try-on! (sm/handle event))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (= (get-in out [:result :id]) (:id user)))))

View file

@ -0,0 +1,151 @@
;; 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) 2019-2020 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.tests.test-services-profile
(:require
[clojure.test :as t]
[clojure.java.io :as io]
[promesa.core :as p]
[cuerdas.core :as str]
[datoteka.core :as fs]
[uxbox.db :as db]
[uxbox.services.mutations :as sm]
[uxbox.services.queries :as sq]
[uxbox.tests.helpers :as th]))
(t/use-fixtures :once th/state-init)
(t/use-fixtures :each th/database-reset)
(t/deftest login-with-failed-auth
(let [user @(th/create-user db/pool 1)
event {::sm/type :login
:email "user1.test@nodomain.com"
:password "foobar"
:scope "foobar"}
out (th/try-on! (sm/handle event))]
;; (th/print-result! out)
(let [error (:error out)]
(t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :service-error)))
(let [error (ex-cause (:error out))]
(t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :validation))
(t/is (th/ex-of-code? error :uxbox.services.mutations.profile/wrong-credentials)))))
(t/deftest login-with-success-auth
(let [user @(th/create-user db/pool 1)
event {::sm/type :login
:email "user1.test@nodomain.com"
:password "123123"
:scope "foobar"}
out (th/try-on! (sm/handle event))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (= (get-in out [:result :id]) (:id user)))))
(t/deftest query-profile
(let [user @(th/create-user db/pool 1)
data {::sq/type :profile
:user (:id user)}
out (th/try-on! (sq/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (= "User 1" (get-in out [:result :fullname])))
(t/is (= "user1.test@nodomain.com" (get-in out [:result :email])))
(t/is (not (contains? (:result out) :password)))))
(t/deftest mutation-update-profile
(let [user @(th/create-user db/pool 1)
data (assoc user
::sm/type :update-profile
:fullname "Full Name"
:username "user222"
:lang "en")
out (th/try-on! (sm/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (= (:fullname data) (get-in out [:result :fullname])))
(t/is (= (:email data) (get-in out [:result :email])))
(t/is (= (:metadata data) (get-in out [:result :metadata])))
(t/is (not (contains? (:result out) :password)))))
(t/deftest mutation-update-profile-photo
(let [user @(th/create-user db/pool 1)
data {::sm/type :update-profile-photo
:user (:id user)
:file {:name "sample.jpg"
:path "tests/uxbox/tests/_files/sample.jpg"
:size 123123
:mtype "image/jpeg"}}
out (th/try-on! (sm/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (= (:id user) (get-in out [:result :id])))))
;; (t/deftest test-mutation-register-profile
;; (let[data {:fullname "Full Name"
;; :username "user222"
;; :email "user222@uxbox.io"
;; :password "user222"
;; ::sv/type :register-profile}
;; [err rsp] (th/try-on (sm/handle data))]
;; (println "RESPONSE:" err rsp)))
;; (t/deftest test-http-validate-recovery-token
;; (with-open [conn (db/connection)]
;; (let [user (th/create-user conn 1)]
;; (with-server {:handler (uft/routes)}
;; (let [token (#'usu/request-password-recovery conn "user1")
;; uri1 (str th/+base-url+ "/api/auth/recovery/not-existing")
;; uri2 (str th/+base-url+ "/api/auth/recovery/" token)
;; [status1 data1] (th/http-get user uri1)
;; [status2 data2] (th/http-get user uri2)]
;; ;; (println "RESPONSE:" status1 data1)
;; ;; (println "RESPONSE:" status2 data2)
;; (t/is (= 404 status1))
;; (t/is (= 204 status2)))))))
;; (t/deftest test-http-request-password-recovery
;; (with-open [conn (db/connection)]
;; (let [user (th/create-user conn 1)
;; sql "select * from user_pswd_recovery"
;; res (sc/fetch-one conn sql)]
;; ;; Initially no tokens exists
;; (t/is (nil? res))
;; (with-server {:handler (uft/routes)}
;; (let [uri (str th/+base-url+ "/api/auth/recovery")
;; data {:username "user1"}
;; [status data] (th/http-post user uri {:body data})]
;; ;; (println "RESPONSE:" status data)
;; (t/is (= 204 status)))
;; (let [res (sc/fetch-one conn sql)]
;; (t/is (not (nil? res)))
;; (t/is (= (:user res) (:id user))))))))
;; (t/deftest test-http-validate-recovery-token
;; (with-open [conn (db/connection)]
;; (let [user (th/create-user conn 1)]
;; (with-server {:handler (uft/routes)}
;; (let [token (#'usu/request-password-recovery conn (:username user))
;; uri (str th/+base-url+ "/api/auth/recovery")
;; data {:token token :password "mytestpassword"}
;; [status data] (th/http-put user uri {:body data})
;; user' (usu/find-full-user-by-id conn (:id user))]
;; (t/is (= status 204))
;; (t/is (hashers/check "mytestpassword" (:password user'))))))))

View file

@ -1,115 +0,0 @@
(ns uxbox.tests.test-services-users
(:require
[clojure.test :as t]
[clojure.java.io :as io]
[promesa.core :as p]
[cuerdas.core :as str]
[datoteka.core :as fs]
[uxbox.db :as db]
[uxbox.services.mutations :as sm]
[uxbox.services.queries :as sq]
[uxbox.tests.helpers :as th]))
(t/use-fixtures :once th/state-init)
(t/use-fixtures :each th/database-reset)
(t/deftest test-query-profile
(let [user @(th/create-user db/pool 1)
data {::sq/type :profile
:user (:id user)}
out (th/try-on! (sq/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (= "User 1" (get-in out [:result :fullname])))
(t/is (= "user1" (get-in out [:result :username])))
(t/is (= "user1.test@uxbox.io" (get-in out [:result :email])))
(t/is (not (contains? (:result out) :password)))))
(t/deftest test-mutation-update-profile
(let [user @(th/create-user db/pool 1)
data (assoc user
::sm/type :update-profile
:fullname "Full Name"
:username "user222"
:lang "en")
out (th/try-on! (sm/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (= (:fullname data) (get-in out [:result :fullname])))
(t/is (= (:username data) (get-in out [:result :username])))
(t/is (= (:email data) (get-in out [:result :email])))
(t/is (= (:metadata data) (get-in out [:result :metadata])))
(t/is (not (contains? (:result out) :password)))))
;; (t/deftest test-mutation-update-profile-photo
;; (let [user @(th/create-user db/pool 1)
;; data {::sm/type :update-profile-photo
;; :user (:id user)
;; :file {:name "sample.jpg"
;; :path (fs/path "test/uxbox/tests/_files/sample.jpg")
;; :size 123123
;; :mtype "image/jpeg"}}
;; out (th/try-on! (sm/handle data))]
;; ;; (th/print-result! out)
;; (t/is (nil? (:error out)))
;; (t/is (= (:id user) (get-in out [:result :id])))
;; (t/is (str/starts-with? (get-in out [:result :photo]) "http"))))
;; (t/deftest test-mutation-register-profile
;; (let[data {:fullname "Full Name"
;; :username "user222"
;; :email "user222@uxbox.io"
;; :password "user222"
;; ::sv/type :register-profile}
;; [err rsp] (th/try-on (sm/handle data))]
;; (println "RESPONSE:" err rsp)))
;; ;; (t/deftest test-http-validate-recovery-token
;; ;; (with-open [conn (db/connection)]
;; ;; (let [user (th/create-user conn 1)]
;; ;; (with-server {:handler (uft/routes)}
;; ;; (let [token (#'usu/request-password-recovery conn "user1")
;; ;; uri1 (str th/+base-url+ "/api/auth/recovery/not-existing")
;; ;; uri2 (str th/+base-url+ "/api/auth/recovery/" token)
;; ;; [status1 data1] (th/http-get user uri1)
;; ;; [status2 data2] (th/http-get user uri2)]
;; ;; ;; (println "RESPONSE:" status1 data1)
;; ;; ;; (println "RESPONSE:" status2 data2)
;; ;; (t/is (= 404 status1))
;; ;; (t/is (= 204 status2)))))))
;; ;; (t/deftest test-http-request-password-recovery
;; ;; (with-open [conn (db/connection)]
;; ;; (let [user (th/create-user conn 1)
;; ;; sql "select * from user_pswd_recovery"
;; ;; res (sc/fetch-one conn sql)]
;; ;; ;; Initially no tokens exists
;; ;; (t/is (nil? res))
;; ;; (with-server {:handler (uft/routes)}
;; ;; (let [uri (str th/+base-url+ "/api/auth/recovery")
;; ;; data {:username "user1"}
;; ;; [status data] (th/http-post user uri {:body data})]
;; ;; ;; (println "RESPONSE:" status data)
;; ;; (t/is (= 204 status)))
;; ;; (let [res (sc/fetch-one conn sql)]
;; ;; (t/is (not (nil? res)))
;; ;; (t/is (= (:user res) (:id user))))))))
;; ;; (t/deftest test-http-validate-recovery-token
;; ;; (with-open [conn (db/connection)]
;; ;; (let [user (th/create-user conn 1)]
;; ;; (with-server {:handler (uft/routes)}
;; ;; (let [token (#'usu/request-password-recovery conn (:username user))
;; ;; uri (str th/+base-url+ "/api/auth/recovery")
;; ;; data {:token token :password "mytestpassword"}
;; ;; [status data] (th/http-put user uri {:body data})
;; ;; user' (usu/find-full-user-by-id conn (:id user))]
;; ;; (t/is (= status 204))
;; ;; (t/is (hashers/check "mytestpassword" (:password user'))))))))

View file

@ -34,8 +34,7 @@
"fr" : "+ Nouvelle couleur"
}
},
"ds.colors" : {
"used-in" : [ "src/uxbox/main/ui/dashboard/header.cljs:48" ],
"dashboard.header.colors" : {
"translations" : {
"en" : "COLORS",
"fr" : "COULEURS"
@ -112,8 +111,7 @@
"fr" : "+ Nouvel icône"
}
},
"ds.icons" : {
"used-in" : [ "src/uxbox/main/ui/dashboard/header.cljs:42" ],
"dashboard.header.icons" : {
"translations" : {
"en" : "ICONS",
"fr" : "ICÔNES"
@ -133,8 +131,7 @@
"fr" : "+ Nouvelle image"
}
},
"ds.images" : {
"used-in" : [ "src/uxbox/main/ui/dashboard/header.cljs:45" ],
"dashboard.header.images" : {
"translations" : {
"en" : "IMAGES",
"fr" : "IMAGES"
@ -210,8 +207,7 @@
},
"unused" : true
},
"ds.projects" : {
"used-in" : [ "src/uxbox/main/ui/dashboard/header.cljs:39" ],
"dashboard.header.projects" : {
"translations" : {
"en" : "PROJECTS",
"fr" : "PROJETS"
@ -287,8 +283,7 @@
"fr" : "Mise en ligne : %s"
}
},
"ds.user.exit" : {
"used-in" : [ "src/uxbox/main/ui/users.cljs:43" ],
"dashboard.header.user-menu.logout" : {
"translations" : {
"en" : "Exit",
"fr" : "Quitter"
@ -301,15 +296,13 @@
"fr" : "Notifications"
}
},
"ds.user.password" : {
"used-in" : [ "src/uxbox/main/ui/users.cljs:37" ],
"dashboard.header.user-menu.password" : {
"translations" : {
"en" : "Password",
"fr" : "Mot de passe"
}
},
"ds.user.profile" : {
"used-in" : [ "src/uxbox/main/ui/users.cljs:34" ],
"dashboard.header.user-menu.profile" : {
"translations" : {
"en" : "Profile",
"fr" : "Profil"
@ -441,11 +434,11 @@
"fr" : null
}
},
"login.email-or-username" : {
"login.email" : {
"used-in" : [ "src/uxbox/main/ui/login.cljs:63" ],
"translations" : {
"en" : "Email or Username",
"fr" : "adresse email ou nom d'utilisateur"
"en" : "Email",
"fr" : "adresse email"
}
},
"login.forgot-password" : {
@ -532,11 +525,11 @@
"fr" : null
}
},
"profile.recovery.username-or-email" : {
"profile.recovery.email" : {
"used-in" : [ "src/uxbox/main/ui/profile/recovery_request.cljs:54" ],
"translations" : {
"en" : "Username or Email Address",
"fr" : "adresse email ou nom d'utilisateur"
"en" : "Email Address",
"fr" : "adresse email"
}
},
"profile.register.already-have-account" : {
@ -574,13 +567,6 @@
"fr" : "Mot de passe"
}
},
"profile.register.username" : {
"used-in" : [ "src/uxbox/main/ui/profile/register.cljs:87" ],
"translations" : {
"en" : "Your username",
"fr" : "Votre nom d'utilisateur"
}
},
"settings.exit" : {
"used-in" : [ "src/uxbox/main/ui/settings/header.cljs:46" ],
"translations" : {
@ -693,7 +679,7 @@
"fr" : "Nom, nom d'utilisateur et adresse email"
}
},
"settings.profile.section-i18n-data" : {
"settings.profile.lang" : {
"used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:117" ],
"translations" : {
"en" : "Default language",

View file

@ -21,10 +21,9 @@
[uxbox.util.i18n :as i18n :refer [tr]]
[uxbox.util.storage :refer [storage]]))
(s/def ::username string?)
(s/def ::email ::us/email)
(s/def ::password string?)
(s/def ::fullname string?)
(s/def ::email ::us/email)
;; --- Logged In
@ -44,10 +43,10 @@
;; --- Login
(s/def ::login-params
(s/keys :req-un [::username ::password]))
(s/keys :req-un [::email ::password]))
(defn login
[{:keys [username password] :as data}]
[{:keys [email password] :as data}]
(us/verify ::login-params data)
(ptk/reify ::login
ptk/UpdateEvent
@ -56,7 +55,7 @@
ptk/WatchEvent
(watch [this state s]
(let [params {:username username
(let [params {:email email
:password password
:scope "webapp"}
on-error #(rx/of (um/error (tr "errors.auth.unauthorized")))]
@ -93,7 +92,6 @@
(s/def ::register
(s/keys :req-un [::fullname
::username
::password
::email]))
@ -115,7 +113,7 @@
;; --- Recovery Request
(s/def ::recovery-request
(s/keys :req-un [::username]))
(s/keys :req-un [::email]))
(defn request-profile-recovery
[data on-success]

View file

@ -19,7 +19,6 @@
;; --- Common Specs
(s/def ::id uuid?)
(s/def ::username string?)
(s/def ::fullname string?)
(s/def ::email ::us/email)
(s/def ::password string?)
@ -30,19 +29,18 @@
(s/def ::password-2 string?)
(s/def ::password-old string?)
;; --- Profile Fetched
(s/def ::profile-fetched
(s/def ::profile
(s/keys :req-un [::id
::username
::fullname
::email
::created-at
::photo]))
;; --- Profile Fetched
(defn profile-fetched
[data]
(us/verify ::profile-fetched data)
(us/verify ::profile data)
(ptk/reify ::profile-fetched
ptk/UpdateEvent
(update [_ state]
@ -51,7 +49,7 @@
ptk/EffectEvent
(effect [_ state stream]
(swap! storage assoc :profile data)
(when-let [lang (get-in data [:metadata :language])]
(when-let [lang (:lang data)]
(i18n/set-current-locale! lang)))))
;; --- Fetch Profile
@ -65,73 +63,56 @@
;; --- Update Profile
(s/def ::update-profile-params
(s/keys :req-un [::fullname
::email
::username
::language]))
(defn form->update-profile
[data on-success on-error]
(us/verify ::update-profile-params data)
(us/verify fn? on-error)
(us/verify fn? on-success)
(reify
(defn update-profile
[data]
(us/assert ::profile data)
(ptk/reify ::update-profile
ptk/WatchEvent
(watch [_ state s]
(letfn [(handle-error [{payload :payload}]
(on-error payload)
(rx/empty))]
(let [data (-> (:profile state)
(assoc :fullname (:fullname data))
(assoc :email (:email data))
(assoc :username (:username data))
(assoc-in [:metadata :language] (:language data)))]
#_(->> (rp/req :update/profile data)
(rx/map :payload)
(rx/do on-success)
(rx/map profile-fetched)
(rx/catch rp/client-error? handle-error)))))))
(let [mdata (meta data)
on-success (:on-success mdata identity)
on-error (:on-error mdata identity)
handle-error #(do (on-error (:payload %))
(rx/empty))]
(->> (rp/mutation :update-profile data)
(rx/do on-success)
(rx/map profile-fetched)
(rx/catch rp/client-error? handle-error))))))
;; --- Update Password (Form)
(s/def ::update-password-params
(s/def ::update-password
(s/keys :req-un [::password-1
::password-2
::password-old]))
(defn update-password
[data {:keys [on-success on-error]}]
(us/verify ::update-password-params data)
(us/verify fn? on-success)
(us/verify fn? on-error)
(reify
[data]
(us/verify ::update-password data)
(ptk/reify ::update-password
ptk/WatchEvent
(watch [_ state s]
(let [params {:old-password (:password-old data)
(let [mdata (meta data)
on-success (:on-success mdata identity)
on-error (:on-error mdata identity)
params {:old-password (:password-old data)
:password (:password-1 data)}]
#_(->> (rp/req :update/profile-password params)
(rx/catch rp/client-error? (fn [e]
(on-error (:payload e))
(rx/empty)))
(->> (rp/mutation :update-profile-password params)
(rx/catch rp/client-error? #(do (on-error (:payload %))
(rx/empty)))
(rx/do on-success)
(rx/ignore))))))
;; --- Update Photo
(deftype UpdatePhoto [file done]
ptk/WatchEvent
(watch [_ state stream]
#_(->> (rp/req :update/profile-photo {:file file})
(rx/do done)
(rx/map (constantly fetch-profile)))))
;; --- Update Photoo
(s/def ::file #(instance? js/File %))
(defn update-photo
([file] (update-photo file (constantly nil)))
([file done]
(us/verify ::file file)
(us/verify fn? done)
(UpdatePhoto. file done)))
[{:keys [file] :as params}]
(us/verify ::file file)
(ptk/reify ::update-photo
ptk/WatchEvent
(watch [_ state stream]
(->> (rp/mutation :update-profile-photo {:file file})
(rx/map (constantly fetch-profile))))))

View file

@ -128,6 +128,14 @@
(seq params))
(send-mutation! id form)))
(defmethod mutation :update-profile-photo
[id params]
(let [form (js/FormData.)]
(run! (fn [[key val]]
(.append form (name key) val))
(seq params))
(send-mutation! id form)))
(defmethod mutation :login
[id params]
(let [url (str url "/api/login")]

View file

@ -6,7 +6,7 @@
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2015-2017 Juan de la Cruz <delacruzgarciajuan@gmail.com>
;; Copyright (c) 2015-2019 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2015-2020 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.main.ui
(:require
@ -44,15 +44,13 @@
(def routes
[["/login" :login]
["/profile"
["/register" :profile-register]
["/recovery/request" :profile-recovery-request]
["/recovery" :profile-recovery]]
["/register" :profile-register]
["/recovery/request" :profile-recovery-request]
["/recovery" :profile-recovery]
["/settings"
["/profile" :settings/profile]
["/password" :settings/password]
["/notifications" :settings/notifications]]
["/profile" :settings-profile]
["/password" :settings-password]]
["/dashboard"
["/projects" :dashboard-projects]
@ -72,9 +70,8 @@
:profile-recovery-request (mf/element profile-recovery-request-page)
:profile-recovery (mf/element profile-recovery-page)
(:settings/profile
:settings/password
:settings/notifications)
(:settings-profile
:settings-password)
(mf/element settings/settings #js {:route route})
:dashboard-projects

View file

@ -2,22 +2,28 @@
;; 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 <niwi@niwi.nz>
;; Copyright (c) 2015-2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
;; 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 <niwi@niwi.nz>
;; Copyright (c) 2015-2020 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(ns uxbox.main.ui.dashboard.header
(:require
[cuerdas.core :as str]
[lentes.core :as l]
[rumext.core :as mx]
[rumext.alpha :as mf]
[uxbox.builtins.icons :as i]
[uxbox.main.data.auth :as da]
[uxbox.main.data.projects :as dp]
[uxbox.main.store :as st]
[uxbox.main.ui.navigation :as nav]
[uxbox.main.ui.users :refer [user]]
[uxbox.util.i18n :refer (tr)]
[uxbox.util.dom :as dom]
[uxbox.util.i18n :as i18n :refer [t]]
[uxbox.util.router :as rt]))
(declare user)
(mf/defc header-link
[{:keys [section content] :as props}]
(let [on-click #(st/emit! (rt/nav section))]
@ -25,7 +31,8 @@
(mf/defc header
[{:keys [section] :as props}]
(let [projects? (= section :dashboard-projects)
(let [locale (i18n/use-locale)
projects? (= section :dashboard-projects)
icons? (= section :dashboard-icons)
images? (= section :dashboard-images)
colors? (= section :dashboard-colors)]
@ -36,16 +43,61 @@
[:ul.main-nav
[:li {:class (when projects? "current")}
[:& header-link {:section :dashboard-projects
:content (tr "ds.projects")}]]
:content (t locale "dashboard.header.projects")}]]
[:li {:class (when icons? "current")}
[:& header-link {:section :dashboard-icons
:content (tr "ds.icons")}]]
:content (t locale "dashboard.header.icons")}]]
[:li {:class (when images? "current")}
[:& header-link {:section :dashboard-images
:content (tr "ds.images")}]]
:content (t locale "dashboard.header.images")}]]
[:li {:class (when colors? "current")}
[:& header-link {:section :dashboard-colors
:content (tr "ds.colors")}]]]
:content (t locale "dashboard.header.colors")}]]]
[:& user]]))
;; --- User Widget
(declare user-menu)
(def profile-ref
(-> (l/key :profile)
(l/derive st/state)))
(mf/defc user
[props]
(let [open (mf/use-state false)
profile (mf/deref profile-ref)
photo (:photo-uri profile "")
photo (if (str/empty? photo)
"/images/avatar.jpg"
photo)]
[:div.user-zone {:on-click #(st/emit! (rt/nav :settings-profile))
:on-mouse-enter #(reset! open true)
:on-mouse-leave #(reset! open false)}
[:span (:fullname profile)]
[:img {:src photo}]
(when @open
[:& user-menu])]))
;; --- User Menu
(mf/defc user-menu
[props]
(let [locale (i18n/use-locale)
on-click
(fn [event section]
(dom/stop-propagation event)
(if (keyword? section)
(st/emit! (rt/nav section))
(st/emit! section)))]
[:ul.dropdown
[:li {:on-click #(on-click % :settings-profile)}
i/user
[:span (t locale "dashboard.header.user-menu.profile")]]
[:li {:on-click #(on-click % :settings-password)}
i/lock
[:span (t locale "dashboard.header.user-menu.password")]]
[:li {:on-click #(on-click % da/logout)}
i/exit
[:span (t locale "dashboard.header.user-menu.logout")]]]))

View file

@ -23,18 +23,17 @@
[uxbox.util.i18n :refer [tr]]
[uxbox.util.router :as rt]))
(s/def ::username ::us/not-empty-string)
(s/def ::email ::us/email)
(s/def ::password ::us/not-empty-string)
(s/def ::login-form
(s/keys :req-un [::username ::password]))
(s/keys :req-un [::email ::password]))
(defn- on-submit
[event form]
(dom/prevent-default event)
(let [{:keys [username password]} (:clean-data form)]
(st/emit! (da/login {:username username
:password password}))))
(let [{:keys [email password]} (:clean-data form)]
(st/emit! (da/login {:email email :password password}))))
(mf/defc demo-warning
[_]
@ -54,13 +53,13 @@
[:& demo-warning])
[:input.input-text
{:name "username"
{:name "email"
:tab-index "2"
: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 "login.email-or-username")
:value (:email data "")
:class (fm/error-class form :email)
:on-blur (fm/on-input-blur form :email)
:on-change (fm/on-input-change form :email)
:placeholder (tr "login.email")
:type "text"}]
[:input.input-text
{:name "password"

View file

@ -26,8 +26,8 @@
[uxbox.util.i18n :as i18n :refer [t]]
[uxbox.util.router :as rt]))
(s/def ::username ::us/not-empty-string)
(s/def ::recovery-request-form (s/keys :req-un [::username]))
(s/def ::email ::us/email)
(s/def ::recovery-request-form (s/keys :req-un [::email]))
(mf/defc recovery-form
[]
@ -46,12 +46,12 @@
[: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 (t locale "profile.recovery.username-or-email")
{:name "email"
:value (:email data "")
:class (fm/error-class form :email)
:on-blur (fm/on-input-blur form :email)
:on-change (fm/on-input-change form :email)
:placeholder (t locale "profile.recovery.email")
:type "text"}]
[:input.btn-primary
{:name "login"

View file

@ -21,14 +21,12 @@
[uxbox.util.i18n :refer [tr]]
[uxbox.util.router :as rt]))
(s/def ::username ::fm/not-empty-string)
(s/def ::fullname ::fm/not-empty-string)
(s/def ::password ::fm/not-empty-string)
(s/def ::email ::fm/email)
(s/def ::register-form
(s/keys :req-un [::username
::password
(s/keys :req-un [::password
::fullname
::email]))
@ -43,11 +41,6 @@
{:type ::api
:message "errors.api.form.email-already-exists"})
:uxbox.services.users/username-already-exists
(swap! form assoc-in [:errors :username]
{:type ::api
:message "errors.api.form.username-already-exists"})
(st/emit! (tr "errors.api.form.unexpected-error"))))
(defn- on-submit
@ -76,20 +69,6 @@
:type #{::api}
:field :fullname}]
[:input.input-text
{:type "text"
:name "username"
:tab-index "2"
:class (fm/error-class form :username)
:on-blur (fm/on-input-blur form :username)
:on-change (fm/on-input-change form :username)
:value (:username data "")
:placeholder (tr "profile.register.username")}]
[:& fm/field-error {:form form
:type #{::api}
:field :username}]
[:input.input-text
{:type "email"
:name "email"

View file

@ -13,9 +13,8 @@
[uxbox.builtins.icons :as i]
[uxbox.main.ui.messages :refer [messages-widget]]
[uxbox.main.ui.settings.header :refer [header]]
[uxbox.main.ui.settings.notifications :as notifications]
[uxbox.main.ui.settings.password :as password]
[uxbox.main.ui.settings.profile :as profile]))
[uxbox.main.ui.settings.password :refer [password-page]]
[uxbox.main.ui.settings.profile :refer [profile-page]]))
(mf/defc settings
{:wrap [mf/wrap-memo]}
@ -25,9 +24,8 @@
[:& messages-widget]
[:& header {:section section}]
(case section
:settings/profile (mf/element profile/profile-page)
:settings/password (mf/element password/password-page)
:settings/notifications (mf/element notifications/notifications-page))]))
:settings-profile (mf/element profile-page)
:settings-password (mf/element password-page))]))

View file

@ -13,8 +13,8 @@
[uxbox.main.data.auth :as da]
[uxbox.main.data.projects :as dp]
[uxbox.main.store :as st]
[uxbox.main.ui.users :refer [user]]
[uxbox.util.i18n :refer [tr]]
[uxbox.main.ui.dashboard.header :refer [user]]
[uxbox.util.i18n :as i18n :refer [tr t]]
[uxbox.util.router :as rt]))
(mf/defc header-link
@ -24,25 +24,18 @@
(mf/defc header
[{:keys [section] :as props}]
(let [profile? (= section :settings/profile)
password? (= section :settings/password)
notifications? (= section :settings/notifications)]
(let [profile? (= section :settings-profile)
password? (= section :settings-password)]
[:header#main-bar.main-bar
[:div.main-logo
[:& header-link {:section :dashboard/projects
[:& header-link {:section :dashboard-projects
:content i/logo}]]
[:ul.main-nav
[:li {:class (when profile? "current")}
[:& header-link {:section :settings/profile
[:& header-link {:section :settings-profile
:content (tr "settings.profile")}]]
[:li {:class (when password? "current")}
[:& header-link {:section :settings/password
:content (tr "settings.password")}]]
[:li {:class (when notifications? "current")}
[:& header-link {:section :settings/notifications
:content (tr "settings.notifications")}]]
#_[:li {:on-click #(st/emit! (da/logout))}
[:& header-link {:section :logout
:content (tr "settings.exit")}]]]
[:& header-link {:section :settings-password
:content (tr "settings.password")}]]]
[:& user]]))

View file

@ -2,8 +2,11 @@
;; 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 <niwi@niwi.nz>
;; Copyright (c) 2016-2019 Juan de la Cruz <delacruzgarciajuan@gmail.com>
;; 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 <niwi@niwi.nz>
;; Copyright (c) 2016-2020 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(ns uxbox.main.ui.settings.password
(:require
@ -30,14 +33,23 @@
[event form]
(dom/prevent-default event)
(let [data (:clean-data form)
opts {:on-success #(st/emit! (um/info (tr "settings.password.password-saved")))
mdata {:on-success #(st/emit! (um/info (tr "settings.password.password-saved")))
:on-error #(on-error form %)}]
(st/emit! (udu/update-password data opts))))
(st/emit! (udu/update-password (with-meta data mdata)))))
(s/def ::password-1 ::fm/not-empty-string)
(s/def ::password-2 ::fm/not-empty-string)
(s/def ::password-old ::fm/not-empty-string)
(defn password-equality
[data]
(let [password-1 (:password-1 data)
password-2 (:password-2 data)]
(when (and password-1 password-2
(not= password-1 password-2))
{:password-2 {:code ::password-not-equal
:message "profile.password.not-equal"}})))
(s/def ::password-form
(s/keys :req-un [::password-1
::password-2
@ -45,7 +57,9 @@
(mf/defc password-form
[props]
(let [{:keys [data] :as form} (fm/use-form ::password-form {})]
(let [{:keys [data] :as form} (fm/use-form2 :spec ::password-form
:validators [password-equality]
:initial {})]
[:form.password-form {:on-submit #(on-submit % form)}
[:span.user-settings-label (tr "settings.password.change-password")]
[:input.input-text
@ -67,7 +81,8 @@
:on-blur (fm/on-input-blur form :password-1)
:on-change (fm/on-input-change form :password-1)
:placeholder (tr "settings.password.new-password")}]
;; [:& fm/field-error {:form form :field :password-1}]
[:& fm/field-error {:form form :field :password-1}]
[:input.input-text
{:type "password"
@ -77,7 +92,8 @@
:on-blur (fm/on-input-blur form :password-2)
:on-change (fm/on-input-change form :password-2)
:placeholder (tr "settings.password.confirm-password")}]
;; [:& fm/field-error {:form form :field :password-2}]
[:& fm/field-error {:form form :field :password-2}]
[:input.btn-primary
{:type "submit"

View file

@ -17,31 +17,19 @@
[uxbox.util.data :refer [read-string]]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :as i18n :refer [tr]]
[uxbox.util.interop :refer [iterable->seq]]
[uxbox.util.i18n :as i18n :refer [tr t]]
[uxbox.util.messages :as um]))
(defn- profile->form
[profile]
(let [language (get-in profile [:metadata :language])]
(-> (select-keys profile [:fullname :username :email])
(cond-> language (assoc :language language)))))
(def ^:private profile-ref
(def ^:private profile-iref
(-> (l/key :profile)
(l/derive st/state)))
(s/def ::fullname ::fm/not-empty-string)
(s/def ::username ::fm/not-empty-string)
(s/def ::language ::fm/not-empty-string)
(s/def ::lang ::fm/not-empty-string)
(s/def ::email ::fm/email)
(s/def ::profile-form
(s/keys :req-un [::fullname
::username
::language
::email]))
(s/keys :req-un [::fullname ::lang ::email]))
(defn- on-error
[error form]
@ -56,26 +44,24 @@
{:type ::api
:message "errors.api.form.username-already-exists"})))
(defn- initial-data
[]
(merge {:language @i18n/locale}
(profile->form (deref profile-ref))))
(defn- on-submit
[event form]
(dom/prevent-default event)
(let [data (:clean-data form)
on-success #(st/emit! (um/info (tr "settings.profile.profile-saved")))
on-error #(on-error % form)]
(st/emit! (udu/form->update-profile data on-success on-error))))
(st/emit! (udu/update-profile (with-meta data
{:on-success on-success
:on-error on-error})))))
;; --- Profile Form
(mf/defc profile-form
[props]
(let [{:keys [data] :as form} (fm/use-form ::profile-form initial-data)]
(let [locale (i18n/use-locale)
{:keys [data] :as form} (fm/use-form ::profile-form #(deref profile-iref))]
[:form.profile-form {:on-submit #(on-submit % form)}
[:span.user-settings-label (tr "settings.profile.section-basic-data")]
[:span.user-settings-label (t locale "settings.profile.section-basic-data")]
[:input.input-text
{:type "text"
:name "fullname"
@ -83,23 +69,11 @@
:on-blur (fm/on-input-blur form :fullname)
:on-change (fm/on-input-change form :fullname)
:value (:fullname data "")
:placeholder (tr "settings.profile.your-name")}]
:placeholder (t locale "settings.profile.your-name")}]
[:& fm/field-error {:form form
:type #{::api}
:field :fullname}]
[:input.input-text
{:type "text"
:name "username"
:class (fm/error-class form :username)
: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")}]
[:& fm/field-error {:form form
:type #{::api}
:field :username}]
[:input.input-text
{:type "email"
@ -108,18 +82,18 @@
: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 (t locale "settings.profile.your-email")}]
[:& fm/field-error {:form form
:type #{::api}
:field :email}]
[:span.user-settings-label (tr "settings.profile.section-i18n-data")]
[:select.input-select {:value (:language data)
:name "language"
:class (fm/error-class form :language)
:on-blur (fm/on-input-blur form :language)
:on-change (fm/on-input-change form :language)}
[:span.user-settings-label (t locale "settings.profile.lang")]
[:select.input-select {:value (:lang data)
:name "lang"
:class (fm/error-class form :lang)
:on-blur (fm/on-input-blur form :lang)
:on-change (fm/on-input-change form :lang)}
[:option {:value "en"} "English"]
[:option {:value "fr"} "Français"]]
@ -127,28 +101,30 @@
{:type "submit"
:class (when-not (:valid form) "btn-disabled")
:disabled (not (:valid form))
:value (tr "settings.update-settings")}]]))
:value (t locale "settings.update-settings")}]]))
;; --- Profile Photo Form
(mf/defc profile-photo-form
[]
(letfn [(on-change [event]
(let [target (dom/get-target event)
file (-> (dom/get-files target)
(iterable->seq)
(first))]
(st/emit! (udu/update-photo file))
(dom/clean-value! target)))]
(let [{:keys [photo] :as profile} (mf/deref profile-ref)
photo (if (or (str/empty? photo) (nil? photo))
"images/avatar.jpg"
photo)]
[:form.avatar-form
[:img {:src photo}]
[:input {:type "file"
:value ""
:on-change on-change}]])))
[props]
(let [photo (:photo-uri (mf/deref profile-iref))
photo (if (or (str/empty? photo) (nil? photo))
"images/avatar.jpg"
photo)
on-change
(fn [event]
(let [target (dom/get-target event)
file (-> (dom/get-files target)
(array-seq)
(first))]
(st/emit! (udu/update-photo {:file file}))
(dom/clean-value! target)))]
[:form.avatar-form
[:img {:src photo}]
[:input {:type "file"
:value ""
:on-change on-change}]]))
;; --- Profile Page

View file

@ -1,64 +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-2019 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.main.ui.users
(:require
[cuerdas.core :as str]
[lentes.core :as l]
[potok.core :as ptk]
[rumext.alpha :as mf]
[uxbox.builtins.icons :as i]
[uxbox.main.data.auth :as da]
[uxbox.main.data.lightbox :as udl]
[uxbox.main.store :as st]
[uxbox.main.ui.navigation :as nav]
[uxbox.util.dom :as dom]
[uxbox.util.i18n :refer (tr)]
[uxbox.util.router :as rt]))
;; --- User Menu
(mf/defc user-menu
[props]
(letfn [(on-click [event section]
(dom/stop-propagation event)
(if (keyword? section)
(st/emit! (rt/nav section))
(st/emit! section)))]
[:ul.dropdown
[:li {:on-click #(on-click % :settings/profile)}
i/user
[:span (tr "ds.user.profile")]]
[:li {:on-click #(on-click % :settings/password)}
i/lock
[:span (tr "ds.user.password")]]
[:li {:on-click #(on-click % :settings/notifications)}
i/mail
[:span (tr "ds.user.notifications")]]
[:li {:on-click #(on-click % da/logout)}
i/exit
[:span (tr "ds.user.exit")]]]))
;; --- User Widget
(def profile-ref
(-> (l/key :profile)
(l/derive st/state)))
(mf/defc user
[props]
(let [open (mf/use-state false)
profile (mf/deref profile-ref)
photo (if (str/empty? (:photo profile ""))
"/images/avatar.jpg"
(:photo profile))]
[:div.user-zone {:on-click #(st/emit! (rt/navigate :settings/profile))
:on-mouse-enter #(reset! open true)
:on-mouse-leave #(reset! open false)}
[:span (:fullname profile)]
[:img {:src photo}]
(when @open
[:& user-menu])]))

View file

@ -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 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2015-2017 Juan de la Cruz <delacruzgarciajuan@gmail.com>
@ -17,7 +20,6 @@
[uxbox.main.refs :as refs]
[uxbox.main.store :as st]
[uxbox.main.ui.modal :as modal]
[uxbox.main.ui.users :refer [user]]
[uxbox.main.ui.workspace.images :refer [import-image-modal]]
[uxbox.util.i18n :refer [tr]]
[uxbox.util.math :as mth]

View file

@ -33,10 +33,6 @@
([self f x y] (update-fn #(f % x y)))
([self f x y more] (update-fn #(apply f % x y more))))))
(defn- translate-error-type
[name]
"errors.undefined-error")
(defn- interpret-problem
[acc {:keys [path pred val via in] :as problem}]
;; (prn "interpret-problem" problem)
@ -45,11 +41,11 @@
(list? pred)
(= (first (last pred)) 'cljs.core/contains?))
(let [path (conj path (last (last pred)))]
(assoc-in acc path {:name ::missing :type :builtin}))
(assoc-in acc path {:code ::missing :type :builtin}))
(and (not (empty? path))
(not (empty? via)))
(assoc-in acc path {:name (last via) :type :builtin})
(assoc-in acc path {:code (last via) :type :builtin})
:else acc))
@ -72,6 +68,28 @@
(not= clean-data ::s/invalid)))
(impl-mutator update-state))))
(defn use-form2
[& {:keys [spec validators initial]}]
(let [[state update-state] (mf/useState {:data (if (fn? initial) (initial) initial)
:errors {}
:touched {}})
clean-data (s/conform spec (:data state))
problems (when (= ::s/invalid clean-data)
(::s/problems (s/explain-data spec (:data state))))
errors (merge (reduce interpret-problem {} problems)
(when (not= clean-data ::s/invalid)
(reduce (fn [errors vf]
(merge errors (vf clean-data)))
{} validators))
(:errors state))]
(-> (assoc state
:errors errors
:clean-data (when (not= clean-data ::s/invalid) clean-data)
:valid (and (empty? errors)
(not= clean-data ::s/invalid)))
(impl-mutator update-state))))
(defn on-input-change
[{:keys [data] :as form} field]
(fn [event]
@ -95,17 +113,17 @@
[{:keys [form field type]
:or {only (constantly true)}
:as props}]
(let [touched? (get-in form [:touched field])
{:keys [message code] :as error} (get-in form [:errors field])]
(when (and touched? error
(cond
(nil? type) true
(keyword? type) (= (:type error) type)
(ifn? type) (type (:type error))
:else false))
(prn "field-error" error)
(let [{:keys [code message] :as error} (get-in form [:errors field])
touched? (get-in form [:touched field])
show? (and touched? error message
(cond
(nil? type) true
(keyword? type) (= (:type error) type)
(ifn? type) (type (:type error))
:else false))]
(when show?
[:ul.form-errors
[:li {:key code} (tr message)]])))
[:li {:key (:code error)} (tr (:message error))]])))
(defn error-class
[form field]
@ -115,7 +133,6 @@
;; --- Form Specs and Conformers
;; TODO: migrate to uxbox.util.spec
(s/def ::email ::us/email)
(s/def ::not-empty-string ::us/not-empty-string)
(s/def ::color ::us/color)