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:
parent
d0defe5d93
commit
7d5f9c1078
59 changed files with 2712 additions and 1407 deletions
19
backend/resources/emails/change-email/en.mustache
Normal file
19
backend/resources/emails/change-email/en.mustache
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -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(),
|
||||
|
||||
|
|
6
backend/resources/migrations/0008.generic-token.sql
Normal file
6
backend/resources/migrations/0008.generic-token.sql
Normal 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
|
||||
);
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)}))))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
255
frontend/package-lock.json
generated
255
frontend/package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
3
frontend/resources/images/icons/at.svg
Normal file
3
frontend/resources/images/icons/at.svg
Normal 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 |
3
frontend/resources/images/icons/logout.svg
Normal file
3
frontend/resources/images/icons/logout.svg
Normal 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
|
@ -55,10 +55,10 @@ svg {
|
|||
|
||||
a {
|
||||
cursor: pointer;
|
||||
color: $color-primary;
|
||||
color: $color-primary-dark;
|
||||
|
||||
&:hover {
|
||||
color: $color-primary-dark;
|
||||
color: $color-primary;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
0
frontend/resources/styles/main/partials/login.scss
Normal file
0
frontend/resources/styles/main/partials/login.scss
Normal file
55
frontend/resources/styles/main/partials/modal.scss
Normal file
55
frontend/resources/styles/main/partials/modal.scss
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)}))
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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}))
|
||||
|
|
|
@ -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))))))
|
||||
|
||||
|
||||
|
|
|
@ -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?)
|
||||
|
|
|
@ -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
|
||||
|
|
93
frontend/src/uxbox/main/ui/auth.cljs
Normal file
93
frontend/src/uxbox/main/ui/auth.cljs
Normal 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]))
|
86
frontend/src/uxbox/main/ui/auth/login.cljs
Normal file
86
frontend/src/uxbox/main/ui/auth/login.cljs
Normal 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")]]]]])
|
98
frontend/src/uxbox/main/ui/auth/recovery.cljs
Normal file
98
frontend/src/uxbox/main/ui/auth/recovery.cljs
Normal 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")]]]]])
|
||||
|
68
frontend/src/uxbox/main/ui/auth/recovery_request.cljs
Normal file
68
frontend/src/uxbox/main/ui/auth/recovery_request.cljs
Normal 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")]]]]])
|
120
frontend/src/uxbox/main/ui/auth/register.cljs
Normal file
120
frontend/src/uxbox/main/ui/auth/register.cljs
Normal 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")]]]]])
|
150
frontend/src/uxbox/main/ui/components/forms.cljs
Normal file
150
frontend/src/uxbox/main/ui/components/forms.cljs
Normal 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]]))
|
||||
|
||||
|
||||
|
|
@ -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))
|
||||
|
|
|
@ -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]]])
|
|
@ -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
|
||||
|
|
|
@ -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]]])
|
|
@ -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]]])
|
|
@ -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]]])
|
|
@ -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))]]))
|
||||
|
||||
|
||||
|
||||
|
|
102
frontend/src/uxbox/main/ui/settings/change_email.cljs
Normal file
102
frontend/src/uxbox/main/ui/settings/change_email.cljs
Normal 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 "We’ll 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}])]))
|
42
frontend/src/uxbox/main/ui/settings/delete_account.cljs
Normal file
42
frontend/src/uxbox/main/ui/settings/delete_account.cljs
Normal 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")]]]]))
|
|
@ -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")]]]]))
|
||||
|
||||
|
|
|
@ -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")}]
|
||||
]])
|
75
frontend/src/uxbox/main/ui/settings/options.cljs
Normal file
75
frontend/src/uxbox/main/ui/settings/options.cljs
Normal 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}]]]))
|
|
@ -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}]]]))
|
||||
|
|
|
@ -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}]]]))
|
||||
|
|
|
@ -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))
|
||||
[]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
Loading…
Add table
Reference in a new issue