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

View file

@ -55,8 +55,8 @@
width 200 width 200
height 200} height 200}
:as opts}] :as opts}]
;; (us/verify ::thumbnail-opts opts) (us/assert ::thumbnail-opts opts)
(us/verify fs/path? input) (us/assert fs/path? input)
(let [ext (format->extension format) (let [ext (format->extension format)
tmp (fs/create-tempfile :suffix ext) tmp (fs/create-tempfile :suffix ext)
opr (doto (IMOperation.) opr (doto (IMOperation.)
@ -80,6 +80,33 @@
(fs/delete tmp) (fs/delete tmp)
(ByteArrayInputStream. thumbnail-data))))) (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 (defn info
[path] [path]
(let [instance (Info. (str path))] (let [instance (Info. (str path))]
@ -96,3 +123,19 @@
row row
(let [url (ust/public-uri media/media-storage value)] (let [url (ust/public-uri media/media-storage value)]
(assoc-in row dst (str url)))))) (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])) [vertx.core :as vc]))
(def sql:insert-user (def sql:insert-user
"insert into users (id, fullname, username, email, password, photo, is_demo) "insert into users (id, fullname, email, password, photo, is_demo)
values ($1, $2, $3, $4, $5, '', true) returning *") values ($1, $2, $3, $4, '', true) returning *")
(def sql:insert-email (def sql:insert-email
"insert into user_emails (user_id, email, is_main) "insert into user_emails (user_id, email, is_main)
@ -44,14 +44,13 @@
[params] [params]
(let [id (uuid/next) (let [id (uuid/next)
sem (System/currentTimeMillis) sem (System/currentTimeMillis)
username (str "demo-" sem) email (str "demo-" sem ".demo@nodomain.com")
email (str username ".demo@uxbox.io")
fullname (str "Demo User " sem) fullname (str "Demo User " sem)
password (-> (sodi.prng/random-bytes 12) password (-> (sodi.prng/random-bytes 12)
(sodi.util/bytes->b64s)) (sodi.util/bytes->b64s))
password' (sodi.pwhash/derive password)] password' (sodi.pwhash/derive password)]
(db/with-atomic [conn db/pool] (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]) (db/query-one conn [sql:insert-email id email])
{:username username {:email email
:password password}))) :password password})))

View file

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

View file

@ -31,16 +31,6 @@
;; --- Query: Profile (own) ;; --- 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 (defn retrieve-profile
[conn id] [conn id]
(let [sql "select * from users where id=$1 and deleted_at is null"] (let [sql "select * from users where id=$1 and deleted_at is null"]
@ -52,12 +42,12 @@
(sq/defquery ::profile (sq/defquery ::profile
[{:keys [user] :as params}] [{:keys [user] :as params}]
(-> (retrieve-profile db/pool user) (-> (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 ;; --- Attrs Helpers
(defn strip-private-attrs (defn strip-private-attrs
"Only selects a publicy visible user attrs." "Only selects a publicy visible user attrs."
[profile] [profile]
(select-keys profile [:id :username :fullname :metadata (select-keys profile [:id :fullname :lang :email :created-at :photo]))
:email :created-at :photo]))

View file

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

View file

@ -66,11 +66,10 @@
(defn create-user (defn create-user
[conn i] [conn i]
(profile/create-profile conn {:id (mk-uuid "user" i) (profile/create-profile conn {:id (mk-uuid "user" i)
:fullname (str "User " i) :fullname (str "User " i)
:username (str "user" i) :email (str "user" i ".test@nodomain.com")
:email (str "user" i ".test@uxbox.io") :password "123123"
:password "123123" :metadata {}}))
:metadata {}}))
(defn create-project (defn create-project
[conn user-id i] [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" "fr" : "+ Nouvelle couleur"
} }
}, },
"ds.colors" : { "dashboard.header.colors" : {
"used-in" : [ "src/uxbox/main/ui/dashboard/header.cljs:48" ],
"translations" : { "translations" : {
"en" : "COLORS", "en" : "COLORS",
"fr" : "COULEURS" "fr" : "COULEURS"
@ -112,8 +111,7 @@
"fr" : "+ Nouvel icône" "fr" : "+ Nouvel icône"
} }
}, },
"ds.icons" : { "dashboard.header.icons" : {
"used-in" : [ "src/uxbox/main/ui/dashboard/header.cljs:42" ],
"translations" : { "translations" : {
"en" : "ICONS", "en" : "ICONS",
"fr" : "ICÔNES" "fr" : "ICÔNES"
@ -133,8 +131,7 @@
"fr" : "+ Nouvelle image" "fr" : "+ Nouvelle image"
} }
}, },
"ds.images" : { "dashboard.header.images" : {
"used-in" : [ "src/uxbox/main/ui/dashboard/header.cljs:45" ],
"translations" : { "translations" : {
"en" : "IMAGES", "en" : "IMAGES",
"fr" : "IMAGES" "fr" : "IMAGES"
@ -210,8 +207,7 @@
}, },
"unused" : true "unused" : true
}, },
"ds.projects" : { "dashboard.header.projects" : {
"used-in" : [ "src/uxbox/main/ui/dashboard/header.cljs:39" ],
"translations" : { "translations" : {
"en" : "PROJECTS", "en" : "PROJECTS",
"fr" : "PROJETS" "fr" : "PROJETS"
@ -287,8 +283,7 @@
"fr" : "Mise en ligne : %s" "fr" : "Mise en ligne : %s"
} }
}, },
"ds.user.exit" : { "dashboard.header.user-menu.logout" : {
"used-in" : [ "src/uxbox/main/ui/users.cljs:43" ],
"translations" : { "translations" : {
"en" : "Exit", "en" : "Exit",
"fr" : "Quitter" "fr" : "Quitter"
@ -301,15 +296,13 @@
"fr" : "Notifications" "fr" : "Notifications"
} }
}, },
"ds.user.password" : { "dashboard.header.user-menu.password" : {
"used-in" : [ "src/uxbox/main/ui/users.cljs:37" ],
"translations" : { "translations" : {
"en" : "Password", "en" : "Password",
"fr" : "Mot de passe" "fr" : "Mot de passe"
} }
}, },
"ds.user.profile" : { "dashboard.header.user-menu.profile" : {
"used-in" : [ "src/uxbox/main/ui/users.cljs:34" ],
"translations" : { "translations" : {
"en" : "Profile", "en" : "Profile",
"fr" : "Profil" "fr" : "Profil"
@ -441,11 +434,11 @@
"fr" : null "fr" : null
} }
}, },
"login.email-or-username" : { "login.email" : {
"used-in" : [ "src/uxbox/main/ui/login.cljs:63" ], "used-in" : [ "src/uxbox/main/ui/login.cljs:63" ],
"translations" : { "translations" : {
"en" : "Email or Username", "en" : "Email",
"fr" : "adresse email ou nom d'utilisateur" "fr" : "adresse email"
} }
}, },
"login.forgot-password" : { "login.forgot-password" : {
@ -532,11 +525,11 @@
"fr" : null "fr" : null
} }
}, },
"profile.recovery.username-or-email" : { "profile.recovery.email" : {
"used-in" : [ "src/uxbox/main/ui/profile/recovery_request.cljs:54" ], "used-in" : [ "src/uxbox/main/ui/profile/recovery_request.cljs:54" ],
"translations" : { "translations" : {
"en" : "Username or Email Address", "en" : "Email Address",
"fr" : "adresse email ou nom d'utilisateur" "fr" : "adresse email"
} }
}, },
"profile.register.already-have-account" : { "profile.register.already-have-account" : {
@ -574,13 +567,6 @@
"fr" : "Mot de passe" "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" : { "settings.exit" : {
"used-in" : [ "src/uxbox/main/ui/settings/header.cljs:46" ], "used-in" : [ "src/uxbox/main/ui/settings/header.cljs:46" ],
"translations" : { "translations" : {
@ -693,7 +679,7 @@
"fr" : "Nom, nom d'utilisateur et adresse email" "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" ], "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:117" ],
"translations" : { "translations" : {
"en" : "Default language", "en" : "Default language",

View file

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

View file

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

View file

@ -128,6 +128,14 @@
(seq params)) (seq params))
(send-mutation! id form))) (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 (defmethod mutation :login
[id params] [id params]
(let [url (str url "/api/login")] (let [url (str url "/api/login")]

View file

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

View file

@ -2,22 +2,28 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; 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/. ;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;; ;;
;; Copyright (c) 2015-2016 Andrey Antukh <niwi@niwi.nz> ;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; Copyright (c) 2015-2016 Juan de la Cruz <delacruzgarciajuan@gmail.com> ;; 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 (ns uxbox.main.ui.dashboard.header
(:require (:require
[cuerdas.core :as str]
[lentes.core :as l] [lentes.core :as l]
[rumext.core :as mx]
[rumext.alpha :as mf] [rumext.alpha :as mf]
[uxbox.builtins.icons :as i] [uxbox.builtins.icons :as i]
[uxbox.main.data.auth :as da]
[uxbox.main.data.projects :as dp] [uxbox.main.data.projects :as dp]
[uxbox.main.store :as st] [uxbox.main.store :as st]
[uxbox.main.ui.navigation :as nav] [uxbox.main.ui.navigation :as nav]
[uxbox.main.ui.users :refer [user]] [uxbox.util.dom :as dom]
[uxbox.util.i18n :refer (tr)] [uxbox.util.i18n :as i18n :refer [t]]
[uxbox.util.router :as rt])) [uxbox.util.router :as rt]))
(declare user)
(mf/defc header-link (mf/defc header-link
[{:keys [section content] :as props}] [{:keys [section content] :as props}]
(let [on-click #(st/emit! (rt/nav section))] (let [on-click #(st/emit! (rt/nav section))]
@ -25,7 +31,8 @@
(mf/defc header (mf/defc header
[{:keys [section] :as props}] [{:keys [section] :as props}]
(let [projects? (= section :dashboard-projects) (let [locale (i18n/use-locale)
projects? (= section :dashboard-projects)
icons? (= section :dashboard-icons) icons? (= section :dashboard-icons)
images? (= section :dashboard-images) images? (= section :dashboard-images)
colors? (= section :dashboard-colors)] colors? (= section :dashboard-colors)]
@ -36,16 +43,61 @@
[:ul.main-nav [:ul.main-nav
[:li {:class (when projects? "current")} [:li {:class (when projects? "current")}
[:& header-link {:section :dashboard-projects [:& header-link {:section :dashboard-projects
:content (tr "ds.projects")}]] :content (t locale "dashboard.header.projects")}]]
[:li {:class (when icons? "current")} [:li {:class (when icons? "current")}
[:& header-link {:section :dashboard-icons [:& header-link {:section :dashboard-icons
:content (tr "ds.icons")}]] :content (t locale "dashboard.header.icons")}]]
[:li {:class (when images? "current")} [:li {:class (when images? "current")}
[:& header-link {:section :dashboard-images [:& header-link {:section :dashboard-images
:content (tr "ds.images")}]] :content (t locale "dashboard.header.images")}]]
[:li {:class (when colors? "current")} [:li {:class (when colors? "current")}
[:& header-link {:section :dashboard-colors [:& header-link {:section :dashboard-colors
:content (tr "ds.colors")}]]] :content (t locale "dashboard.header.colors")}]]]
[:& user]])) [:& 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.i18n :refer [tr]]
[uxbox.util.router :as rt])) [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 ::password ::us/not-empty-string)
(s/def ::login-form (s/def ::login-form
(s/keys :req-un [::username ::password])) (s/keys :req-un [::email ::password]))
(defn- on-submit (defn- on-submit
[event form] [event form]
(dom/prevent-default event) (dom/prevent-default event)
(let [{:keys [username password]} (:clean-data form)] (let [{:keys [email password]} (:clean-data form)]
(st/emit! (da/login {:username username (st/emit! (da/login {:email email :password password}))))
:password password}))))
(mf/defc demo-warning (mf/defc demo-warning
[_] [_]
@ -54,13 +53,13 @@
[:& demo-warning]) [:& demo-warning])
[:input.input-text [:input.input-text
{:name "username" {:name "email"
:tab-index "2" :tab-index "2"
:value (:username data "") :value (:email data "")
:class (fm/error-class form :username) :class (fm/error-class form :email)
:on-blur (fm/on-input-blur form :username) :on-blur (fm/on-input-blur form :email)
:on-change (fm/on-input-change form :username) :on-change (fm/on-input-change form :email)
:placeholder (tr "login.email-or-username") :placeholder (tr "login.email")
:type "text"}] :type "text"}]
[:input.input-text [:input.input-text
{:name "password" {:name "password"

View file

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

View file

@ -21,14 +21,12 @@
[uxbox.util.i18n :refer [tr]] [uxbox.util.i18n :refer [tr]]
[uxbox.util.router :as rt])) [uxbox.util.router :as rt]))
(s/def ::username ::fm/not-empty-string)
(s/def ::fullname ::fm/not-empty-string) (s/def ::fullname ::fm/not-empty-string)
(s/def ::password ::fm/not-empty-string) (s/def ::password ::fm/not-empty-string)
(s/def ::email ::fm/email) (s/def ::email ::fm/email)
(s/def ::register-form (s/def ::register-form
(s/keys :req-un [::username (s/keys :req-un [::password
::password
::fullname ::fullname
::email])) ::email]))
@ -43,11 +41,6 @@
{:type ::api {:type ::api
:message "errors.api.form.email-already-exists"}) :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")))) (st/emit! (tr "errors.api.form.unexpected-error"))))
(defn- on-submit (defn- on-submit
@ -76,20 +69,6 @@
:type #{::api} :type #{::api}
:field :fullname}] :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 [:input.input-text
{:type "email" {:type "email"
:name "email" :name "email"

View file

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

View file

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

View file

@ -2,8 +2,11 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; 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/. ;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;; ;;
;; Copyright (c) 2016-2019 Andrey Antukh <niwi@niwi.nz> ;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; Copyright (c) 2016-2019 Juan de la Cruz <delacruzgarciajuan@gmail.com> ;; 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 (ns uxbox.main.ui.settings.password
(:require (:require
@ -30,14 +33,23 @@
[event form] [event form]
(dom/prevent-default event) (dom/prevent-default event)
(let [data (:clean-data form) (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 %)}] :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-1 ::fm/not-empty-string)
(s/def ::password-2 ::fm/not-empty-string) (s/def ::password-2 ::fm/not-empty-string)
(s/def ::password-old ::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/def ::password-form
(s/keys :req-un [::password-1 (s/keys :req-un [::password-1
::password-2 ::password-2
@ -45,7 +57,9 @@
(mf/defc password-form (mf/defc password-form
[props] [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)} [:form.password-form {:on-submit #(on-submit % form)}
[:span.user-settings-label (tr "settings.password.change-password")] [:span.user-settings-label (tr "settings.password.change-password")]
[:input.input-text [:input.input-text
@ -67,7 +81,8 @@
:on-blur (fm/on-input-blur form :password-1) :on-blur (fm/on-input-blur form :password-1)
:on-change (fm/on-input-change form :password-1) :on-change (fm/on-input-change form :password-1)
:placeholder (tr "settings.password.new-password")}] :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 [:input.input-text
{:type "password" {:type "password"
@ -77,7 +92,8 @@
:on-blur (fm/on-input-blur form :password-2) :on-blur (fm/on-input-blur form :password-2)
:on-change (fm/on-input-change form :password-2) :on-change (fm/on-input-change form :password-2)
:placeholder (tr "settings.password.confirm-password")}] :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 [:input.btn-primary
{:type "submit" {:type "submit"

View file

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

View file

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