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:
parent
841ace3aa8
commit
146faf74a9
26 changed files with 595 additions and 664 deletions
|
@ -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',
|
||||
'',
|
||||
'!');
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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})))
|
||||
|
|
|
@ -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 %))
|
||||
|
|
|
@ -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]))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)))))
|
151
backend/tests/uxbox/tests/test_services_profile.clj
Normal file
151
backend/tests/uxbox/tests/test_services_profile.clj
Normal 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'))))))))
|
||||
|
||||
|
|
@ -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'))))))))
|
||||
|
|
@ -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",
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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))))))
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")]]]))
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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))]))
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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]]))
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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])]))
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue