0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-18 02:32:13 -05:00

♻️ Initial profile and auth refactor.

This commit is contained in:
Andrey Antukh 2020-05-22 13:48:21 +02:00
parent d0defe5d93
commit 7d5f9c1078
59 changed files with 2712 additions and 1407 deletions

View file

@ -0,0 +1,19 @@
-- begin :subject
Email change.
-- end
-- begin :body-text
Hello {{name}}!
We received a request to change your current email to {{ pendingEmail }}.
Click to the link below to confirm the change:
{{ publicUrl }}/#/auth/verify-token?token={{token}}
If you received this email by mistake, please consider changing your password
for security reasons.
Enjoy!
The UXBOX team.
-- end

View file

@ -1,42 +1,18 @@
-- begin :subject
Password recovery.
Password reset.
-- end
-- begin :body-text
Hello {{name}}!
You have requested a password recovery.
We received a request to reset your password. Click the link below to choose a
new one:
The token is:
{{ publicUrl }}/#/auth/recovery?token={{token}}
{{ token }}
If you received this email by mistake, you can safely ignore it. Your password
won't be changed.
Enjoy!
The UXBOX team.
-- end
-- begin :body-html
<html>
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<meta content="width=device-width" name="viewport" />
<title>title</title>
{{> ../partials/inline_style }}
</head>
<body bgcolor="#f6f6f6" cz-shortcut-listen="true">
<table class="body-wrap">
<tbody>
<tr>
<td></td>
<td bgcolor="#FFFFFF" class="container">
<div class="logo">
<img alt="UXBOX" src="{{#static}}images/email/logo.png{{/static}}" />
</div>
<p>TODO</p>
<p>{{ token }}</p>
</td>
<td></td>
</tr>
</tbody>
</table>
{{> ../partials/en/footer }}
</body>
</html>
-- end

View file

@ -1,41 +1,15 @@
-- begin :subject
Welcome to UXBOX.
Verify email.
-- end
-- begin :body-text
Hello {{name}}!
Welcome to UXBOX.
Thanks for signing up for your UXBOX account! Please verify your email using the
link below adn get started building mockups and prototypes today!
UXBOX team.
-- end
{{ publicUrl }}/#/auth/verify-token?token={{token}}
-- begin :body-html
<html>
<head>
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
<meta content="width=device-width" name="viewport" />
<title>title</title>
{{> ../partials/inline_style }}
</head>
<body bgcolor="#f6f6f6" cz-shortcut-listen="true">
<table class="body-wrap">
<tbody>
<tr>
<td></td>
<td bgcolor="#FFFFFF" class="container">
<div class="logo">
<img alt="UXBOX" src="{{#static}}images/email/logo.png{{/static}}" />
</div>
<p>Hello {{name}}!</p>
<p>Welcome to UXBOX.</p>
<p>UXBOX team.</p>
</td>
<td></td>
</tr>
</tbody>
</table>
{{> ../partials/en/footer }}
</body>
</html>
Enjoy!
The UXBOX team.
-- end

View file

@ -6,7 +6,10 @@ CREATE TABLE profile (
deleted_at timestamptz NULL,
fullname text NOT NULL DEFAULT '',
email text NOT NULL,
pending_email text NULL,
photo text NOT NULL,
password text NOT NULL,
@ -33,26 +36,6 @@ VALUES ('00000000-0000-0000-0000-000000000000'::uuid,
CREATE TABLE profile_email (
profile_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
verified_at timestamptz NULL DEFAULT NULL,
email text NOT NULL,
is_main boolean NOT NULL DEFAULT false,
is_verified boolean NOT NULL DEFAULT false
);
CREATE INDEX profile_email__profile_id__idx
ON profile_email (profile_id);
CREATE UNIQUE INDEX profile_email__email__idx
ON profile_email (email);
CREATE TABLE team (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
@ -121,19 +104,6 @@ BEFORE UPDATE ON profile_attr
FOR EACH ROW EXECUTE PROCEDURE update_modified_at();
CREATE TABLE password_recovery_token (
profile_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE,
token text NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
used_at timestamptz NULL,
PRIMARY KEY (profile_id, token)
);
CREATE TABLE session (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),

View file

@ -0,0 +1,6 @@
CREATE TABLE generic_token (
token text PRIMARY KEY,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
valid_until timestamptz NOT NULL,
content bytea NOT NULL
);

View file

@ -26,6 +26,8 @@
:database-username "uxbox"
:database-password "uxbox"
:public-url "http://localhost:3449"
:redis-uri "redis://redis/0"
:media-directory "resources/public/media"
:assets-directory "resources/public/static"
@ -67,11 +69,13 @@
(s/def ::registration-enabled ::us/boolean)
(s/def ::registration-domain-whitelist ::us/string)
(s/def ::debug-humanize-transit ::us/boolean)
(s/def ::public-url ::us/string)
(s/def ::config
(s/keys :opt-un [::http-server-cors
::http-server-debug
::http-server-port
::public-url
::database-username
::database-password
::database-uri

View file

@ -62,3 +62,11 @@
(def password-recovery
"A password recovery notification email."
(emails/build ::password-recovery default-context))
(s/def ::pending-email ::us/string)
(s/def ::change-email
(s/keys :req-un [::name ::pending-email ::token]))
(def change-email
"Password change confirmation email"
(emails/build ::change-email default-context))

View file

@ -18,6 +18,7 @@
#{:create-demo-profile
:logout
:profile
:verify-profile-token
:recover-profile
:register-profile
:request-profile-recovery
@ -50,8 +51,17 @@
(:profile-id req) (assoc :profile-id (:profile-id req)))]
(if (or (:profile-id req)
(contains? unauthorized-services type))
{:status 200
:body (sm/handle (with-meta data {:req req}))}
(let [body (sm/handle (with-meta data {:req req}))]
(if (= type :delete-profile)
(do
(some-> (get-in req [:cookies "auth-token" :value])
(uuid/uuid)
(session/delete))
{:status 204
:cookies {"auth-token" {:value "" :max-age -1}}
:body ""})
{:status 200
:body body}))
{:status 403
:body {:type :authentication
:code :unauthorized}})))
@ -68,11 +78,11 @@
(defn logout-handler
[req]
(some-> (get-in req [:cookies "auth-token"])
(some-> (get-in req [:cookies "auth-token" :value])
(uuid/uuid)
(session/delete))
{:status 204
:cookies {"auth-token" nil}
{:status 200
:cookies {"auth-token" {:value "" :max-age -1}}
:body ""})
(defn echo-handler

View file

@ -34,8 +34,11 @@
:name "0006-presence"
:fn (mg/resource "migrations/0006.presence.sql")}
{:desc "Remove version"
:name "0007.remove_version"
:fn (mg/resource "migrations/0007.remove_version.sql")}]})
:name "0007-remove-version"
:fn (mg/resource "migrations/0007.remove-version.sql")}]})
{:desc "Initial generic token tables"
:name "0008-generic-token"
:fn (mg/resource "migrations/0007.generic-token.sql")}]})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Entry point

View file

@ -46,6 +46,12 @@
(s/def ::old-password ::us/string)
(s/def ::theme ::us/string)
(defn decode-token-row
[{:keys [content] :as row}]
(when row
(cond-> row
content (assoc :content (blob/decode content)))))
;; --- Mutation: Login
@ -86,15 +92,6 @@
;; --- Mutation: Update Profile (own)
(def ^:private sql:update-profile
"update profile
set fullname = $2,
lang = $3,
theme = $4
where id = $1
and deleted_at is null
returning *")
(defn- update-profile
[conn {:keys [id fullname lang theme] :as params}]
(db/update! conn :profile
@ -117,7 +114,7 @@
(defn- validate-password!
[conn {:keys [profile-id old-password] :as params}]
(let [profile (profile/retrieve-profile conn profile-id)
(let [profile (profile/retrieve-profile-data conn profile-id)
result (sodi.pwhash/verify old-password (:password profile))]
(when-not (:valid result)
(ex/raise :type :validation
@ -179,14 +176,10 @@
(defn- update-profile-photo
[conn profile-id path]
(let [sql "update profile set photo=$1
where id=$2
and deleted_at is null
returning id"]
(db/update! conn :profile
{:photo (str path)}
{:id profile-id})
nil))
(db/update! conn :profile
{:photo (str path)}
{:id profile-id})
nil)
;; --- Mutation: Register Profile
@ -211,36 +204,44 @@
[params]
(when-not (:registration-enabled cfg/config)
(ex/raise :type :restriction
:code :registration-disabled))
:code ::registration-disabled))
(when-not (email-domain-in-whitelist? (:registration-domain-whitelist cfg/config)
(:email params))
(ex/raise :type :validation
:code ::email-domain-is-not-allowed))
(db/with-atomic [conn db/pool]
(check-profile-existence! conn params)
(let [profile (register-profile conn params)]
;; TODO: send a correct link for email verification
(let [data {:to (:email params)
:name (:fullname params)}]
(emails/send! conn emails/register data)
profile))))
(let [profile (register-profile conn params)
token (-> (sodi.prng/random-bytes 32)
(sodi.util/bytes->b64s))
payload {:type :verify-email
:profile-id (:id profile)
:email (:email profile)}]
(def ^:private sql:insert-profile
"insert into profile (id, fullname, email, password, photo, is_demo)
values ($1, $2, $3, $4, '', $5) returning *")
(db/insert! conn :generic-token
{:token token
:valid-until (dt/plus (dt/now)
(dt/duration {:days 30}))
:content (blob/encode payload)})
(def ^:private sql:insert-email
"insert into profile_email (profile_id, email, is_main)
values ($1, $2, true)")
(emails/send! conn emails/register
{:to (:email profile)
:name (:fullname profile)
:public-url (:public-url cfg/config)
:token token})
profile)))
(def ^:private sql:profile-existence
"select exists (select * from profile
where email = $1
where email = ?
and deleted_at is null) as val")
(defn- check-profile-existence!
[conn {:keys [email] :as params}]
(let [result (db/exec-one! conn [sql:profile-existence email])]
(let [email (str/lower email)
result (db/exec-one! conn [sql:profile-existence email])]
(when (:val result)
(ex/raise :type :validation
:code ::email-already-exists))
@ -256,68 +257,192 @@
(db/insert! conn :profile
{:id id
:fullname fullname
:email email
:email (str/lower email)
:pending-email (if demo? nil email)
:photo ""
:password password
:is-demo demo?})))
(defn- create-profile-email
[conn {:keys [id email] :as profile}]
(db/insert! conn :profile-email
{:profile-id id
:email email
:is-main true}))
(defn register-profile
[conn params]
(let [prof (create-profile conn params)
_ (create-profile-email conn prof)
team (mt.teams/create-team conn {:profile-id (:id prof)
:name "Default"
:default? true})
_ (mt.teams/create-team-profile conn {:team-id (:id team)
:profile-id (:id prof)})
proj (mt.projects/create-project conn {:profile-id (:id prof)
:team-id (:id team)
:name "Drafts"
:default? true})
_ (mt.projects/create-project-profile conn {:project-id (:id proj)
:profile-id (:id prof)})
]
:default? true})]
(mt.teams/create-team-profile conn {:team-id (:id team)
:profile-id (:id prof)})
(mt.projects/create-project-profile conn {:project-id (:id proj)
:profile-id (:id prof)})
;; TODO: rename to -default-team-id...
(merge (profile/strip-private-attrs prof)
{:default-team (:id team)
:default-project (:id proj)})))
;; --- Mutation: Request Email Change
(declare select-profile-for-update)
(s/def ::request-email-change
(s/keys :req-un [::email]))
(sm/defmutation ::request-email-change
[{:keys [profile-id email] :as params}]
(db/with-atomic [conn db/pool]
(let [email (str/lower email)
profile (select-profile-for-update conn profile-id)
token (-> (sodi.prng/random-bytes 32)
(sodi.util/bytes->b64s))
payload {:type :change-email
:profile-id profile-id
:email email}]
(when (not= email (:email profile))
(check-profile-existence! conn params))
(db/update! conn :profile
{:pending-email email}
{:id profile-id})
(db/insert! conn :generic-token
{:token token
:valid-until (dt/plus (dt/now)
(dt/duration {:hours 48}))
:content (blob/encode payload)})
(emails/send! conn emails/change-email
{:to (:email profile)
:name (:fullname profile)
:public-url (:public-url cfg/config)
:pending-email email
:token token})
nil)))
(defn- select-profile-for-update
[conn id]
(db/get-by-id conn :profile id {:for-update true}))
;; --- Mutation: Verify Profile Token
;; Generic mutation for perform token based verification for auth
;; domain.
(declare retrieve-token)
(s/def ::verify-profile-token
(s/keys :req-un [::token]))
(sm/defmutation ::verify-profile-token
[{:keys [token] :as params}]
(letfn [(handle-email-change [conn token]
(let [profile (select-profile-for-update conn (:profile-id token))]
(when (not= (:email token)
(:pending-email profile))
(ex/raise :type :validation
:code ::email-does-not-match))
(check-profile-existence! conn {:email (:pending-email profile)})
(db/update! conn :profile
{:pending-email nil
:email (:pending-email profile)}
{:id (:id profile)})
token))
(handle-email-verify [conn token]
(let [profile (select-profile-for-update conn (:profile-id token))]
(when (or (not= (:email profile)
(:pending-email profile))
(not= (:email profile)
(:email token)))
(ex/raise :type :validation
:code ::invalid-token))
(db/update! conn :profile
{:pending-email nil}
{:id (:id profile)})
token))]
(db/with-atomic [conn db/pool]
(let [token (retrieve-token conn token)]
(db/delete! conn :generic-token {:token (:token params)})
;; Validate the token expiration
(when (> (inst-ms (dt/now))
(inst-ms (:valid-until token)))
(ex/raise :type :validation
:code ::invalid-token))
(case (:type token)
:change-email (handle-email-change conn token)
:verify-email (handle-email-verify conn token)
(ex/raise :type :validation
:code ::invalid-token))))))
(defn- retrieve-token
[conn token]
(let [row (-> (db/get-by-params conn :generic-token {:token token})
(decode-token-row))]
(when-not row
(ex/raise :type :validation
:code ::invalid-token))
(-> row
(dissoc :content)
(merge (:content row)))))
;; --- Mutation: Cancel Email Change
(s/def ::cancel-email-change
(s/keys :req-un [::profile-id]))
(sm/defmutation ::cancel-email-change
[{:keys [profile-id] :as params}]
(db/with-atomic [conn db/pool]
(let [profile (select-profile-for-update conn profile-id)]
(when (= (:email profile)
(:pending-email profile))
(ex/raise :type :validation
:code ::unexpected-request))
(db/update! conn :profile {:pending-email nil} {:id profile-id})
nil)))
;; --- Mutation: Request Profile Recovery
(s/def ::request-profile-recovery
(s/keys :req-un [::email]))
(def sql:insert-recovery-token
"insert into password_recovery_token (profile_id, token) values ($1, $2)")
(sm/defmutation ::request-profile-recovery
[{:keys [email] :as params}]
(letfn [(create-recovery-token [conn {:keys [id] :as profile}]
(let [token (-> (sodi.prng/random-bytes 32)
(sodi.util/bytes->b64s))
sql sql:insert-recovery-token]
(db/insert! conn :password-recovery-token
{:profile-id id
:token token})
(let [token (-> (sodi.prng/random-bytes 32)
(sodi.util/bytes->b64s))
payload {:type :password-recovery-token
:profile-id id}]
(db/insert! conn :generic-token
{:token token
:valid-until (dt/plus (dt/now) (dt/duration {:hours 24}))
:content (blob/encode payload)})
(assoc profile :token token)))
(send-email-notification [conn profile]
(emails/send! conn emails/password-recovery
{:to (:email profile)
:public-url (:public-url cfg/config)
:token (:token profile)
:name (:fullname profile)})
nil)]
:name (:fullname profile)}))]
(db/with-atomic [conn db/pool]
(let [profile (->> (retrieve-profile-by-email conn email)
(create-recovery-token conn))]
(send-email-notification conn profile)))))
(send-email-notification conn profile)
nil))))
;; --- Mutation: Recover Profile
@ -326,27 +451,30 @@
(s/def ::recover-profile
(s/keys :req-un [::token ::password]))
(def sql:remove-recovery-token
"delete from password_recovery_token where profile_id=$1 and token=$2")
(sm/defmutation ::recover-profile
[{:keys [token password]}]
(letfn [(validate-token [conn token]
(let [sql "delete from password_recovery_token
where token=$1 returning *"
sql "select * from password_recovery_token
where token=$1"]
(-> {:token token}
(db/get-by-params conn :password-recovery-token)
(:profile-id))))
(let [{:keys [token content]}
(-> (db/get-by-params conn :generic-token {:token token})
(decode-token-row))]
(when (not= (:type content) :password-recovery-token)
(ex/raise :type :validation
:code :invalid-token))
(:profile-id content)))
(update-password [conn profile-id]
(let [sql "update profile set password=$2 where id=$1"
pwd (sodi.pwhash/derive password)]
(db/update! conn :profile {:password pwd} {:id profile-id})
nil))]
(let [pwd (sodi.pwhash/derive password)]
(db/update! conn :profile {:password pwd} {:id profile-id})))
(delete-token [conn token]
(db/delete! conn :generic-token {:token token}))]
(db/with-atomic [conn db/pool]
(-> (validate-token conn token)
(update-password conn)))))
(->> (validate-token conn token)
(update-password conn))
(delete-token conn token)
nil)))
;; --- Mutation: Delete Profile
@ -391,6 +519,6 @@
(let [rows (db/exec! conn [sql:teams-ownership-check profile-id])]
(when-not (empty? rows)
(ex/raise :type :validation
:code :owner-teams-with-people
:code ::owner-teams-with-people
:hint "The user need to transfer ownership of owned teams."
:context {:teams (mapv :team-id rows)}))))

View file

@ -73,8 +73,7 @@
(defn retrieve-profile-data
[conn id]
(let [sql "select * from profile where id=? and deleted_at is null"]
(db/exec-one! conn [sql id])))
(db/get-by-id conn :profile id))
(defn retrieve-profile
[conn id]
@ -93,4 +92,4 @@
(defn strip-private-attrs
"Only selects a publicy visible profile attrs."
[o]
(select-keys o [:id :fullname :lang :email :created-at :photo :theme :photo-uri]))
(dissoc o :password :deleted-at))

View file

@ -11,6 +11,7 @@
[clojure.tools.logging :as log]
[clojure.walk :as walk]
[clojure.java.io :as io]
[cuerdas.core :as str]
[uxbox.common.exceptions :as ex])
(:import
java.io.StringReader
@ -26,7 +27,7 @@
(walk/postwalk (fn [x]
(cond
(instance? clojure.lang.Named x)
(name x)
(str/camel (name x))
(instance? clojure.lang.MapEntry x)
x

View file

@ -17,6 +17,7 @@
java.time.OffsetDateTime
java.time.Duration
java.util.Date
java.time.temporal.TemporalAmount
org.apache.logging.log4j.core.util.CronExpression))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -32,6 +33,10 @@
[]
(Instant/now))
(defn plus
[d ta]
(.plus d ^TemporalAmount ta))
(defn- obj->duration
[{:keys [days minutes seconds hours nanos millis]}]
(cond-> (Duration/ofMillis (if (int? millis) ^long millis 0))

View file

@ -5,10 +5,11 @@ application.
## Access to clojure from javascript console
The uxbox namespace of the main application is exported, so that is accessible from
javascript console in Chrome developer tools. Object names and data types are converted
to javascript style. For example you can emit the event to reset zoom level by typing
this at the console (there is autocompletion for help):
The uxbox namespace of the main application is exported, so that is
accessible from javascript console in Chrome developer tools. Object
names and data types are converted to javascript style. For example
you can emit the event to reset zoom level by typing this at the
console (there is autocompletion for help):
```javascript
uxbox.main.store.emit_BANG_(uxbox.main.data.workspace.reset_zoom)
@ -16,17 +17,21 @@ uxbox.main.store.emit_BANG_(uxbox.main.data.workspace.reset_zoom)
## Visual debug mode and utilities
Debugging a problem in the viewport algorithms for grouping and rotating
is difficult. We have set a visual debug mode that displays some
annotations on screen, to help understanding what's happening.
Debugging a problem in the viewport algorithms for grouping and
rotating is difficult. We have set a visual debug mode that displays
some annotations on screen, to help understanding what's happening.
To activate it, open the javascript console and type
```javascript
uxbox.util.debug.toggle_debug("option")
```
Current options are `bounding-boxes`, `group`, `events` and `rotation-handler`.
Current options are `bounding-boxes`, `group`, `events` and
`rotation-handler`.
You can also activate or deactivate all visual aids with
```javascript
uxbox.util.debug.debug_all()
uxbox.util.debug.debug_none()
@ -34,8 +39,8 @@ uxbox.util.debug.debug_none()
## Debug state and objects
There are also some useful functions to visualize the global state or any
complex object. To use them from clojure:
There are also some useful functions to visualize the global state or
any complex object. To use them from clojure:
```clojure
(ns uxbox.util.debug)

View file

@ -1,11 +1,11 @@
{:paths ["src" "vendor" "resources" "../common"]
:deps
{org.clojure/clojurescript {:mvn/version "1.10.753"}
{org.clojure/clojurescript {:mvn/version "1.10.764"}
org.clojure/clojure {:mvn/version "1.10.1"}
com.cognitect/transit-cljs {:mvn/version "0.8.264"}
environ/environ {:mvn/version "1.1.0"}
metosin/reitit-core {:mvn/version "0.4.2"}
environ/environ {:mvn/version "1.2.0"}
metosin/reitit-core {:mvn/version "0.5.1"}
expound/expound {:mvn/version "0.8.4"}
danlentz/clj-uuid {:mvn/version "0.1.9"}
@ -17,7 +17,7 @@
funcool/okulary {:mvn/version "2020.04.14-0"}
funcool/potok {:mvn/version "2.8.0-SNAPSHOT"}
funcool/promesa {:mvn/version "5.1.0"}
funcool/rumext {:mvn/version "2020.05.04-0"}
funcool/rumext {:mvn/version "2020.05.22-1"}
}
:aliases
{:dev

View file

@ -29,14 +29,14 @@
"integrity": "sha512-QzVKww91fJv/KzARJBS/Im5GS2A8iE64E1HxOed72EmYOvPLG4PBw77QCIUjFl7VwWB3G/SVrxsHedJD/wtn1A=="
},
"@types/lodash": {
"version": "4.14.150",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.150.tgz",
"integrity": "sha512-kMNLM5JBcasgYscD9x/Gvr6lTAv2NVgsKtet/hm93qMyf/D1pt+7jeEZklKJKxMVmXjxbRVQQGfqDSfipYCO6w=="
"version": "4.14.152",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.152.tgz",
"integrity": "sha512-Vwf9YF2x1GE3WNeUMjT5bTHa2DqgUo87ocdgTScupY2JclZ5Nn7W2RLM/N0+oreexUk8uaVugR81NnTY/jNNXg=="
},
"@types/q": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz",
"integrity": "sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw==",
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.4.tgz",
"integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==",
"dev": true
},
"ajv": {
@ -256,6 +256,14 @@
"bn.js": "^4.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0"
},
"dependencies": {
"bn.js": {
"version": "4.11.8",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
"dev": true
}
}
},
"assert": {
@ -352,18 +360,18 @@
"dev": true
},
"autoprefixer": {
"version": "9.7.6",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.7.6.tgz",
"integrity": "sha512-F7cYpbN7uVVhACZTeeIeealwdGM6wMtfWARVLTy5xmKtgVdBNJvbDRoCK3YO1orcs7gv/KwYlb3iXwu9Ug9BkQ==",
"version": "9.8.0",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.0.tgz",
"integrity": "sha512-D96ZiIHXbDmU02dBaemyAg53ez+6F5yZmapmgKcjm35yEe1uVDYI8hGW3VYoGRaG290ZFf91YxHrR518vC0u/A==",
"dev": true,
"requires": {
"browserslist": "^4.11.1",
"caniuse-lite": "^1.0.30001039",
"browserslist": "^4.12.0",
"caniuse-lite": "^1.0.30001061",
"chalk": "^2.4.2",
"normalize-range": "^0.1.2",
"num2fraction": "^1.2.2",
"postcss": "^7.0.27",
"postcss-value-parser": "^4.0.3"
"postcss": "^7.0.30",
"postcss-value-parser": "^4.1.0"
}
},
"aws-sign2": {
@ -483,15 +491,25 @@
"integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==",
"dev": true
},
"bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"dev": true,
"optional": true,
"requires": {
"file-uri-to-path": "1.0.0"
}
},
"bintrees": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz",
"integrity": "sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ="
},
"bn.js": {
"version": "4.11.8",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.1.tgz",
"integrity": "sha512-IUTD/REb78Z2eodka1QZyyEk66pciRcP6Sroka0aI3tG/iwIdYLrBD62RsubR7vqdt3WyX8p4jxeatzmRSphtA==",
"dev": true
},
"boolbase": {
@ -602,21 +620,50 @@
"requires": {
"bn.js": "^4.1.0",
"randombytes": "^2.0.1"
},
"dependencies": {
"bn.js": {
"version": "4.11.8",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
"dev": true
}
}
},
"browserify-sign": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz",
"integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.0.tgz",
"integrity": "sha512-hEZC1KEeYuoHRqhGhTy6gWrpJA3ZDjFWv0DE61643ZnOXAKJb3u7yWcrU0mMc9SwAqK1n7myPGndkp0dFG7NFA==",
"dev": true,
"requires": {
"bn.js": "^4.1.1",
"browserify-rsa": "^4.0.0",
"create-hash": "^1.1.0",
"create-hmac": "^1.1.2",
"elliptic": "^6.0.0",
"inherits": "^2.0.1",
"parse-asn1": "^5.0.0"
"bn.js": "^5.1.1",
"browserify-rsa": "^4.0.1",
"create-hash": "^1.2.0",
"create-hmac": "^1.1.7",
"elliptic": "^6.5.2",
"inherits": "^2.0.4",
"parse-asn1": "^5.1.5",
"readable-stream": "^3.6.0",
"safe-buffer": "^5.2.0"
},
"dependencies": {
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true
}
}
},
"browserify-zlib": {
@ -718,9 +765,9 @@
"dev": true
},
"caniuse-lite": {
"version": "1.0.30001048",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001048.tgz",
"integrity": "sha512-g1iSHKVxornw0K8LG9LLdf+Fxnv7T1Z+mMsf0/YYLclQX4Cd522Ap0Lrw6NFqHgezit78dtyWxzlV2Xfc7vgRg==",
"version": "1.0.30001062",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001062.tgz",
"integrity": "sha512-ei9ZqeOnN7edDrb24QfJ0OZicpEbsWxv7WusOiQGz/f2SfvBgHHbOEwBJ8HKGVSyx8Z6ndPjxzR6m0NQq+0bfw==",
"dev": true
},
"caseless": {
@ -749,6 +796,7 @@
"anymatch": "^2.0.0",
"async-each": "^1.0.1",
"braces": "^2.3.2",
"fsevents": "^1.2.7",
"glob-parent": "^3.1.0",
"inherits": "^2.0.3",
"is-binary-path": "^1.0.0",
@ -1073,6 +1121,14 @@
"requires": {
"bn.js": "^4.1.0",
"elliptic": "^6.0.0"
},
"dependencies": {
"bn.js": {
"version": "4.11.8",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
"dev": true
}
}
},
"create-hash": {
@ -1233,9 +1289,9 @@
}
},
"date-fns": {
"version": "2.12.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.12.0.tgz",
"integrity": "sha512-qJgn99xxKnFgB1qL4jpxU7Q2t0LOn1p8KMIveef3UZD7kqjT3tpFNNdXJelEHhE+rUgffriXriw/sOSU+cS1Hw=="
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.14.0.tgz",
"integrity": "sha512-1zD+68jhFgDIM0rF05rcwYO8cExdNqxjq4xP1QKM60Q45mnO6zaMWB4tOzrIr4M4GSLntsKeE4c9Bdl2jhL/yw=="
},
"dateformat": {
"version": "3.0.3",
@ -1391,6 +1447,14 @@
"bn.js": "^4.1.0",
"miller-rabin": "^4.0.0",
"randombytes": "^2.0.0"
},
"dependencies": {
"bn.js": {
"version": "4.11.8",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
"dev": true
}
}
},
"direction": {
@ -1488,9 +1552,9 @@
}
},
"electron-to-chromium": {
"version": "1.3.426",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.426.tgz",
"integrity": "sha512-sdQ7CXQbFflKY5CU63ra+kIYq9F7d1OqI33856qJZxTrwo0sLASdmoRl9lWpGrQDS9Nk/RFliQWd3PPDrZ+Meg==",
"version": "1.3.446",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.446.tgz",
"integrity": "sha512-CLQaFuvkKqR9FD2G3cJrr1fV7DRMXiAKWLP2F8cxtvvtzAS7Tubt0kF47/m+uE61kiT+I7ZEn7HqLnmWdOhmuA==",
"dev": true
},
"elliptic": {
@ -1506,6 +1570,14 @@
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.0"
},
"dependencies": {
"bn.js": {
"version": "4.11.8",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
"dev": true
}
}
},
"enabled": {
@ -1527,9 +1599,9 @@
}
},
"entities": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz",
"integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.2.tgz",
"integrity": "sha512-dmD3AvJQBUjKpcNkoqr+x+IF0SdRtPz9Vk0uTy4yWqga9ibB6s4v++QFWNohjiUGoMlF552ZvNyXDxz5iW0qmw==",
"dev": true
},
"env-variable": {
@ -1917,6 +1989,13 @@
"integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==",
"dev": true
},
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"dev": true,
"optional": true
},
"fill-range": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
@ -2081,6 +2160,17 @@
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
},
"fsevents": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
"dev": true,
"optional": true,
"requires": {
"bindings": "^1.5.0",
"nan": "^2.12.1"
}
},
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@ -2254,9 +2344,9 @@
},
"dependencies": {
"gulp-cli": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.2.0.tgz",
"integrity": "sha512-rGs3bVYHdyJpLqR0TUBnlcZ1O5O++Zs4bA0ajm+zr3WFCfiSLjGwoCBqFs18wzN+ZxahT9DkOK5nDf26iDsWjA==",
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.2.1.tgz",
"integrity": "sha512-yEMxrXqY8mJFlaauFQxNrCpzWJThu0sH1sqlToaTOT063Hub9s/Nt2C+GSLe6feQ/IMWrHvGOOsyES7CQc9O+A==",
"dev": true,
"requires": {
"ansi-colors": "^1.0.1",
@ -2488,9 +2578,9 @@
}
},
"safe-buffer": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
"integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==",
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"dev": true
}
}
@ -3362,6 +3452,14 @@
"requires": {
"bn.js": "^4.0.0",
"brorand": "^1.0.1"
},
"dependencies": {
"bn.js": {
"version": "4.11.8",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
"dev": true
}
}
},
"mime-db": {
@ -3545,6 +3643,13 @@
"integrity": "sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==",
"dev": true
},
"nan": {
"version": "2.14.1",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz",
"integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==",
"dev": true,
"optional": true
},
"nanomatch": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
@ -3616,9 +3721,9 @@
}
},
"node-releases": {
"version": "1.1.53",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.53.tgz",
"integrity": "sha512-wp8zyQVwef2hpZ/dJH7SfSrIPD6YoJz6BDQDpGEkcA0s3LpAQoxBIYmfIq6QAhC1DhwsyCgTaTTcONwX8qzCuQ==",
"version": "1.1.56",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.56.tgz",
"integrity": "sha512-EVo605FhWLygH8a64TjgpjyHYOihkxECwX1bHHr8tETJKWEiWS2YJjPbvsX2jFjnjTNEgBCmk9mLjKG1Mf11cw==",
"dev": true
},
"normalize-package-data": {
@ -4141,9 +4246,9 @@
"dev": true
},
"postcss": {
"version": "7.0.27",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.27.tgz",
"integrity": "sha512-WuQETPMcW9Uf1/22HWUWP9lgsIC+KEHg2kozMflKjbeUtw9ujvFX6QmIfozaErDkmLWS9WEnEdEe6Uo9/BNTdQ==",
"version": "7.0.30",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.30.tgz",
"integrity": "sha512-nu/0m+NtIzoubO+xdAlwZl/u5S5vi/y6BCsoL8D+8IxsD3XvBS8X4YEADNIVXKVuQvduiucnRv+vPIqj56EGMQ==",
"dev": true,
"requires": {
"chalk": "^2.4.2",
@ -4226,6 +4331,14 @@
"parse-asn1": "^5.0.0",
"randombytes": "^2.0.1",
"safe-buffer": "^5.1.2"
},
"dependencies": {
"bn.js": {
"version": "4.11.8",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==",
"dev": true
}
}
},
"pump": {
@ -4314,9 +4427,9 @@
}
},
"react-color": {
"version": "2.18.0",
"resolved": "https://registry.npmjs.org/react-color/-/react-color-2.18.0.tgz",
"integrity": "sha512-FyVeU1kQiSokWc8NPz22azl1ezLpJdUyTbWL0LPUpcuuYDrZ/Y1veOk9rRK5B3pMlyDGvTk4f4KJhlkIQNRjEA==",
"version": "2.18.1",
"resolved": "https://registry.npmjs.org/react-color/-/react-color-2.18.1.tgz",
"integrity": "sha512-X5XpyJS6ncplZs74ak0JJoqPi+33Nzpv5RYWWxn17bslih+X7OlgmfpmGC1fNvdkK7/SGWYf1JJdn7D2n5gSuQ==",
"requires": {
"@icons/material": "^0.2.4",
"lodash": "^4.17.11",
@ -4488,9 +4601,9 @@
"dev": true
},
"replace-ext": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz",
"integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz",
"integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==",
"dev": true
},
"replace-homedir": {
@ -4749,9 +4862,9 @@
}
},
"shadow-cljs": {
"version": "2.8.109",
"resolved": "https://registry.npmjs.org/shadow-cljs/-/shadow-cljs-2.8.109.tgz",
"integrity": "sha512-xUN5kBYgyk2OVv3Gz9/dxJdDNoImskYg6VNLpHkubCG46Q1Lv9tymd11Hyekka6WWk24QCNSVIyPta82txZGfQ==",
"version": "2.9.8",
"resolved": "https://registry.npmjs.org/shadow-cljs/-/shadow-cljs-2.9.8.tgz",
"integrity": "sha512-pZQT6hbTnT2CLN2lrp5bV9vglYd4hdlIqPqEateOZGmy+2RHYI6BLd2Zbx96hjnTaWcsSx3H9nv/B4nOGD6eDA==",
"dev": true,
"requires": {
"node-libs-browser": "^2.0.0",
@ -4878,9 +4991,9 @@
}
},
"slate": {
"version": "0.57.2",
"resolved": "https://registry.npmjs.org/slate/-/slate-0.57.2.tgz",
"integrity": "sha512-qxx9iwNmN3fn13hPbwh1p65aNLCgpHMMK/XXLX7rBVv+GT2UFys9tU8OK6FyUF/lU2uEJ++sScDu8cHjzwLefw==",
"version": "0.58.1",
"resolved": "https://registry.npmjs.org/slate/-/slate-0.58.1.tgz",
"integrity": "sha512-2Vj1jfzHQ/X4t23iKaWoEw09iuIo1oYIsl2tZjZTEl61VNwFEIZkjzI5yuyGS4x0QnUMbNtMoOCeJQx8HxHvdw==",
"requires": {
"@types/esrever": "^0.2.0",
"esrever": "^0.2.0",
@ -4890,9 +5003,9 @@
}
},
"slate-react": {
"version": "0.57.2",
"resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.57.2.tgz",
"integrity": "sha512-fg91E7XISMnFfoHB8vPbbaKoTDpaTfE+iwnG9i6EzbIfwNysz8XvLDpRW3XExm1ZtAfhEKB3Um8nPMtGaugVRg==",
"version": "0.58.1",
"resolved": "https://registry.npmjs.org/slate-react/-/slate-react-0.58.1.tgz",
"integrity": "sha512-y94fhdUYjCFsZiN0vEMo9pxL+HA9U8RH2E4w5LxjA2ZVFk2htf8rRZC+6sq5OHBrVjp2Tw09EJbMQhrahqrtew==",
"requires": {
"@types/is-hotkey": "^0.1.1",
"@types/lodash": "^4.14.149",
@ -5078,9 +5191,9 @@
"dev": true
},
"spdx-expression-parse": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz",
"integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==",
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
"integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
"dev": true,
"requires": {
"spdx-exceptions": "^2.1.0",
@ -5733,9 +5846,9 @@
"dev": true
},
"tslib": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz",
"integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA=="
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
"integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q=="
},
"tty-browserify": {
"version": "0.0.0",

View file

@ -13,7 +13,7 @@
],
"scripts": {},
"devDependencies": {
"autoprefixer": "^9.7.6",
"autoprefixer": "^9.8.0",
"clean-css": "^4.2.3",
"gulp": "4.0.2",
"gulp-gzip": "^1.4.2",
@ -22,21 +22,21 @@
"gulp-rename": "^2.0.0",
"gulp-svg-sprite": "^1.5.0",
"mkdirp": "^1.0.4",
"postcss": "^7.0.27",
"postcss": "^7.0.30",
"rimraf": "^3.0.0",
"sass": "^1.26.0",
"shadow-cljs": "^2.8.96"
"shadow-cljs": "^2.9.7"
},
"dependencies": {
"date-fns": "^2.12.0",
"date-fns": "^2.13.0",
"mousetrap": "^1.6.5",
"randomcolor": "^0.5.4",
"react": "^16.13.1",
"react-color": "^2.18.0",
"react-color": "^2.18.1",
"react-dom": "^16.13.1",
"rxjs": "^7.0.0-beta.0",
"slate": "^0.57.1",
"slate-react": "^0.57.1",
"slate": "^0.58.1",
"slate-react": "^0.58.1",
"source-map-support": "^0.5.16",
"tdigest": "^0.1.1",
"xregexp": "^4.3.0"

View file

@ -0,0 +1,3 @@
<svg width="500" height="500" viewBox="0 0 495 500">
<path d="M175.754 251.669q0 39.766 19.744 62.569 19.744 22.525 54.227 22.525 34.204 0 53.67-22.803 19.745-22.803 19.745-62.291 0-38.933-20.023-61.736-20.022-23.081-53.948-23.081-33.649 0-53.671 22.803-19.744 22.803-19.744 62.014zm151.557 83.147q-16.685 21.413-38.376 31.702-21.413 10.011-50.056 10.011-47.83 0-77.864-34.482-29.755-34.761-29.755-90.378 0-55.618 30.033-90.379 30.033-34.76 77.586-34.76 28.643 0 50.334 10.567 21.69 10.289 38.098 31.424v-36.43h39.766v204.672q40.601-6.118 63.404-36.985 23.081-31.146 23.081-80.368 0-29.755-8.898-55.895-8.621-26.14-26.419-48.387-28.92-36.43-70.634-55.617-41.435-19.467-90.378-19.467-34.205 0-65.628 9.177-31.424 8.9-58.12 26.697-43.66 28.365-68.41 74.527Q40.603 196.329 40.603 250q0 44.216 15.851 82.87 16.13 38.654 46.44 68.131 29.2 28.921 67.576 43.938 38.376 15.295 82.036 15.295 35.873 0 70.356-12.236 34.76-11.958 63.681-34.483l25.028 30.868q-34.76 26.974-75.917 41.156Q294.774 500 252.506 500q-51.446 0-97.053-18.354-45.606-18.075-81.201-52.836-35.595-34.761-54.227-80.367Q1.393 302.558 1.393 250q0-50.612 18.91-96.496 18.91-45.884 53.949-80.645 35.873-35.317 82.87-53.95Q204.118 0 256.677 0q58.954 0 109.288 24.194 50.612 24.193 84.816 68.687 20.857 27.252 31.702 59.232 11.124 31.98 11.124 66.185 0 73.137-44.216 115.406-44.216 42.27-122.08 43.938z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,3 @@
<svg width="500" height="500">
<path d="M142.771 278.2l247.992.6-33.635 35.2-34.136 35.3 18.775 20 20.08 18.5L500 249.5 363.253 112.2l-21.285 17.4-18.876 20 34.137 35.3 33.534 35-247.992.7v57.5zM265.562 500H.702L0 499.2V.8L.703 0h264.86v45H45.18v410h220.381z"/>
</svg>

After

Width:  |  Height:  |  Size: 271 B

File diff suppressed because it is too large Load diff

View file

@ -55,10 +55,10 @@ svg {
a {
cursor: pointer;
color: $color-primary;
color: $color-primary-dark;
&:hover {
color: $color-primary-dark;
color: $color-primary;
}
}

View file

@ -41,39 +41,41 @@
// Partials
//#################################################
@import "main/partials/login";
@import "main/partials/messages";
@import "main/partials/texts";
@import "main/partials/viewer";
@import "main/partials/viewer-header";
@import "main/partials/viewer-thumbnails";
@import "main/partials/zoom-widget";
@import 'main/partials/activity-bar';
@import 'main/partials/color-palette';
@import 'main/partials/colorpicker';
@import 'main/partials/context-menu';
@import 'main/partials/dashboard-bar';
@import 'main/partials/dashboard-grid';
@import 'main/partials/debug-icons-preview';
@import 'main/partials/editable-label';
@import 'main/partials/forms';
@import 'main/partials/left-toolbar';
@import 'main/partials/library-bar';
@import 'main/partials/lightbox';
@import 'main/partials/loader';
@import 'main/partials/main-bar';
@import 'main/partials/workspace';
@import 'main/partials/workspace-header';
@import 'main/partials/workspace-libraries';
@import 'main/partials/tool-bar';
@import 'main/partials/modal';
@import 'main/partials/project-bar';
@import 'main/partials/sidebar';
@import 'main/partials/sidebar-tools';
@import 'main/partials/sidebar-align-options';
@import 'main/partials/sidebar-document-history';
@import 'main/partials/sidebar-element-options';
@import 'main/partials/sidebar-icons';
@import 'main/partials/sidebar-interactions';
@import 'main/partials/sidebar-layers';
@import 'main/partials/sidebar-sitemap';
@import 'main/partials/sidebar-document-history';
@import 'main/partials/left-toolbar';
@import 'main/partials/dashboard-bar';
@import 'main/partials/dashboard-grid';
@import 'main/partials/user-settings';
@import 'main/partials/activity-bar';
@import 'main/partials/library-bar';
@import 'main/partials/lightbox';
@import 'main/partials/color-palette';
@import 'main/partials/colorpicker';
@import 'main/partials/forms';
@import 'main/partials/loader';
@import 'main/partials/context-menu';
@import 'main/partials/debug-icons-preview';
@import 'main/partials/editable-label';
@import 'main/partials/sidebar-tools';
@import 'main/partials/tab-container';
@import "main/partials/zoom-widget";
@import "main/partials/viewer-header";
@import "main/partials/viewer-thumbnails";
@import "main/partials/viewer";
@import "main/partials/messages";
@import "main/partials/texts";
@import 'main/partials/tool-bar';
@import 'main/partials/user-settings';
@import 'main/partials/workspace';
@import 'main/partials/workspace-header';
@import 'main/partials/workspace-libraries';

View file

@ -5,115 +5,51 @@
// Copyright (c) 2015-2020 Andrey Antukh <niwi@niwi.nz>
// Copyright (c) 2015-2020 Juan de la Cruz <delacruzgarciajuan@gmail.com>
.login {
align-items: center;
background-color: $color-gray-40;
background-image: url("/images/login-bg.jpg");
background-position: center;
background-repeat: no-repeat;
background-size: cover;
display: flex;
// TODO: rename to auth.scss
.auth {
display: grid;
grid-template-rows: auto;
grid-template-columns: 388px auto;
}
.auth-sidebar {
grid-column: 1 / span 1;
height: 100vh;
justify-content: center;
position: relative;
width: 100%;
.login-body {
align-items: center;
display: flex;
flex-direction: column;
svg {
fill: $color-black;
height: 70px;
margin-bottom: $x-big;
width: 200px;
@include animation(.1s,1.5s,fadeInDown);
}
.login-content {
display: flex;
flex-direction: column;
width: 320px;
@include animation(1s,1s,fadeIn);
.input-text {
background-color: transparent;
border-color: $color-black;
color: $color-black;
font-size: $fs16;
margin-bottom: $big*2;
@include placeholder {
color: $color-gray-30;
}
&:hover {
@include placeholder {
color: $color-black;
}
}
&:focus {
background-color: $color-white;
border-color: $color-gray-10;
}
&.success {
background-color: $color-success-light;
color: $color-success-dark;
@include placeholder {
color: $color-white;
}
}
&.error {
background-color: rgba(234,35,35,.3);
color: red;
@include placeholder {
color: $color-white;
}
}
}
.input-checkbox {
margin: $big 0;
label {
color: $color-gray-20;
}
}
.login-links {
display: flex;
font-size: $fs13;
justify-content: space-between;
margin-top: $medium;
a {
color: $color-black;
text-align: center;
&:hover {
color: $color-primary-dark;
}
}
}
.btn-secondary {
margin-top: 5rem;
}
}
display: flex;
padding-top: 100px;
flex-direction: column;
align-items: center;
justify-content: flex-start;
background-color:#2C233E;
.tagline {
text-align: center;
width: 280px;
font-size: $fs24;
margin-top: 25px;
color: white;
}
.logo {
svg {
fill: white;
width: 280px;
height: 80px;
}
}
}
.auth-content {
grid-column: 2 / span 1;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background-color: $color-white;
.form-container {
width: 368px;
}
}

View file

@ -31,3 +31,15 @@
.dashboard-content {
background-color: lighten($color-gray-10, 5%);
}
.verify-token {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
svg#loader-pencil {
fill: $color-gray-50;
}
}

View file

@ -15,3 +15,265 @@ textarea {
color: $color-danger;
}
}
.featured-note {
display: flex;
align-items: center;
justify-content: center;
font-size: $fs11;
padding: 10px;
margin-bottom: 25px;
background-color: rgba(#59B9E2, 0.05);
color: $color-gray-60;
.icon {
display: flex;
padding: 10px;
svg {
width: 16px;
height: 16px;
}
}
&.warning {
color: $color-danger;
}
}
.generic-form {
display: flex;
justify-content: center;
.forms-container {
display: flex;
margin-top: 40px;
width: 536px;
justify-content: center;
}
form {
display: flex;
flex-direction: column;
// flex-basis: 368px;
}
h1 {
font-size: $fs36;
color: #2C233E;
margin-bottom: 20px;
}
.subtitle {
font-size: $fs24;
color: #2C233E;
margin-bottom: 20px;
}
h2 {
font-size: $fs14;
color: $color-gray-60;
// height: 40px;
display: flex;
align-items: center;
}
a {
text-decoration: underline;
}
.links {
font-size: $fs11;
}
.link-entry {
font-size: $fs12;
color: $color-gray-40;
margin-bottom: 10px;
}
.link-entry a {
font-size: $fs12;
color: $color-primary-dark;
}
}
.custom-input {
display: flex;
flex-direction: column;
margin-bottom: 20px;
label {
font-size: $fs10;
color: $color-gray-30;
}
input {
color: $color-gray-60;
font-size: $fs12;
width: 100%;
border: 0px;
padding: 0px;
margin: 0px;
background-color: transparent;
}
.input-container {
display: flex;
flex-direction: row;
background-color: $color-white;
border-radius: 2px;
border: 1px solid $color-gray-20;
height: 40px;
padding-left: 15px;
padding-right: 15px;
&.invalid {
border-color: $color-danger;
label {
color: $color-danger;
}
}
&.valid {
border-color: $color-success;
}
&.focus {
border-color: $color-gray-60;
}
&.disabled {
background-color: lighten($color-gray-10, 5%);
user-select: none;
}
}
.hint {
padding: 4px;
font-size: $fs10;
}
.error {
color: $color-danger;
padding: 4px;
font-size: $fs10;
}
.main-content {
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
padding-top: 6px;
padding-bottom: 6px;
}
.help-icon {
display: flex;
justify-content: center;
align-items: center;
padding-left: 10px;
svg {
fill: $color-gray-30;
width: 15px;
height: 15px;
}
}
}
.custom-select {
display: flex;
flex-direction: column;
margin-bottom: $big;
position: relative;
label {
font-size: $fs10;
color: $color-gray-30;
}
select {
cursor: pointer;
font-size: $fs12;
border: 0px;
opacity: 0;
z-index: 10;
padding: 0px;
margin: 0px;
background-color: transparent;
position: absolute;
width: calc(100% - 1px);
height: 100%;
padding: 15px;
}
.main-content {
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
padding-top: 6px;
padding-bottom: 6px;
}
.input-container {
display: flex;
flex-direction: row;
background-color: $color-white;
border-radius: 2px;
border: 1px solid $color-gray-20;
height: 40px;
padding-left: 15px;
padding-right: 15px;
&.invalid {
border-color: $color-danger;
label {
color: $color-danger;
}
}
&.valid {
border-color: $color-success;
}
&.focus {
border-color: $color-gray-60;
}
&.disabled {
background-color: $color-gray-10;
user-select: none;
}
}
.value {
color: $color-gray-60;
font-size: $fs12;
width: 100%;
border: 0px;
padding: 0px;
margin: 0px;
}
.icon {
display: flex;
justify-content: center;
align-items: center;
padding-left: 10px;
svg {
fill: $color-gray-30;
transform: rotate(90deg);
width: 15px;
height: 15px;
}
}
}

View file

@ -0,0 +1,55 @@
.generic-modal {
background-color: $color-white;
width: 565px;
display: flex;
position: relative;
.close {
cursor: pointer;
position: absolute;
right: 16px;
top: 16px;
svg {
width: 16px;
height: 16px;
transform: rotate(45deg);
}
}
.modal-content {
display: flex;
flex-grow: 1;
flex-direction: column;
padding: 100px;
}
.button-row {
display: flex;
justify-content: space-between;
> button {
font-size: $fs13;
}
> button:not(:first-child) {
margin-left: 25px;
}
}
}
.change-email-modal {
h2 {
font-size: $fs14;
margin-bottom: 20px;
}
.confirmation {
.btn-primary {
margin-bottom: 30px;
}
.featured-note .icon svg {
fill: $color-success;
}
}
}

View file

@ -1,70 +1,164 @@
.settings-content {
.main-logo {
position: fixed;
left: 0;
top:0;
width: 40px;
height: 40px;
z-index: 12;
cursor: pointer;
}
nav {
header {
display: flex;
left: 0;
width: 100%;
justify-content: center;
flex-direction: column;
height: 160px;
background-color: $color-white;
.nav-item {
margin: 0 $size-6;
color: $color-gray-30;
text-transform: uppercase;
border-bottom: 1px solid transparent;
.secondary-menu {
display: flex;
justify-content: space-between;
height: 40px;
font-size: $fs14;
color: $color-gray-60;
&:hover {
color: $color-black;
.icon {
display: flex;
align-items: center;
}
&.current {
color: $color-black;
border-bottom: 1px solid $color-primary;
.left {
margin-left: 30px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
.label {
margin-left: 15px;
}
svg {
fill: $color-gray-60;
width: 14px;
height: 14px;
transform: rotate(180deg);
}
}
.right {
align-items: center;
cursor: pointer;
display: flex;
justify-content: center;
margin-right: 30px;
.label {
color: $color-primary-dark;
margin-right: 15px;
}
svg {
fill: $color-primary-dark;
width: 14px;
height: 14px;
}
&:hover {
.label {
color: $color-danger;
}
svg {
fill: $color-danger;
}
}
}
}
h1 {
align-items: top;
color: $color-gray-60;
display: flex;
flex-grow: 1;
font-size: $fs24;
font-weight: normal;
justify-content: center;
}
nav {
display: flex;
justify-content: center;
height: 40px;
.nav-item {
align-items: center;
color: $color-gray-40;
display: flex;
flex-basis: 140px;
justify-content: center;
&.current {
border-bottom: 3px solid $color-primary;
}
}
}
}
}
.settings-profile,
.settings-password {
display: flex;
flex-direction: column;
margin: 0 auto;
width: 500px;
.settings-label {
color: $color-black;
font-size: $fs15;
margin: $x-big 0 $x-small 0;
padding: $medium 0;
}
.input-text {
color: $color-gray-60;
}
.btn-primary {
margin-top: $medium;
}
.profile-form,
.password-form {
display: flex;
flex-direction: column;
.settings-profile {
.forms-container {
margin-top: 80px;
}
}
.avatar-form {
align-items: center;
flex-basis: 168px;
height: 100vh;
display: flex;
position: relative;
.image-change-field {
position: relative;
width: 120px;
height: 120px;
.update-overlay {
opacity: 0;
cursor: pointer;
position: absolute;
width: 121px;
height: 121px;
border-radius: 50%;
font-size: $fs24;
color: $color-white;
line-height: 120px;
text-align: center;
background: $color-primary-dark;
z-index: 14;
}
input[type=file] {
width: 120px;
height: 120px;
position: absolute;
opacity: 0;
cursor: pointer;
top: 0;
z-index: 15;
}
&:hover {
img {display: none;}
.update-overlay {opacity: 1};
}
}
}
.profile-form {
flex-grow: 1;
display: flex;
flex-direction: column;
.change-email {
display: flex;
flex-direction: row;
font-size: $fs12;
color: $color-primary-dark;
justify-content: flex-end;
margin-bottom: 20px;
}
}
.avatar-form {
img {
border-radius: 50%;
flex-shrink: 0;
@ -72,7 +166,18 @@
margin-right: $medium;
width: 120px;
}
}
.options-form,
.password-form {
display: flex;
flex-direction: column;
flex-basis: 368px;
h2 {
font-size: $fs14;
font-weight: normal;
margin-bottom: $medium;
}
}
}

View file

@ -41,7 +41,7 @@
(and (or (= path "")
(nil? match))
(not authed?))
(st/emit! (rt/nav :login))
(st/emit! (rt/nav :auth-login))
(and (nil? match) authed?)
(st/emit! (rt/nav :dashboard-team {:team-id (:default-team-id profile)}))

View file

@ -51,14 +51,18 @@
ptk/WatchEvent
(watch [this state s]
(let [params {:email email
(let [{:keys [on-error on-success]
:or {on-error identity
on-success identity}} (meta data)
params {:email email
:password password
:scope "webapp"}
on-error #(rx/of (dm/error (tr "errors.auth.unauthorized")))]
:scope "webapp"}]
(->> (rp/mutation :login params)
(rx/map logged-in)
(rx/catch rp/client-error? on-error))))))
(rx/tap on-success)
(rx/catch (fn [err]
(on-error err)
(rx/empty)))
(rx/map logged-in))))))
;; --- Logout
(def clear-user-data
@ -81,8 +85,8 @@
(ptk/reify ::logout
ptk/WatchEvent
(watch [_ state stream]
(rx/of (rt/nav :login)
clear-user-data))))
(rx/of clear-user-data
(rt/nav :auth-login)))))
;; --- Register
@ -93,18 +97,37 @@
(defn register
"Create a register event instance."
[data on-error]
[data]
(s/assert ::register data)
(s/assert fn? on-error)
(ptk/reify ::register
ptk/WatchEvent
(watch [_ state stream]
(letfn [(handle-error [{payload :payload}]
(on-error payload)
(rx/empty))]
(let [{:keys [on-error on-success]
:or {on-error identity
on-success identity}} (meta data)]
(->> (rp/mutation :register-profile data)
(rx/map (fn [_] (login data)))
(rx/catch rp/client-error? handle-error))))))
(rx/tap on-success)
(rx/map #(login data))
(rx/catch (fn [err]
(on-error err)
(rx/empty))))))))
;; --- Request Account Deletion
(def request-account-deletion
(letfn [(on-error [{:keys [code] :as error}]
(if (= :uxbox.services.mutations.profile/owner-teams-with-people code)
(let [msg (tr "settings.notifications.profile-deletion-not-allowed")]
(rx/of (dm/error msg)))
(rx/empty)))]
(ptk/reify ::request-account-deletion
ptk/WatchEvent
(watch [_ state stream]
(rx/concat
(->> (rp/mutation :delete-profile {})
(rx/map #(rt/nav :auth-goodbye))
(rx/catch on-error)))))))
;; --- Recovery Request
@ -112,38 +135,43 @@
(s/keys :req-un [::email]))
(defn request-profile-recovery
[data on-success]
[data]
(us/verify ::recovery-request data)
(us/verify fn? on-success)
(ptk/reify ::request-profile-recovery
ptk/WatchEvent
(watch [_ state stream]
(letfn [(on-error [{payload :payload}]
(rx/empty))]
(let [{:keys [on-error on-success]
:or {on-error identity
on-success identity}} (meta data)]
(->> (rp/mutation :request-profile-recovery data)
(rx/tap on-success)
(rx/catch rp/client-error? on-error))))))
(rx/catch (fn [err]
(on-error err)
(rx/empty))))))))
;; --- Recovery (Password)
(s/def ::token string?)
(s/def ::on-error fn?)
(s/def ::on-success fn?)
(s/def ::recover-profile
(s/keys :req-un [::password ::token ::on-error ::on-success]))
(s/keys :req-un [::password ::token]))
(defn recover-profile
[{:keys [token password on-error on-success] :as data}]
[{:keys [token password] :as data}]
(us/verify ::recover-profile data)
(ptk/reify ::recover-profile
ptk/WatchEvent
(watch [_ state stream]
(->> (rp/mutation :recover-profile {:token token :password password})
(rx/tap on-success)
(rx/catch (fn [err]
(on-error)
(rx/empty)))))))
(let [{:keys [on-error on-success]
:or {on-error identity
on-success identity}} (meta data)]
(->> (rp/mutation :recover-profile data)
(rx/tap on-success)
(rx/catch (fn [err]
(on-error)
(rx/empty))))))))
;; --- Create Demo Profile

View file

@ -143,7 +143,7 @@
(->> (rp/query :projects-by-team {:team-id team-id})
(rx/map projects-fetched)
(rx/catch (fn [error]
(rx/of (rt/nav' :not-authorized))))))))
(rx/of (rt/nav' :auth-login))))))))
(defn projects-fetched
[projects]
@ -212,7 +212,7 @@
(->> (rp/query :recent-files params)
(rx/map recent-files-fetched)
(rx/catch (fn [e]
(rx/of (rt/nav' :not-authorized)))))))))
(rx/of (rt/nav' :auth-login)))))))))
(defn recent-files-fetched
[recent-files]

View file

@ -61,3 +61,9 @@
(show {:content message
:type :info
:timeout timeout}))
(defn success
[message & {:keys [timeout] :or {timeout 3000}}]
(show {:content message
:type :info
:timeout timeout}))

View file

@ -13,6 +13,7 @@
[uxbox.common.spec :as us]
[uxbox.config :as cfg]
[uxbox.main.repo :as rp]
[uxbox.util.router :as rt]
[uxbox.util.i18n :as i18n :refer [tr]]
[uxbox.util.storage :refer [storage]]
[uxbox.util.avatars :as avatars]
@ -74,7 +75,11 @@
ptk/WatchEvent
(watch [_ state s]
(->> (rp/query! :profile)
(rx/map profile-fetched)))))
(rx/map profile-fetched)
(rx/catch (fn [error]
(if (= (:type error) :not-found)
(rx/of (rt/nav :auth-login))
(rx/empty))))))))
;; --- Update Profile
@ -91,9 +96,35 @@
(rx/empty))]
(->> (rp/mutation :update-profile data)
(rx/do on-success)
(rx/map profile-fetched)
(rx/map (constantly fetch-profile))
(rx/catch rp/client-error? handle-error))))))
;; --- Request Email Change
(defn request-email-change
[{:keys [email] :as data}]
(ptk/reify ::request-email-change
ptk/WatchEvent
(watch [_ state stream]
(let [{:keys [on-error on-success]
:or {on-error identity
on-success identity}} (meta data)]
(->> (rp/mutation :request-email-change data)
(rx/tap on-success)
(rx/map (constantly fetch-profile))
(rx/catch (fn [err]
(on-error err)
(rx/empty))))))))
;; --- Cancel Email Change
(def cancel-email-change
(ptk/reify ::cancel-email-change
ptk/WatchEvent
(watch [_ state stream]
(->> (rp/mutation :cancel-email-change {})
(rx/map (constantly fetch-profile))))))
;; --- Update Password (Form)
(s/def ::update-password
@ -107,15 +138,16 @@
(ptk/reify ::update-password
ptk/WatchEvent
(watch [_ state s]
(let [mdata (meta data)
on-success (:on-success mdata identity)
on-error (:on-error mdata identity)
(let [{:keys [on-error on-success]
:or {on-error identity
on-success identity}} (meta data)
params {:old-password (:password-old data)
:password (:password-1 data)}]
(->> (rp/mutation :update-profile-password params)
(rx/catch rp/client-error? #(do (on-error (:payload %))
(rx/empty)))
(rx/do on-success)
(rx/tap on-success)
(rx/catch (fn [err]
(on-error err)
(rx/empty)))
(rx/ignore))))))

View file

@ -95,7 +95,7 @@
(defmethod mutation :logout
[id params]
(let [url (str url "/api/logout")]
(->> (http/send! {:method :post :url url :body params :auth false})
(->> (http/send! {:method :post :url url :body params})
(rx/mapcat handle-response))))
(def client-error? http/client-error?)

View file

@ -21,11 +21,8 @@
[uxbox.main.refs :as refs]
[uxbox.main.store :as st]
[uxbox.main.ui.dashboard :refer [dashboard]]
[uxbox.main.ui.login :refer [login-page]]
[uxbox.main.ui.static :refer [not-found-page not-authorized-page]]
[uxbox.main.ui.profile.recovery :refer [profile-recovery-page]]
[uxbox.main.ui.profile.recovery-request :refer [profile-recovery-request-page]]
[uxbox.main.ui.profile.register :refer [profile-register-page]]
[uxbox.main.ui.auth :refer [auth verify-token]]
[uxbox.main.ui.settings :as settings]
[uxbox.main.ui.viewer :refer [viewer-page]]
[uxbox.main.ui.workspace :as workspace]
@ -35,14 +32,18 @@
;; --- Routes
(def routes
[["/login" :login]
["/register" :profile-register]
["/recovery/request" :profile-recovery-request]
["/recovery" :profile-recovery]
[["/auth"
["/login" :auth-login]
["/register" :auth-register]
["/recovery/request" :auth-recovery-request]
["/recovery" :auth-recovery]
["/verify-token" :auth-verify-token]
["/goodbye" :auth-goodbye]]
["/settings"
["/profile" :settings-profile]
["/password" :settings-password]]
["/password" :settings-password]
["/options" :settings-options]]
["/view/:page-id" :viewer]
["/not-found" :not-found]
@ -84,20 +85,20 @@
{::mf/wrap [#(mf/catch % {:fallback app-error})]}
[{:keys [route] :as props}]
(case (get-in route [:data :name])
:login
[:& login-page]
:profile-register
[:& profile-register-page]
(:auth-login
:auth-register
:auth-goodbye
:auth-recovery-request
:auth-recovery)
[:& auth {:route route}]
:profile-recovery-request
[:& profile-recovery-request-page]
:profile-recovery
[:& profile-recovery-page]
:auth-verify-token
[:& verify-token {:route route}]
(:settings-profile
:settings-password)
:settings-password
:settings-options)
[:& settings/settings {:route route}]
:debug-icons-preview

View file

@ -0,0 +1,93 @@
;; 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) 2020 UXBOX Labs SL
(ns uxbox.main.ui.auth
(:require
[cljs.spec.alpha :as s]
[beicon.core :as rx]
[rumext.alpha :as mf]
[uxbox.main.ui.icons :as i]
[uxbox.main.data.users :as du]
[uxbox.main.data.messages :as dm]
[uxbox.main.store :as st]
[uxbox.main.ui.messages :refer [messages]]
[uxbox.main.ui.auth.login :refer [login-page]]
[uxbox.main.ui.auth.recovery :refer [recovery-page]]
[uxbox.main.ui.auth.recovery-request :refer [recovery-request-page]]
[uxbox.main.ui.auth.register :refer [register-page]]
[uxbox.main.repo :as rp]
[uxbox.util.timers :as ts]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :as i18n :refer [tr t]]
[uxbox.util.router :as rt]))
(mf/defc goodbye-page
[{:keys [locale] :as props}]
[:div.goodbay
[:h1 (t locale "auth.goodbye-title")]])
(mf/defc auth
[{:keys [route] :as props}]
(let [section (get-in route [:data :name])
locale (mf/deref i18n/locale)]
[:*
[:& messages]
[:div.auth
[:section.auth-sidebar
[:a.logo i/logo]
[:span.tagline (t locale "auth.sidebar-tagline")]]
[:section.auth-content
(case section
:auth-register [:& register-page {:locale locale}]
:auth-login [:& login-page {:locale locale}]
:auth-goodbye [:& goodbye-page {:locale locale}]
:auth-recovery-request [:& recovery-request-page {:locale locale}]
:auth-recovery [:& recovery-page {:locale locale
:params (:query-params route)}])]]]))
(defn- handle-email-verified
[data]
(let [msg (tr "settings.notifications.email-verified-successfully")]
(ts/schedule 100 #(st/emit! (dm/success msg)))
(st/emit! (rt/nav :settings-profile)
du/fetch-profile)))
(defn- handle-email-changed
[data]
(let [msg (tr "settings.notifications.email-changed-successfully")]
(ts/schedule 100 #(st/emit! (dm/success msg)))
(st/emit! (rt/nav :settings-profile)
du/fetch-profile)))
(mf/defc verify-token
[{:keys [route] :as props}]
(let [token (get-in route [:query-params :token])]
(mf/use-effect
(fn []
(->> (rp/mutation :verify-profile-token {:token token})
(rx/subs
(fn [response]
(case (:type response)
:verify-email (handle-email-verified response)
:change-email (handle-email-changed response)
nil))
(fn [error]
(case (:code error)
:uxbox.services.mutations.profile/email-already-exists
(let [msg (tr "errors.email-already-exists")]
(ts/schedule 100 #(st/emit! (dm/error msg)))
(st/emit! (rt/nav :settings-profile)))
(let [msg (tr "errors.generic")]
(ts/schedule 100 #(st/emit! (dm/error msg)))
(st/emit! (rt/nav :settings-profile)))))))))
[:div.verify-token
i/loader-pencil]))

View file

@ -0,0 +1,86 @@
;; 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) 2020 UXBOX Labs SL
(ns uxbox.main.ui.auth.login
(:require
[cljs.spec.alpha :as s]
[rumext.alpha :as mf]
[uxbox.common.spec :as us]
[uxbox.main.ui.icons :as i]
[uxbox.main.data.auth :as da]
[uxbox.main.store :as st]
[uxbox.main.data.messages :as dm]
[uxbox.main.ui.components.forms :refer [input submit-button form]]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :refer [tr t]]
[uxbox.util.router :as rt]))
(s/def ::email ::us/email)
(s/def ::password ::us/not-empty-string)
(s/def ::login-form
(s/keys :req-un [::email ::password]))
(defn- on-error
[form error]
(st/emit! (dm/error (tr "errors.auth.unauthorized"))))
(defn- on-submit
[form event]
(let [params (with-meta (:clean-data form)
{:on-error (partial on-error form)})]
(st/emit! (da/login params))))
(mf/defc login-form
[{:keys [locale] :as props}]
[:& form {:on-submit on-submit
:spec ::login-form
:initial {}}
[:& input
{:name :email
:type "text"
:tab-index "2"
:help-icon i/at
:label (t locale "auth.email-label")}]
[:& input
{:type "password"
:name :password
:tab-index "3"
:help-icon i/eye
:label (t locale "auth.password-label")}]
[:& submit-button
{:label (t locale "auth.login-submit-label")}]])
(mf/defc login-page
[{:keys [locale] :as props}]
[:div.generic-form.login-form
[:div.form-container
[:h1 (t locale "auth.login-title")]
[:div.subtitle (t locale "auth.login-subtitle")]
[:& login-form {:locale locale}]
[:div.links
[:div.link-entry
[:a {:on-click #(st/emit! (rt/nav :auth-recovery-request))
:tab-index "5"}
(t locale "auth.forgot-password")]]
[:div.link-entry
[:span (t locale "auth.register-label") " "]
[:a {:on-click #(st/emit! (rt/nav :auth-register))
:tab-index "6"}
(t locale "auth.register")]]
[:div.link-entry
[:span (t locale "auth.create-demo-profile-label") " "]
[:a {:on-click #(st/emit! da/create-demo-profile)
:tab-index "6"}
(t locale "auth.create-demo-profile")]]]]])

View file

@ -0,0 +1,98 @@
;; 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) 2020 UXBOX Labs SL
(ns uxbox.main.ui.auth.recovery
(:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[rumext.alpha :as mf]
[uxbox.main.ui.icons :as i]
[uxbox.common.spec :as us]
[uxbox.main.data.auth :as uda]
[uxbox.main.data.messages :as dm]
[uxbox.main.store :as st]
[uxbox.main.ui.components.forms :refer [input submit-button form]]
[uxbox.main.ui.messages :refer [messages]]
[uxbox.main.ui.navigation :as nav]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :as i18n :refer [t tr]]
[uxbox.util.router :as rt]))
(s/def ::password-1 ::fm/not-empty-string)
(s/def ::password-2 ::fm/not-empty-string)
(s/def ::token ::fm/not-empty-string)
(s/def ::recovery-form
(s/keys :req-un [::password-1
::password-2]))
(defn- password-equality
[data]
(let [password-1 (:password-1 data)
password-2 (:password-2 data)]
(cond-> {}
(and password-1 password-2
(not= password-1 password-2))
(assoc :password-2 {:message "errors.password-invalid-confirmation"})
(and password-1 (> 8 (count password-1)))
(assoc :password-1 {:message "errors.password-too-short"}))))
(defn- on-error
[form error]
(st/emit! (dm/error (tr "auth.notifications.invalid-token-error"))))
(defn- on-success
[_]
(st/emit! (dm/info (tr "auth.notifications.password-changed-succesfully"))
(rt/nav :auth-login)))
(defn- on-submit
[form event]
(let [params (with-meta {:token (get-in form [:clean-data :token])
:password (get-in form [:clean-data :password-2])}
{:on-error (partial on-error form)
:on-success (partial on-success form)})]
(st/emit! (uda/recover-profile params))))
(mf/defc recovery-form
[{:keys [locale params] :as props}]
[:& form {:on-submit on-submit
:spec ::recovery-form
:validators [password-equality]
:initial params}
[:& input {:type "password"
:name :password-1
:label (t locale "auth.new-password-label")}]
[:& input {:type "password"
:name :password-2
:label (t locale "auth.confirm-password-label")}]
[:& submit-button
{:label (t locale "auth.recovery-submit-label")}]])
;; --- Recovery Request Page
(mf/defc recovery-page
[{:keys [locale params] :as props}]
[:section.generic-form
[:div.form-container
[:h1 "Forgot your password?"]
[:div.subtitle "Please enter your new password"]
[:& recovery-form {:locale locale :params params}]
[:div.links
[:div.link-entry
[:a {:on-click #(st/emit! (rt/nav :auth-login))}
(t locale "profile.recovery.go-to-login")]]]]])

View file

@ -0,0 +1,68 @@
;; 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) 2020 UXBOX Labs SL
(ns uxbox.main.ui.auth.recovery-request
(:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[lentes.core :as l]
[rumext.alpha :as mf]
[uxbox.main.ui.icons :as i]
[uxbox.common.spec :as us]
[uxbox.main.data.auth :as uda]
[uxbox.main.data.messages :as dm]
[uxbox.main.store :as st]
[uxbox.main.ui.components.forms :refer [input submit-button form]]
[uxbox.main.ui.messages :refer [messages]]
[uxbox.main.ui.navigation :as nav]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :as i18n :refer [tr t]]
[uxbox.util.router :as rt]))
(s/def ::email ::us/email)
(s/def ::recovery-request-form (s/keys :req-un [::email]))
(defn- on-submit
[form event]
(let [on-success #(st/emit! (dm/info (tr "auth.notifications.recovery-token-sent"))
(rt/nav :auth-login))
params (with-meta (:clean-data form)
{:on-success on-success})]
(st/emit! (uda/request-profile-recovery params))))
(mf/defc recovery-form
[{:keys [locale] :as props}]
[:& form {:on-submit on-submit
:spec ::recovery-request-form
:initial {}}
[:& input {:name :email
:label (t locale "auth.email-label")
:help-icon i/at
:type "text"}]
[:& submit-button
{:label (t locale "auth.recovery-request-submit-label")}]])
;; --- Recovery Request Page
(mf/defc recovery-request-page
[{:keys [locale] :as props}]
[:section.generic-form
[:div.form-container
[:h1 (t locale "auth.recovery-request-title")]
[:div.subtitle (t locale "auth.recovery-request-subtitle")]
[:& recovery-form {:locale locale}]
[:div.links
[:div.link-entry
[:a {:on-click #(st/emit! (rt/nav :auth-login))}
(t locale "auth.go-back-to-login")]]]]])

View file

@ -0,0 +1,120 @@
;; 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) 2020 UXBOX Labs SL
(ns uxbox.main.ui.auth.register
(:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[lentes.core :as l]
[rumext.alpha :as mf]
[uxbox.config :as cfg]
[uxbox.main.ui.icons :as i]
[uxbox.main.data.auth :as uda]
[uxbox.main.store :as st]
[uxbox.main.data.auth :as da]
[uxbox.main.ui.messages :refer [messages]]
[uxbox.main.ui.components.forms :refer [input submit-button form]]
[uxbox.main.ui.navigation :as nav]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :refer [tr t]]
[uxbox.util.router :as rt]))
(mf/defc demo-warning
[_]
[:div.featured-note.warning
[:span
[:strong "WARNING: "]
"This is a " [:strong "demo"] " service, "
[:strong "DO NOT USE"] " for real work, "
" the projects will be periodicaly wiped."]])
(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 [::password
::fullname
::email]))
(defn- on-error
[form error]
(case (:code error)
:uxbox.services.mutations.profile/registration-disabled
(st/emit! (tr "errors.registration-disabled"))
:uxbox.services.mutations.profile/email-already-exists
(swap! form assoc-in [:errors :email]
{:message "errors.email-already-exists"})
(st/emit! (tr "errors.unexpected-error"))))
(defn- validate
[data]
(let [password (:password data)]
(when (> 8 (count password))
{:password {:message "errors.password-too-short"}})))
(defn- on-submit
[form event]
(let [data (with-meta (:clean-data form)
{:on-error (partial on-error form)})]
(st/emit! (uda/register data))))
(mf/defc register-form
[{:keys [locale] :as props}]
[:& form {:on-submit on-submit
:spec ::register-form
:validators [validate]
:initial {}}
[:& input {:name :fullname
:tab-index "1"
:label (t locale "auth.fullname-label")
:type "text"}]
[:& input {:type "email"
:name :email
:tab-index "2"
:help-icon i/at
:label (t locale "auth.email-label")}]
[:& input {:name :password
:tab-index "3"
:hint (t locale "auth.password-length-hint")
:label (t locale "auth.password-label")
:type "password"}]
[:& submit-button
{:label (t locale "auth.register-submit-label")}]])
;; --- Register Page
(mf/defc register-page
[{:keys [locale] :as props}]
[:section.generic-form
[:div.form-container
[:h1 (t locale "auth.register-title")]
[:div.subtitle (t locale "auth.register-subtitle")]
(when cfg/demo-warning
[:& demo-warning])
[:& register-form {:locale locale}]
[:div.links
[:div.link-entry
[:span (t locale "auth.already-have-account") " "]
[:a {:on-click #(st/emit! (rt/nav :auth-login))
:tab-index "4"}
(t locale "auth.login-here")]]
[:div.link-entry
[:span (t locale "auth.create-demo-profile-label") " "]
[:a {:on-click #(st/emit! da/create-demo-profile)
:tab-index "5"}
(t locale "auth.create-demo-profile")]]]]])

View file

@ -0,0 +1,150 @@
;; 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) 2020 UXBOX Labs SL
(ns uxbox.main.ui.components.forms
(:require
[rumext.alpha :as mf]
[cuerdas.core :as str]
[uxbox.common.data :as d]
[uxbox.main.ui.icons :as i]
[uxbox.util.object :as obj]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :as i18n :refer [t]]
["react" :as react]
[uxbox.util.dom :as dom]))
(def form-ctx (mf/create-context nil))
(mf/defc input
[{:keys [type label help-icon disabled name form hint] :as props}]
(let [form (mf/use-ctx form-ctx)
type' (mf/use-state type)
focus? (mf/use-state false)
locale (mf/deref i18n/locale)
touched? (get-in form [:touched name])
error (get-in form [:errors name])
klass (dom/classnames
:focus @focus?
:valid (and touched? (not error))
:invalid (and touched? error)
:disabled disabled)
swap-text-password
(fn []
(swap! type' (fn [type]
(if (= "password" type)
"text"
"password"))))
on-focus #(reset! focus? true)
on-change (fm/on-input-change form name)
on-blur
(fn [event]
(reset! focus? false)
(when-not (get-in form [:touched name])
(swap! form assoc-in [:touched name] true)))
value (get-in form [:data name] "")
props (-> props
(dissoc :help-icon :form)
(assoc :value value
:on-focus on-focus
:on-blur on-blur
:placeholder label
:on-change on-change
:type @type')
(obj/clj->props))]
[:div.custom-input
[:div.input-container {:class klass}
[:div.main-content
(when-not (str/empty? value)
[:label label])
[:> :input props]]
[:div.help-icon
{:style {:cursor "pointer"}
:on-click (when (= "password" type)
swap-text-password)}
(cond
(and (= type "password")
(= @type' "password"))
i/eye
(and (= type "password")
(= @type' "text"))
i/eye-closed
:else
help-icon)]]
(cond
(and touched? (:message error))
[:span.error (t locale (:message error))]
(string? hint)
[:span.hint hint])]))
(mf/defc select
[{:keys [options label name form default]
:or {default ""}}]
(let [form (mf/use-ctx form-ctx)
value (get-in form [:data name] default)
cvalue (d/seek #(= value (:value %)) options)
on-change (fm/on-input-change form name)]
[:div.custom-select
[:select {:value value
:on-change on-change}
(for [item options]
[:option {:key (:value item) :value (:value item)} (:label item)])]
[:div.input-container
[:div.main-content
[:label label]
[:span.value (:label cvalue "")]]
[:div.icon
i/arrow-slide]]]))
(mf/defc submit-button
[{:keys [label form] :as props}]
(let [form (mf/use-ctx form-ctx)]
[:input.btn-primary.btn-large
{:name "submit"
:class (when-not (:valid form) "btn-disabled")
:disabled (not (:valid form))
:value label
:type "submit"}]))
(mf/defc form
[{:keys [on-submit spec validators initial children class] :as props}]
(let [frm (fm/use-form :spec spec
:validators validators
:initial initial)]
(mf/use-effect
(mf/deps initial)
(fn []
(if (fn? initial)
(swap! frm update :data merge (initial))
(swap! frm update :data merge initial))))
[:& (mf/provider form-ctx) {:value frm}
[:form {:class class
:on-submit (fn [event]
(dom/prevent-default event)
(on-submit frm event))}
children]]))

View file

@ -22,6 +22,7 @@
(def arrow-end (icon-xref :arrow-end))
(def arrow-slide (icon-xref :arrow-slide))
(def artboard (icon-xref :artboard))
(def at (icon-xref :at))
(def auto-fix (icon-xref :auto-fix))
(def auto-height (icon-xref :auto-height))
(def auto-width (icon-xref :auto-width))
@ -60,6 +61,7 @@
(def lock (icon-xref :lock))
(def lock-open (icon-xref :lock-open))
(def logo (icon-xref :uxbox-logo))
(def logout (icon-xref :logout))
(def logo-icon (icon-xref :uxbox-logo-icon))
(def lowercase (icon-xref :lowercase))
(def mail (icon-xref :mail))

View file

@ -1,99 +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/.
;;
;; 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.login
(:require
[cljs.spec.alpha :as s]
[rumext.alpha :as mf]
[uxbox.common.spec :as us]
[uxbox.main.ui.icons :as i]
[uxbox.config :as cfg]
[uxbox.main.data.auth :as da]
[uxbox.main.store :as st]
[uxbox.main.ui.messages :refer [messages]]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :refer [tr]]
[uxbox.util.router :as rt]))
(s/def ::email ::us/email)
(s/def ::password ::us/not-empty-string)
(s/def ::login-form
(s/keys :req-un [::email ::password]))
(defn- on-submit
[event form]
(dom/prevent-default event)
(let [{:keys [email password]} (:clean-data form)]
(st/emit! (da/login {:email email :password password}))))
(mf/defc demo-warning
[_]
[:div.message-inline
[:p
[:strong "WARNING: "]
"This is a " [:strong "demo"] " service, "
[:strong "DO NOT USE"] " for real work, "
" the projects will be periodicaly wiped."]])
(mf/defc login-form
[]
(let [{:keys [data] :as form} (fm/use-form ::login-form {})]
[:form {:on-submit #(on-submit % form)}
[:div.login-content
(when cfg/demo-warning
[:& demo-warning])
[:input.input-text
{:name "email"
:tab-index "2"
: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"
:tab-index "3"
:value (:password data "")
:class (fm/error-class form :password)
:on-blur (fm/on-input-blur form :password)
:on-change (fm/on-input-change form :password)
:placeholder (tr "login.password")
:type "password"}]
[:input.btn-primary.btn-large
{:name "login"
:tab-index "4"
:class (when-not (:valid form) "btn-disabled")
:disabled (not (:valid form))
:value (tr "login.submit")
:type "submit"}]
[:div.login-links
[:a {:on-click #(st/emit! (rt/nav :profile-recovery-request))
:tab-index "5"}
(tr "login.forgot-password")]
[:a {:on-click #(st/emit! (rt/nav :profile-register))
:tab-index "6"}
(tr "login.register")]]
[:a.btn-secondary.btn-small {:on-click #(st/emit! da/create-demo-profile)
:tab-index "7"
:title (tr "login.create-demo-profile-description")}
(tr "login.create-demo-profile")]]]))
(mf/defc login-page
[]
[:div.login
[:div.login-body
[:& messages]
[:a i/logo]
[:& login-form]]])

View file

@ -27,13 +27,12 @@
(defn- on-parent-clicked
[event parent-ref]
(dom/stop-propagation event)
(dom/prevent-default event)
(let [parent (mf/ref-val parent-ref)
current (dom/get-target event)]
(when (dom/equals? parent current)
(reset! state nil)
#_(st/emit! (udl/hide-lightbox)))))
(dom/stop-propagation event)
(dom/prevent-default event)
(reset! state nil))))
(mf/defc modal-wrapper
[{:keys [component props]}]
@ -46,7 +45,8 @@
parent-ref (mf/use-ref nil)]
[:div.lightbox {:class classes
:ref parent-ref
:on-click #(on-parent-clicked % parent-ref)}
:on-click #(on-parent-clicked % parent-ref)
}
(mf/element component props)]))
(mf/defc modal

View file

@ -1,91 +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/.
;;
;; 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.profile.recovery
(:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[lentes.core :as l]
[rumext.alpha :as mf]
[uxbox.main.ui.icons :as i]
[uxbox.common.spec :as us]
[uxbox.main.data.auth :as uda]
[uxbox.main.data.messages :as dm]
[uxbox.main.store :as st]
[uxbox.main.ui.messages :refer [messages]]
[uxbox.main.ui.navigation :as nav]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :as i18n :refer [t]]
[uxbox.util.router :as rt]))
(s/def ::token ::us/not-empty-string)
(s/def ::password ::us/not-empty-string)
(s/def ::recovery-form (s/keys :req-un [::token ::password]))
(mf/defc recovery-form
[]
(let [{:keys [data] :as form} (fm/use-form ::recovery-form {})
locale (i18n/use-locale)
on-success
(fn []
(st/emit! (dm/info (t locale "profile.recovery.password-changed"))
(rt/nav :login)))
on-error
(fn []
(st/emit! (dm/error (t locale "profile.recovery.invalid-token"))))
on-submit
(fn [event]
(dom/prevent-default event)
(st/emit! (uda/recover-profile (assoc (:clean-data form)
:on-error on-error
:on-success on-success))))]
[:form {:on-submit on-submit}
[:div.login-content
[:input.input-text
{:name "token"
:value (:token data "")
:class (fm/error-class form :token)
:on-blur (fm/on-input-blur form :token)
:on-change (fm/on-input-change form :token)
:placeholder (t locale "profile.recovery.token")
:auto-complete "off"
:type "text"}]
[:input.input-text
{:name "password"
:value (:password data "")
:class (fm/error-class form :password)
:on-blur (fm/on-input-blur form :password)
:on-change (fm/on-input-change form :password)
:placeholder (t locale "profile.recovery.password")
:type "password"}]
[:input.btn-primary
{:name "recover"
:class (when-not (:valid form) "btn-disabled")
:disabled (not (:valid form))
:value (t locale "profile.recovery.submit-recover")
:type "submit"}]
[:div.login-links
[:a {:on-click #(st/emit! (rt/nav :login))}
(t locale "profile.recovery.go-to-login")]]]]))
;; --- Recovery Request Page
(mf/defc profile-recovery-page
[]
[:div.login
[:div.login-body
[:& messages]
[:a i/logo]
[:& recovery-form]]])

View file

@ -1,75 +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/.
;;
;; 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.profile.recovery-request
(:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[lentes.core :as l]
[rumext.alpha :as mf]
[uxbox.main.ui.icons :as i]
[uxbox.common.spec :as us]
[uxbox.main.data.auth :as uda]
[uxbox.main.data.messages :as dm]
[uxbox.main.store :as st]
[uxbox.main.ui.messages :refer [messages]]
[uxbox.main.ui.navigation :as nav]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :as i18n :refer [t]]
[uxbox.util.router :as rt]))
(s/def ::email ::us/email)
(s/def ::recovery-request-form (s/keys :req-un [::email]))
(mf/defc recovery-form
[]
(let [{:keys [data] :as form} (fm/use-form ::recovery-request-form {})
locale (i18n/use-locale)
on-success
(fn []
(st/emit! (dm/info (t locale "profile.recovery.recovery-token-sent"))
(rt/nav :profile-recovery)))
on-submit
(fn [event]
(dom/prevent-default event)
(st/emit! (uda/request-profile-recovery (:clean-data form) on-success)))]
[:form {:on-submit on-submit}
[:div.login-content
[:input.input-text
{:name "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"
:class (when-not (:valid form) "btn-disabled")
:disabled (not (:valid form))
:value (t locale "profile.recovery.submit-request")
:type "submit"}]
[:div.login-links
[:a {:on-click #(st/emit! (rt/nav :login))}
(t locale "profile.recovery.go-to-login")]]]]))
;; --- Recovery Request Page
(mf/defc profile-recovery-request-page
[]
[:div.login
[:div.login-body
[:& messages]
[:a i/logo]
[:& recovery-form]]])

View file

@ -1,120 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2015-2017 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2015-2017 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(ns uxbox.main.ui.profile.register
(:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[lentes.core :as l]
[rumext.alpha :as mf]
[uxbox.main.ui.icons :as i]
[uxbox.main.data.auth :as uda]
[uxbox.main.store :as st]
[uxbox.main.ui.messages :refer [messages]]
[uxbox.main.ui.navigation :as nav]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :refer [tr]]
[uxbox.util.router :as rt]))
(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 [::password
::fullname
::email]))
(defn- on-error
[error form]
(case (:code error)
:uxbox.services.users/registration-disabled
(st/emit! (tr "errors.api.form.registration-disabled"))
:uxbox.services.users/email-already-exists
(swap! form assoc-in [:errors :email]
{:type ::api
:message "errors.api.form.email-already-exists"})
(st/emit! (tr "errors.api.form.unexpected-error"))))
(defn- on-submit
[event form]
(dom/prevent-default event)
(let [data (:clean-data form)
on-error #(on-error % form)]
(st/emit! (uda/register data on-error))))
(mf/defc register-form
[props]
(let [{:keys [data] :as form} (fm/use-form ::register-form {})]
[:form {:on-submit #(on-submit % form)}
[:div.login-content
[:input.input-text
{:name "fullname"
:tab-index "1"
:value (:fullname data "")
:class (fm/error-class form :fullname)
:on-blur (fm/on-input-blur form :fullname)
:on-change (fm/on-input-change form :fullname)
:placeholder (tr "profile.register.fullname")
:type "text"}]
[:& fm/field-error {:form form
:type #{::api}
:field :fullname}]
[:input.input-text
{:type "email"
:name "email"
:tab-index "3"
:class (fm/error-class form :email)
:on-blur (fm/on-input-blur form :email)
:on-change (fm/on-input-change form :email)
:value (:email data "")
:placeholder (tr "profile.register.email")}]
[:& fm/field-error {:form form
:type #{::api}
:field :email}]
[:input.input-text
{:name "password"
:tab-index "4"
:value (:password data "")
:class (fm/error-class form :password)
:on-blur (fm/on-input-blur form :password)
:on-change (fm/on-input-change form :password)
:placeholder (tr "profile.register.password")
:type "password"}]
[:& fm/field-error {:form form
:type #{::api}
:field :email}]
[:input.btn-primary
{:type "submit"
:tab-index "5"
:class (when-not (:valid form) "btn-disabled")
:disabled (not (:valid form))
:value (tr "profile.register.get-started")}]
[:div.login-links
[:a {:on-click #(st/emit! (rt/nav :login))}
(tr "profile.register.already-have-account")]]]]))
;; --- Register Page
(mf/defc profile-register-page
[props]
[:div.login
[:div.login-body
[:& messages]
[:a i/logo]
[:& register-form]]])

View file

@ -2,8 +2,10 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2015-2016 Andrey Antukh <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) 2020 UXBOX Labs SL
(ns uxbox.main.ui.settings
(:require
@ -18,6 +20,7 @@
[uxbox.main.ui.messages :refer [messages]]
[uxbox.main.ui.settings.header :refer [header]]
[uxbox.main.ui.settings.password :refer [password-page]]
[uxbox.main.ui.settings.options :refer [options-page]]
[uxbox.main.ui.settings.profile :refer [profile-page]]))
(mf/defc settings
@ -30,7 +33,8 @@
[:& header {:section section :profile profile}]
(case section
:settings-profile (mf/element profile-page)
:settings-password (mf/element password-page))]]))
:settings-password (mf/element password-page)
:settings-options (mf/element options-page))]]))

View file

@ -0,0 +1,102 @@
;; 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) 2020 UXBOX Labs SL
(ns uxbox.main.ui.settings.change-email
(:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[lentes.core :as l]
[rumext.alpha :as mf]
[uxbox.main.ui.icons :as i]
[uxbox.main.data.auth :as da]
[uxbox.main.data.users :as du]
[uxbox.main.ui.components.forms :refer [input submit-button form]]
[uxbox.main.data.messages :as dm]
[uxbox.main.store :as st]
[uxbox.main.refs :as refs]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.main.ui.modal :as modal]
[uxbox.util.i18n :as i18n :refer [tr t]]))
(s/def ::email-1 ::fm/email)
(s/def ::email-2 ::fm/email)
(s/def ::email-change-form
(s/keys :req-un [::email-1 ::email-2]))
(defn- on-error
[form error]
(cond
(= (:code error) :uxbox.services.mutations.profile/email-already-exists)
(swap! form (fn [data]
(let [error {:message (tr "errors.email-already-exists")}]
(-> data
(assoc-in [:errors :email-1] error)
(assoc-in [:errors :email-2] error)))))
:else
(let [msg (tr "errors.unexpected-error")]
(st/emit! (dm/error msg)))))
(defn- on-submit
[form event]
(let [data (with-meta {:email (get-in form [:clean-data :email-1])}
{:on-error (partial on-error form)})]
(st/emit! (du/request-email-change data))))
(mf/defc change-email-form
[{:keys [locale profile] :as props}]
[:section.modal-content.generic-form
[:h2 (t locale "settings.change-email-title")]
[:span.featured-note
[:span.text
[:span "Well send you an email to your current email "]
[:strong (:email profile)]
[:span " to verify your identity."]]]
[:& form {:on-submit on-submit
:spec ::email-change-form
:initial {}}
[:& input {:type "text"
:name :email-1
:label (t locale "settings.new-email-label")}]
[:& input {:type "text"
:name :email-2
:label (t locale "settings.confirm-email-label")}]
[:& submit-button
{:label (t locale "settings.change-email-submit-label")}]]])
(mf/defc change-email-confirmation
[{:keys [locale profile] :as locale}]
[:section.modal-content.generic-form.confirmation
[:h2 (t locale "settings.verification-sent-title")]
[:span.featured-note
[:span.icon i/trash]
[:span.text
[:span (str/format "We have sent you an email to “")]
[:strong (:email profile)]
[:span "” Please follow the instructions to verify the email."]]]
[:button.btn-primary.btn-large
{:on-click #(modal/hide!)}
(t locale "settings.close-modal-label")]])
(mf/defc change-email-modal
[props]
(let [locale (mf/deref i18n/locale)
profile (mf/deref refs/profile)]
[:section.generic-modal.change-email-modal
[:span.close {:on-click #(modal/hide!)} i/close]
(if (:pending-email profile)
[:& change-email-confirmation {:locale locale :profile profile}]
[:& change-email-form {:locale locale :profile profile}])]))

View file

@ -0,0 +1,42 @@
;; 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) 2020 UXBOX Labs SL
(ns uxbox.main.ui.settings.delete-account
(:require
[cljs.spec.alpha :as s]
[rumext.alpha :as mf]
[uxbox.main.ui.icons :as i]
[uxbox.main.data.auth :as da]
[uxbox.main.data.users :as du]
[uxbox.main.store :as st]
[uxbox.main.ui.modal :as modal]
[uxbox.util.i18n :as i18n :refer [tr t]]))
(mf/defc delete-account-modal
[props]
(let [locale (mf/deref i18n/locale)]
[:section.generic-modal.change-email-modal
[:span.close {:on-click #(modal/hide!)} i/close]
[:section.modal-content.generic-form
[:h2 (t locale "settings.delete-account-title")]
[:span.featured-note
[:span.text
[:span (t locale "settings.delete-account-info")]]]
[:div.button-row
[:button.btn-warning.btn-large
{:on-click #(do
(modal/hide!)
(st/emit! da/request-account-deletion))}
(t locale "settings.yes-delete-my-account")]
[:button.btn-secondary.btn-large
{:on-click #(modal/hide!)}
(t locale "settings.cancel-and-keep-my-account")]]]]))

View file

@ -16,22 +16,60 @@
(mf/defc header
[{:keys [section profile] :as props}]
(let [profile? (= section :settings-profile)
(let [profile? (= section :settings-profile)
password? (= section :settings-password)
locale (i18n/use-locale)
team-id (:default-team-id profile)]
[:header
[:div.main-logo
{:on-click #(st/emit! (rt/nav :dashboard-team {:team-id team-id}))}
i/logo-icon]
[:section.main-bar
[:nav
[:a.nav-item
{:class (when profile? "current")
:on-click #(st/emit! (rt/nav :settings-profile))}
(t locale "settings.profile")]
[:a.nav-item
{:class (when password? "current")
:on-click #(st/emit! (rt/nav :settings-password))}
(t locale "settings.password")]]]]))
options? (= section :settings-options)
team-id (:default-team-id profile)
go-back #(st/emit! (rt/nav :dashboard-team {:team-id team-id}))
logout #(st/emit! da/logout)
locale (mf/deref i18n/locale)
team-id (:default-team-id profile)]
[:header
[:section.secondary-menu
[:div.left {:on-click go-back}
[:span.icon i/arrow-slide]
[:span.label "Dashboard"]]
[:div.right {:on-click logout}
[:span.label "Log out"]
[:span.icon i/logout]]]
[:h1 "Your account"]
[:nav
[:a.nav-item
{:class (when profile? "current")
:on-click #(st/emit! (rt/nav :settings-profile))}
(t locale "settings.profile")]
[:a.nav-item
{:class (when password? "current")
:on-click #(st/emit! (rt/nav :settings-password))}
(t locale "settings.password")]
[:a.nav-item
{:class (when options? "current")
:on-click #(st/emit! (rt/nav :settings-options))}
(t locale "settings.options")]
[:a.nav-item
{:class "foobar"
:on-click #(st/emit! (rt/nav :settings-profile))}
(t locale "settings.teams")]]]))
;; [:div.main-logo
;; {:on-click #(st/emit! (rt/nav :dashboard-team {:team-id team-id}))}
;; i/logo-icon]
;; [:section.main-bar
;; [:nav
;; [:a.nav-item
;; {:class (when profile? "current")
;; :on-click #(st/emit! (rt/nav :settings-profile))}
;; (t locale "settings.profile")]
;; [:a.nav-item
;; {:class (when password? "current")
;; :on-click #(st/emit! (rt/nav :settings-password))}
;; (t locale "settings.password")]]]]))

View file

@ -1,43 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2016 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(ns uxbox.main.ui.settings.notifications
(:require
[cuerdas.core :as str]
[rumext.alpha :as mf]
[uxbox.util.i18n :refer [tr]]))
(mf/defc notifications-page
[]
[:section.dashboard-content.user-settings
[:section.user-settings-content
[:span.user-settings-label (tr "settings.notifications.notifications-saved")]
[:p (tr "settings.notifications.description")]
[:div.input-radio.radio-primary
[:input {:type "radio"
:id "notification-1"
:name "notification"
:value "none"}]
[:label {:for "notification-1"
:value (tr "settings.notifications.none")} (tr "settings.notifications.none")]
[:input {:type "radio"
:id "notification-2"
:name "notification"
:value "every-hour"}]
[:label {:for "notification-2"
:value (tr "settings.notifications.every-hour")} (tr "settings.notifications.every-hour")]
[:input {:type "radio"
:id "notification-3"
:name "notification"
:value "every-day"}]
[:label {:for "notification-3"
:value (tr "settings.notifications.every-day")} (tr "settings.notifications.every-day")]]
[:input.btn-primary {:type "submit"
:class "btn-disabled"
:disabled true
:value (tr "settings.update-settings")}]
]])

View file

@ -0,0 +1,75 @@
;; 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) 2020 UXBOX Labs SL
(ns uxbox.main.ui.settings.options
(:require
[rumext.alpha :as mf]
[cljs.spec.alpha :as s]
[uxbox.main.ui.icons :as i]
[uxbox.main.data.users :as udu]
[uxbox.main.data.messages :as dm]
[uxbox.main.ui.components.forms :refer [select submit-button form]]
[uxbox.main.refs :as refs]
[uxbox.main.store :as st]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :as i18n :refer [t tr]]))
(s/def ::lang (s/nilable ::fm/not-empty-string))
(s/def ::theme (s/nilable ::fm/not-empty-string))
(s/def ::options-form
(s/keys :opt-un [::lang ::theme]))
(defn- on-error
[form error])
(defn- on-submit
[form event]
(dom/prevent-default event)
(let [data (:clean-data form)
on-success #(st/emit! (dm/info (tr "settings.notifications.profile-saved")))
on-error #(on-error % form)]
(st/emit! (udu/update-profile (with-meta data
{:on-success on-success
:on-error on-error})))))
(mf/defc options-form
[{:keys [locale profile] :as props}]
[:& form {:class "options-form"
:on-submit on-submit
:spec ::options-form
:initial profile}
[:h2 (t locale "settings.language-change-title")]
[:& select {:options [{:label "English" :value "en"}
{:label "Français" :value "fr"}]
:label (t locale "settings.language-label")
:default "en"
:name :lang}]
[:h2 (t locale "settings.theme-change-title")]
[:& select {:label (t locale "settings.theme-label")
:name :theme
:default "default"
:options [{:label "Default" :value "default"}]}]
[:& submit-button
{:label (t locale "settings.profile-submit-label")}]])
;; --- Password Page
(mf/defc options-page
[props]
(let [locale (mf/deref i18n/locale)
profile (mf/deref refs/profile)]
[:section.settings-options.generic-form
[:div.forms-container
[:& options-form {:locale locale :profile profile}]]]))

View file

@ -5,8 +5,7 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2016-2020 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2016-2020 Juan de la Cruz <delacruzgarciajuan@gmail.com>
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.settings.password
(:require
@ -15,40 +14,52 @@
[uxbox.main.ui.icons :as i]
[uxbox.main.data.users :as udu]
[uxbox.main.data.messages :as dm]
[uxbox.main.ui.components.forms :refer [input submit-button form]]
[uxbox.main.store :as st]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :refer [tr]]))
[uxbox.util.i18n :as i18n :refer [t tr]]))
(defn- on-error
[form error]
(case (:code error)
:uxbox.services.users/old-password-not-match
:uxbox.services.mutations.profile/old-password-not-match
(swap! form assoc-in [:errors :password-old]
{:type ::api :message "settings.password.wrong-old-password"})
{:message (tr "errors.wrong-old-password")})
:else (throw (ex-info "unexpected" {:error error}))))
:else
(let [msg (tr "generic.error")]
(st/emit! (dm/error msg)))))
(defn- on-success
[form]
(let [msg (tr "settings.notifications.password-saved")]
(st/emit! (dm/info msg))))
(defn- on-submit
[event form]
[form event]
(dom/prevent-default event)
(let [data (:clean-data form)
mdata {:on-success #(st/emit! (dm/info (tr "settings.password.password-saved")))
:on-error #(on-error form %)}]
(st/emit! (udu/update-password (with-meta data mdata)))))
(let [params (with-meta (:clean-data form)
{:on-success (partial on-success form)
:on-error (partial on-error form)})]
(st/emit! (udu/update-password params))))
(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
(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"}})))
(cond-> {}
(and password-1 password-2
(not= password-1 password-2))
(assoc :password-2 {:message (tr "errors.password-invalid-confirmation")})
(and password-1 (> 8 (count password-1)))
(assoc :password-1 {:message (tr "errors.password-too-short")}))))
(s/def ::password-form
(s/keys :req-un [::password-1
@ -56,54 +67,37 @@
::password-old]))
(mf/defc password-form
[props]
(let [{:keys [data] :as form} (fm/use-form2 :spec ::password-form
:validators [password-equality]
:initial {})]
[:form.password-form {:on-submit #(on-submit % form)}
[:span.settings-label (tr "settings.password.change-password")]
[:input.input-text
{:type "password"
:name "password-old"
:value (:password-old data "")
:class (fm/error-class form :password-old)
:on-blur (fm/on-input-blur form :password-old)
:on-change (fm/on-input-change form :password-old)
:placeholder (tr "settings.password.old-password")}]
[{:keys [locale] :as props}]
[:& form {:class "password-form"
:on-submit on-submit
:spec ::password-form
:validators [password-equality]
:initial {}}
[:h2 (t locale "settings.password-change-title")]
[:& fm/field-error {:form form :field :password-old :type ::api}]
[:& input
{:type "password"
:name :password-old
:label (t locale "settings.old-password-label")}]
[:input.input-text
{:type "password"
:name "password-1"
:value (:password-1 data "")
:class (fm/error-class form :password-1)
:on-blur (fm/on-input-blur form :password-1)
:on-change (fm/on-input-change form :password-1)
:placeholder (tr "settings.password.new-password")}]
[:& input
{:type "password"
:name :password-1
:label (t locale "settings.new-password-label")}]
[:& fm/field-error {:form form :field :password-1}]
[:& input
{:type "password"
:name :password-2
:label (t locale "settings.confirm-password-label")}]
[:input.input-text
{:type "password"
:name "password-2"
:value (:password-2 data "")
:class (fm/error-class form :password-2)
: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}]
[:input.btn-primary.btn-large
{:type "submit"
:class (when-not (:valid form) "btn-disabled")
:disabled (not (:valid form))
:value (tr "settings.update-settings")}]]))
[:& submit-button
{:label (t locale "settings.profile-submit-label")}]])
;; --- Password Page
(mf/defc password-page
[props]
[:section.settings-password
[:& password-form]])
(let [locale (mf/deref i18n/locale)]
[:section.settings-password.generic-form
[:div.forms-container
[:& password-form {:locale locale}]]]))

View file

@ -2,8 +2,10 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2016-2017 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2016-2017 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) 2020 UXBOX Labs SL
(ns uxbox.main.ui.settings.profile
(:require
@ -13,6 +15,10 @@
[rumext.alpha :as mf]
[uxbox.main.ui.icons :as i]
[uxbox.main.data.users :as udu]
[uxbox.main.ui.components.forms :refer [input submit-button form]]
[uxbox.main.ui.settings.change-email :refer [change-email-modal]]
[uxbox.main.ui.settings.delete-account :refer [delete-account-modal]]
[uxbox.main.ui.modal :as modal]
[uxbox.main.data.messages :as dm]
[uxbox.main.store :as st]
[uxbox.main.refs :as refs]
@ -21,8 +27,6 @@
[uxbox.util.i18n :as i18n :refer [tr t]]))
(s/def ::fullname ::fm/not-empty-string)
(s/def ::lang (s/nilable ::fm/not-empty-string))
(s/def ::theme ::fm/not-empty-string)
(s/def ::email ::fm/email)
(s/def ::profile-form
@ -30,22 +34,12 @@
(defn- on-error
[error form]
(case (:code error)
:uxbox.services.users/email-already-exists
(swap! form assoc-in [:errors :email]
{: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! (dm/error (tr "errors.generic"))))
(defn- on-submit
[event form]
(dom/prevent-default event)
[form event]
(let [data (:clean-data form)
on-success #(st/emit! (dm/info (tr "settings.profile.profile-saved")))
on-success #(st/emit! (dm/info (tr "settings.notifications.profile-saved")))
on-error #(on-error % form)]
(st/emit! (udu/update-profile (with-meta data
{:on-success on-success
@ -54,64 +48,58 @@
;; --- Profile Form
(mf/defc profile-form
[props]
(let [locale (i18n/use-locale)
form (fm/use-form ::profile-form #(deref refs/profile))
data (:data form)]
[:form.profile-form {:on-submit #(on-submit % form)}
[:span.settings-label (t locale "settings.profile.section-basic-data")]
[:input.input-text
[{:keys [locale] :as props}]
(let [prof (mf/deref refs/profile)]
[:& form {:on-submit on-submit
:class "profile-form"
:spec ::profile-form
:initial prof}
[:& input
{:type "text"
:name "fullname"
:class (fm/error-class form :fullname)
:on-blur (fm/on-input-blur form :fullname)
:on-change (fm/on-input-change form :fullname)
:value (:fullname data "")
:placeholder (t locale "settings.profile.your-name")}]
[:& fm/field-error {:form form
:type #{::api}
:field :fullname}]
:name :fullname
:label (t locale "settings.fullname-label")}]
[:input.input-text
[:& input
{:type "email"
:name "email"
:class (fm/error-class form :email)
:on-blur (fm/on-input-blur form :email)
:on-change (fm/on-input-change form :email)
:value (:email data "")
:placeholder (t locale "settings.profile.your-email")}]
[:& fm/field-error {:form form
:type #{::api}
:field :email}]
:name :email
:disabled true
:help-icon i/at
:label (t locale "settings.email-label")}]
[:span.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"]]
(cond
(nil? (:pending-email prof))
[:div.change-email
[:a {:on-click #(modal/show! change-email-modal {})}
(t locale "settings.change-email-label")]]
[:span.user-settings-label (tr "settings.profile.section-theme-data")]
[:select.input-select {:value (:theme data)
:name "theme"
:class (fm/error-class form :theme)
:on-blur (fm/on-input-blur form :theme)
:on-change (fm/on-input-change form :theme)}
[:option {:value "light"} "Default"]]
(not= (:pending-email prof) (:email prof))
[:span.featured-note
[:span.icon i/trash]
[:span.text
[:span "There is a pending change of your email to "]
[:strong (:pending-email prof)]
[:span "."] [:br]
[:a {:on-click #(st/emit! udu/cancel-email-change)}
"Dismiss"]]]
[:input.btn-primary.btn-large
{:type "submit"
:class (when-not (:valid form) "btn-disabled")
:disabled (not (:valid form))
:value (t locale "settings.update-settings")}]]))
:else
[:span.featured-note.warning
[:span.text
[:span "There is a pending email validation."]]])
[:& submit-button
{:label (t locale "settings.profile-submit-label")}]
[:div.links
[:div.link-item
[:a {:on-click #(modal/show! delete-account-modal {})}
(t locale "settings.remove-account-label")]]]]))
;; --- Profile Photo Form
(mf/defc profile-photo-form
[props]
[{:keys [locale] :as props}]
(let [profile (mf/deref refs/profile)
photo (:photo-uri profile)
photo (if (or (str/empty? photo) (nil? photo))
@ -127,10 +115,12 @@
(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}]]))
[:div.image-change-field
[:span.update-overlay (t locale "settings.update-photo-label")]
[:img {:src photo}]
[:input {:type "file"
:value ""
:on-change on-change}]]]))
;; --- Profile Page
@ -138,7 +128,7 @@
{::mf/wrap-props false}
[props]
(let [locale (i18n/use-locale)]
[:section.settings-profile
[:span.settings-label (t locale "settings.profile.your-avatar")]
[:& profile-photo-form]
[:& profile-form]]))
[:section.settings-profile.generic-form
[:div.forms-container
[:& profile-photo-form {:locale locale}]
[:& profile-form {:locale locale}]]]))

View file

@ -35,7 +35,7 @@
[& params]
(assert (even? (count params)))
(str/join " " (reduce (fn [acc [k v]]
(if (true? v)
(if (true? (boolean v))
(conj acc (name k))
acc))
[]

View file

@ -50,44 +50,25 @@
:else acc))
(defn use-form
[spec 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)
(: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 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)
cleaned (s/conform spec (:data state))
problems (when (= ::s/invalid cleaned)
(::s/problems (s/explain-data spec (:data state))))
errors (merge (reduce interpret-problem {} problems)
(when (not= clean-data ::s/invalid)
errors (merge (reduce interpret-problem {} problems)
(reduce (fn [errors vf]
(merge errors (vf clean-data)))
{} validators))
(:errors state))]
(merge errors (vf (:data state))))
{} validators)
(:errors state))]
(-> (assoc state
:errors errors
:clean-data (when (not= clean-data ::s/invalid) clean-data)
:clean-data (when (not= cleaned ::s/invalid) cleaned)
:valid (and (empty? errors)
(not= clean-data ::s/invalid)))
(not= cleaned ::s/invalid)))
(impl-mutator update-state))))
(defn on-input-change

View file

@ -9,8 +9,11 @@
(ns uxbox.util.object
"A collection of helpers for work with javascript objects."
(:refer-clojure :exclude [get get-in assoc!])
(:require [goog.object :as gobj]))
(:refer-clojure :exclude [set! get get-in assoc!])
(:require
[cuerdas.core :as str]
[goog.object :as gobj]
["lodash/omit" :as omit]))
(defn get
([obj k]
@ -32,6 +35,14 @@
(rest keys)
(unchecked-get res key))))))
(defn without
[obj keys]
(let [keys (cond
(vector? keys) (into-array keys)
(array? keys) keys
:else (throw (js/Error. "unexpected input")))]
(omit obj keys)))
(defn merge!
([a b]
(js/Object.assign a b))
@ -42,3 +53,13 @@
[obj key value]
(unchecked-set obj key value)
obj)
(defn- props-key-fn
[key]
(if (or (= key :class) (= key :class-name))
"className"
(str/camel (name key))))
(defn clj->props
[props]
(clj->js props :keyword-fn props-key-fn))