Merge pull request #225 from uxbox/23/user-profile

23/user profile
This commit is contained in:
Hirunatan 2020-05-26 13:05:19 +02:00 committed by GitHub
commit 288e8e061c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
85 changed files with 3243 additions and 1558 deletions

View File

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

View File

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

View File

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

View File

@ -32,6 +32,7 @@ VALUES ('00000000-0000-0000-0000-000000000000'::uuid,
'!');
--- NOTE: this table is deleted in the next migrations
CREATE TABLE profile_email (
profile_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE,
@ -121,6 +122,7 @@ BEFORE UPDATE ON profile_attr
FOR EACH ROW EXECUTE PROCEDURE update_modified_at();
--- NOTE: this table is removed in the following migrations
CREATE TABLE password_recovery_token (
profile_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE,

View File

@ -0,0 +1,14 @@
--- Delete previously token related tables
DROP TABLE password_recovery_token;
--- Create a new generic table for store tokens.
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
);
COMMENT ON TABLE generic_token IS 'Table for generic tokens storage';

View File

@ -0,0 +1,6 @@
DROP INDEX profile_email__profile_id__idx;
DROP INDEX profile_email__email__idx;
DROP TABLE profile_email;
ALTER TABLE profile
ADD COLUMN pending_email text NULL;

View File

@ -0,0 +1,14 @@
DROP TABLE session;
CREATE TABLE http_session (
id text PRIMARY KEY,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
modified_at timestamptz NOT NULL DEFAULT clock_timestamp(),
profile_id uuid REFERENCES profile(id) ON DELETE CASCADE,
user_agent text NULL
);
CREATE INDEX http_session__profile_id__idx
ON http_session(profile_id);

View File

@ -26,6 +26,9 @@
:database-username "uxbox"
:database-password "uxbox"
:public-uri "http://localhost:3449"
:backend-uri "http://localhost:6060"
:redis-uri "redis://redis/0"
:media-directory "resources/public/media"
:assets-directory "resources/public/static"
@ -67,11 +70,19 @@
(s/def ::registration-enabled ::us/boolean)
(s/def ::registration-domain-whitelist ::us/string)
(s/def ::debug-humanize-transit ::us/boolean)
(s/def ::public-uri ::us/string)
(s/def ::backend-uri ::us/string)
(s/def ::google-client-id ::us/string)
(s/def ::google-client-secret ::us/string)
(s/def ::config
(s/keys :opt-un [::http-server-cors
::http-server-debug
::http-server-port
::google-client-id
::google-client-secret
::public-uri
::database-username
::database-password
::database-uri

View File

@ -75,8 +75,10 @@
(jdbc/get-connection pool))
(defn exec!
[ds sv]
(jdbc/execute! ds sv {:builder-fn as-kebab-maps}))
([ds sv]
(exec! ds sv {}))
([ds sv opts]
(jdbc/execute! ds sv (assoc opts :builder-fn as-kebab-maps))))
(defn exec-one!
([ds sv] (exec-one! ds sv {}))
@ -120,6 +122,15 @@
([ds table id opts]
(get-by-params ds table {:id id} opts)))
(defn query
([ds table params]
(query ds table params nil))
([ds table params opts]
(let [opts (cond-> (merge default-options opts)
(:for-update opts)
(assoc :suffix "for update"))]
(exec! ds (jdbc-bld/for-query table params opts) opts))))
(defn pgobject?
[v]
(instance? PGobject v))

View File

@ -2,7 +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-2019 Andrey Antukh <niwi@niwi.nz>
;; 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.emails
"Main api for send emails."
@ -62,3 +65,11 @@
(def password-recovery
"A password recovery notification email."
(emails/build ::password-recovery default-context))
(s/def ::pending-email ::us/email)
(s/def ::change-email
(s/keys :req-un [::name ::pending-email ::token]))
(def change-email
"Password change confirmation email"
(emails/build ::change-email default-context))

View File

@ -17,7 +17,7 @@
[uxbox.db :as db]
[uxbox.media :as media]
[uxbox.migrations]
[uxbox.services.mutations.profile :as mt.profile]
[uxbox.services.mutations.profile :as profile]
[uxbox.util.blob :as blob]))
(defn- mk-uuid
@ -66,6 +66,11 @@
[f items]
(reduce #(conj %1 (f %2)) [] items))
(defn- register-profile
[conn params]
(->> (#'profile/create-profile conn params)
(#'profile/create-profile-relations conn)))
(defn impl-run
[opts]
(let [rng (java.util.Random. 1)
@ -74,12 +79,12 @@
(fn [conn index]
(let [id (mk-uuid "profile" index)]
(log/info "create profile" id)
(mt.profile/register-profile conn
{:id id
:fullname (str "Profile " index)
:password "123123"
:demo? true
:email (str "profile" index ".test@uxbox.io")})))
(register-profile conn
{:id id
:fullname (str "Profile " index)
:password "123123"
:demo? true
:email (str "profile" index ".test@uxbox.io")})))
create-profiles
(fn [conn]

View File

@ -2,7 +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) 2019 Andrey Antukh <niwi@niwi.nz>
;; 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.http
(:require
@ -14,6 +17,8 @@
[uxbox.http.debug :as debug]
[uxbox.http.errors :as errors]
[uxbox.http.handlers :as handlers]
[uxbox.http.auth :as auth]
[uxbox.http.auth.google :as google]
[uxbox.http.middleware :as middleware]
[uxbox.http.session :as session]
[uxbox.http.ws :as ws]
@ -31,12 +36,17 @@
[middleware/multipart-params]
[middleware/keyword-params]
[middleware/cookies]]}
["/oauth"
["/google" {:post google/auth}]
["/google/callback" {:get google/callback}]]
["/echo" {:get handlers/echo-handler
:post handlers/echo-handler}]
["/login" {:handler handlers/login-handler
["/login" {:handler auth/login-handler
:method :post}]
["/logout" {:handler handlers/logout-handler
["/logout" {:handler auth/logout-handler
:method :post}]
["/w" {:middleware [session/auth]}

View File

@ -0,0 +1,33 @@
;; 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.http.auth
(:require
[uxbox.common.exceptions :as ex]
[uxbox.common.uuid :as uuid]
[uxbox.http.session :as session]
[uxbox.services.mutations :as sm]))
(defn login-handler
[req]
(let [data (:body-params req)
uagent (get-in req [:headers "user-agent"])]
(let [profile (sm/handle (assoc data ::sm/type :login))
id (session/create (:id profile) uagent)]
{:status 200
:cookies (session/cookies id)
:body profile})))
(defn logout-handler
[req]
(some-> (session/extract-auth-token req)
(session/delete))
{:status 200
:cookies (session/cookies "" {:max-age -1})
:body ""})

View File

@ -0,0 +1,133 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.http.auth.google
(:require
[clojure.data.json :as json]
[clojure.tools.logging :as log]
[lambdaisland.uri :as uri]
[uxbox.common.exceptions :as ex]
[uxbox.config :as cfg]
[uxbox.db :as db]
[uxbox.services.tokens :as tokens]
[uxbox.services.mutations :as sm]
[uxbox.http.session :as session]
[uxbox.util.http :as http]))
(def base-goauth-uri "https://accounts.google.com/o/oauth2/v2/auth")
(def scope
(str "email profile "
"https://www.googleapis.com/auth/userinfo.email "
"https://www.googleapis.com/auth/userinfo.profile "
"openid"))
(defn- build-redirect-url
[]
(let [public (uri/uri (:backend-uri cfg/config))]
(str (assoc public :path "/api/oauth/google/callback"))))
(defn- get-access-token
[code]
(let [params {:code code
:client_id (:google-client-id cfg/config)
:client_secret (:google-client-secret cfg/config)
:redirect_uri (build-redirect-url)
:grant_type "authorization_code"}
req {:method :post
:headers {"content-type" "application/x-www-form-urlencoded"}
:uri "https://oauth2.googleapis.com/token"
:body (uri/map->query-string params)}
res (http/send! req)]
(when (not= 200 (:status res))
(ex/raise :type :internal
:code :invalid-response-from-google
:context {:status (:status res)
:body (:body res)}))
(try
(let [data (json/read-str (:body res))]
(get data "access_token"))
(catch Throwable e
(log/error "unexpected error on parsing response body from google access tooken request" e)
nil))))
(defn- get-user-info
[token]
(let [req {:uri "https://openidconnect.googleapis.com/v1/userinfo"
:headers {"Authorization" (str "Bearer " token)}
:method :get}
res (http/send! req)]
(when (not= 200 (:status res))
(ex/raise :type :internal
:code :invalid-response-from-google
:context {:status (:status res)
:body (:body res)}))
(try
(let [data (json/read-str (:body res))]
;; (clojure.pprint/pprint data)
{:email (get data "email")
:fullname (get data "name")})
(catch Throwable e
(log/error "unexpected error on parsing response body from google access tooken request" e)
nil))))
(defn auth
[req]
(let [token (tokens/create! db/pool {:type :google-oauth})
params {:scope scope
:access_type "offline"
:include_granted_scopes true
:state token
:response_type "code"
:redirect_uri (build-redirect-url)
:client_id (:google-client-id cfg/config)}
query (uri/map->query-string params)
uri (-> (uri/uri base-goauth-uri)
(assoc :query query))]
{:status 200
:body {:redirect-uri (str uri)}}))
(defn callback
[req]
(let [token (get-in req [:params :state])
tdata (tokens/retrieve db/pool token)
info (some-> (get-in req [:params :code])
(get-access-token)
(get-user-info))]
(when (not= :google-oauth (:type tdata))
(ex/raise :type :validation
:code ::tokens/invalid-token))
(when-not info
(ex/raise :type :authentication
:code ::unable-to-authenticate-with-google))
(let [profile (sm/handle {::sm/type :login-or-register
:email (:email info)
:fullname (:fullname info)})
uagent (get-in req [:headers "user-agent"])
tdata {:type :authentication
:profile profile}
token (tokens/create! db/pool tdata {:valid {:minutes 10}})
uri (-> (uri/uri (:public-uri cfg/config))
(assoc :path "/#/auth/verify-token")
(assoc :query (uri/map->query-string {:token token})))
sid (session/create (:id profile) uagent)]
{:status 302
:headers {"location" (str uri)}
:cookies (session/cookies sid)
:body ""})))

View File

@ -2,12 +2,14 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
;; 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.http.handlers
(:require
[uxbox.common.exceptions :as ex]
[uxbox.common.uuid :as uuid]
[uxbox.emails :as emails]
[uxbox.http.session :as session]
[uxbox.services.init]
@ -18,6 +20,7 @@
#{:create-demo-profile
:logout
:profile
:verify-profile-token
:recover-profile
:register-profile
:request-profile-recovery
@ -50,31 +53,20 @@
(: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-> (session/extract-auth-token req)
(session/delete))
{:status 204
:cookies (session/cookies "" {:max-age -1})
:body ""})
{:status 200
:body body}))
{:status 403
:body {:type :authentication
:code :unauthorized}})))
(defn login-handler
[req]
(let [data (:body-params req)
user-agent (get-in req [:headers "user-agent"])]
(let [profile (sm/handle (assoc data ::sm/type :login))
token (session/create (:id profile) user-agent)]
{:status 200
:cookies {"auth-token" {:value token :path "/"}}
:body profile})))
(defn logout-handler
[req]
(some-> (get-in req [:cookies "auth-token"])
(uuid/uuid)
(session/delete))
{:status 204
:cookies {"auth-token" nil}
:body ""})
(defn echo-handler
[req]
{:status 200

View File

@ -2,49 +2,51 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
;; 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.http.session
(:require
[uxbox.db :as db]
[uxbox.services.tokens :as tokens]
[uxbox.common.uuid :as uuid]))
;; --- Main API
(defn retrieve
"Retrieves a user id associated with the provided auth token."
[token]
(when token
(let [row (db/get-by-params db/pool :session {:id token})]
(:profile-id row))))
(-> (db/query db/pool :http-session {:id token})
(first)
(:profile-id))))
(defn create
[user-id user-agent]
(let [id (uuid/random)]
(db/insert! db/pool :session {:id id
:profile-id user-id
:user-agent user-agent})
(str id)))
[profile-id user-agent]
(let [id (tokens/next)]
(db/insert! db/pool :http-session {:id id
:profile-id profile-id
:user-agent user-agent})
id))
(defn delete
[token]
(db/delete! db/pool :session {:id token})
(db/delete! db/pool :http-session {:id token})
nil)
;; --- Interceptor
(defn cookies
([id] (cookies id {}))
([id opts]
{"auth-token" (merge opts {:value id :path "/" :http-only true})}))
(defn- parse-token
[request]
(try
(when-let [token (get-in request [:cookies "auth-token"])]
(uuid/uuid (:value token)))
(catch java.lang.IllegalArgumentException e
nil)))
(defn extract-auth-token
[req]
(get-in req [:cookies "auth-token" :value]))
(defn wrap-auth
[handler]
(fn [request]
(let [token (parse-token request)
(let [token (get-in request [:cookies "auth-token" :value])
profile-id (retrieve token)]
(if profile-id
(handler (assoc request :profile-id profile-id))

View File

@ -5,7 +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) 2020 UXBOX Labs SL
(ns uxbox.main
(:require

View File

@ -2,7 +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 Andrey Antukh <niwi@niwi.nz>
;; 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.migrations
(:require
@ -15,27 +18,45 @@
(def +migrations+
{:name "uxbox-main"
:steps
[{:desc "Initial triggers and utils."
:name "0001-main"
:fn (mg/resource "migrations/0001.main.sql")}
{:desc "Initial auth related tables"
:name "0002-users"
:fn (mg/resource "migrations/0002.users.sql")}
{:desc "Initial projects tables"
:name "0003-projects"
:fn (mg/resource "migrations/0003.projects.sql")}
{:desc "Initial tasks related tables"
:name "0004-tasks"
:fn (mg/resource "migrations/0004.tasks.sql")}
{:desc "Initial libraries tables"
:name "0005-libraries"
:fn (mg/resource "migrations/0005.libraries.sql")}
{:desc "Initial presence tables"
: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")}]})
[{:desc "Add initial extensions and functions."
:name "0001-add-extensions"
:fn (mg/resource "migrations/0001-add-extensions.sql")}
{:desc "Add profile related tables"
:name "0002-add-profile-tables"
:fn (mg/resource "migrations/0002-add-profile-tables.sql")}
{:desc "Add project related tables"
:name "0003-add-project-tables"
:fn (mg/resource "migrations/0003-add-project-tables.sql")}
{:desc "Add tasks related tables"
:name "0004-add-tasks-tables"
:fn (mg/resource "migrations/0004-add-tasks-tables.sql")}
{:desc "Add libraries related tables"
:name "0005-add-libraries-tables"
:fn (mg/resource "migrations/0005-add-libraries-tables.sql")}
{:desc "Add presence related tables"
:name "0006-add-presence-tables"
:fn (mg/resource "migrations/0006-add-presence-tables.sql")}
{:desc "Drop version field from page table."
:name "0007-drop-version-field-from-page-table"
:fn (mg/resource "migrations/0007-drop-version-field-from-page-table.sql")}
{:desc "Add generic token related tables."
:name "0008-add-generic-token-table"
:fn (mg/resource "migrations/0008-add-generic-token-table.sql")}
{:desc "Drop the profile_email table"
:name "0009-drop-profile-email-table"
:fn (mg/resource "migrations/0009-drop-profile-email-table.sql")}
{:desc "Add new HTTP session table"
:name "0010-add-http-session-table"
:fn (mg/resource "migrations/0010-add-http-session-table.sql")}]})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Entry point

View File

@ -29,13 +29,15 @@
email (str "demo-" sem ".demo@nodomain.com")
fullname (str "Demo User " sem)
password (-> (sodi.prng/random-bytes 12)
(sodi.util/bytes->b64s))]
(sodi.util/bytes->b64s))
params {:id id
:email email
:fullname fullname
:demo? true
:password password}]
(db/with-atomic [conn db/pool]
(#'profile/register-profile conn {:id id
:email email
:fullname fullname
:demo? true
:password password})
(->> (#'profile/create-profile conn params)
(#'profile/create-profile-relations conn))
;; Schedule deletion of the demo profile
(tasks/submit! conn {:name "delete-profile"

View File

@ -25,10 +25,11 @@
[uxbox.emails :as emails]
[uxbox.images :as images]
[uxbox.media :as media]
[uxbox.services.tokens :as tokens]
[uxbox.services.mutations :as sm]
[uxbox.services.mutations.images :as imgs]
[uxbox.services.mutations.projects :as mt.projects]
[uxbox.services.mutations.teams :as mt.teams]
[uxbox.services.mutations.projects :as projects]
[uxbox.services.mutations.teams :as teams]
[uxbox.services.queries.profile :as profile]
[uxbox.tasks :as tasks]
[uxbox.util.blob :as blob]
@ -46,6 +47,100 @@
(s/def ::old-password ::us/string)
(s/def ::theme ::us/string)
;; --- Mutation: Register Profile
(declare check-profile-existence!)
(declare create-profile)
(declare create-profile-relations)
(s/def ::register-profile
(s/keys :req-un [::email ::password ::fullname]))
(defn email-domain-in-whitelist?
"Returns true if email's domain is in the given whitelist or if given
whitelist is an empty string."
[whitelist email]
(if (str/blank? whitelist)
true
(let [domains (str/split whitelist #",\s*")
email-domain (second (str/split email #"@"))]
(contains? (set domains) email-domain))))
(sm/defmutation ::register-profile
[params]
(when-not (:registration-enabled cfg/config)
(ex/raise :type :restriction
: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 (->> (create-profile conn params)
(create-profile-relations conn))
payload {:type :verify-email
:profile-id (:id profile)
:email (:email profile)}
token (tokens/create! conn payload {:valid {:days 30}})]
(emails/send! conn emails/register
{:to (:email profile)
:name (:fullname profile)
:public-url (:public-uri cfg/config)
:token token})
profile)))
(def ^:private sql:profile-existence
"select exists (select * from profile
where email = ?
and deleted_at is null) as val")
(defn- check-profile-existence!
[conn {:keys [email] :as params}]
(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))
params))
(defn- create-profile
"Create the profile entry on the database with limited input
filling all the other fields with defaults."
[conn {:keys [id fullname email password demo?] :as params}]
(let [id (or id (uuid/next))
demo? (if (boolean? demo?) demo? false)
password (sodi.pwhash/derive password)]
(db/insert! conn :profile
{:id id
:fullname fullname
:email (str/lower email)
:pending-email (if demo? nil email)
:photo ""
:password password
:is-demo demo?})))
(defn- create-profile-relations
[conn profile]
(let [team (teams/create-team conn {:profile-id (:id profile)
:name "Default"
:default? true})
proj (projects/create-project conn {:profile-id (:id profile)
:team-id (:id team)
:name "Drafts"
:default? true})]
(teams/create-team-profile conn {:team-id (:id team)
:profile-id (:id profile)})
(projects/create-project-profile conn {:project-id (:id proj)
:profile-id (:id profile)})
(merge (profile/strip-private-attrs profile)
{:default-team-id (:id team)
:default-project-id (:id proj)})))
;; --- Mutation: Login
@ -64,7 +159,7 @@
(let [result (sodi.pwhash/verify password (:password profile))]
(:valid result)))
(check-profile [profile]
(validate-profile [profile]
(when-not profile
(ex/raise :type :validation
:code ::wrong-credentials))
@ -74,27 +169,55 @@
profile)]
(db/with-atomic [conn db/pool]
(let [prof (-> (retrieve-profile-by-email conn email)
(check-profile)
(validate-profile)
(profile/strip-private-attrs))
addt (profile/retrieve-additional-data conn (:id prof))]
(merge prof addt)))))
(def sql:profile-by-email
"select * from profile
where email=? and deleted_at is null
for update")
(defn- retrieve-profile-by-email
[conn email]
(db/get-by-params conn :profile {:email email} {:for-update true}))
(let [email (str/lower email)]
(db/exec-one! conn [sql:profile-by-email email])))
;; --- Mutation: Register if not exists
(sm/defmutation ::login-or-register
[{:keys [email fullname] :as params}]
(letfn [(populate-additional-data [conn profile]
(let [data (profile/retrieve-additional-data conn (:id profile))]
(merge profile data)))
(create-profile [conn {:keys [fullname email]}]
(db/insert! conn :profile
{:id (uuid/next)
:fullname fullname
:email (str/lower email)
:pending-email nil
:photo ""
:password "!"
:is-demo false}))
(register-profile [conn params]
(->> (create-profile conn params)
(create-profile-relations conn)))]
(db/with-atomic [conn db/pool]
(let [profile (retrieve-profile-by-email conn email)
profile (if profile
(populate-additional-data conn profile)
(register-profile conn params))]
(profile/strip-private-attrs profile)))))
;; --- 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 +240,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,145 +302,140 @@
(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
;; --- Mutation: Request Email Change
(declare check-profile-existence!)
(declare register-profile)
(declare select-profile-for-update)
(s/def ::register-profile
(s/keys :req-un [::email ::password ::fullname]))
(s/def ::request-email-change
(s/keys :req-un [::email]))
(defn email-domain-in-whitelist?
"Returns true if email's domain is in the given whitelist or if given
whitelist is an empty string."
[whitelist email]
(if (str/blank? whitelist)
true
(let [domains (str/split whitelist #",\s*")
email-domain (second (str/split email #"@"))]
(contains? (set domains) email-domain))))
(sm/defmutation ::register-profile
[params]
(when-not (:registration-enabled cfg/config)
(ex/raise :type :restriction
: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))
(sm/defmutation ::request-email-change
[{:keys [profile-id email] :as params}]
(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 [email (str/lower email)
profile (select-profile-for-update conn profile-id)
payload {:type :change-email
:profile-id profile-id
:email email}
(def ^:private sql:insert-profile
"insert into profile (id, fullname, email, password, photo, is_demo)
values ($1, $2, $3, $4, '', $5) returning *")
token (tokens/create! conn payload)]
(def ^:private sql:insert-email
"insert into profile_email (profile_id, email, is_main)
values ($1, $2, true)")
(when (not= email (:email profile))
(check-profile-existence! conn params))
(def ^:private sql:profile-existence
"select exists (select * from profile
where email = $1
and deleted_at is null) as val")
(db/update! conn :profile
{:pending-email email}
{:id profile-id})
(defn- check-profile-existence!
[conn {:keys [email] :as params}]
(let [result (db/exec-one! conn [sql:profile-existence email])]
(when (:val result)
(ex/raise :type :validation
:code ::email-already-exists))
params))
(emails/send! conn emails/change-email
{:to (:email profile)
:name (:fullname profile)
:public-url (:public-uri cfg/config)
:pending-email email
:token token})
nil)))
(defn- create-profile
"Create the profile entry on the database with limited input
filling all the other fields with defaults."
[conn {:keys [id fullname email password demo?] :as params}]
(let [id (or id (uuid/next))
demo? (if (boolean? demo?) demo? false)
password (sodi.pwhash/derive password)]
(db/insert! conn :profile
{:id id
:fullname fullname
:email email
:photo ""
:password password
:is-demo demo?})))
(defn- select-profile-for-update
[conn id]
(db/get-by-id conn :profile id {:for-update true}))
(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)
;; --- Mutation: Verify Profile Token
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)})
;; Generic mutation for perform token based verification for auth
;; domain.
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)})
]
(merge (profile/strip-private-attrs prof)
{:default-team (:id team)
:default-project (:id proj)})))
(s/def ::verify-profile-token
(s/keys :req-un [::token]))
(sm/defmutation ::verify-profile-token
[{:keys [token] :as params}]
(letfn [(handle-email-change [conn tdata]
(let [profile (select-profile-for-update conn (:profile-id tdata))]
(when (not= (:email tdata)
(: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)})
tdata))
(handle-email-verify [conn tdata]
(let [profile (select-profile-for-update conn (:profile-id tdata))]
(when (or (not= (:email profile)
(:pending-email profile))
(not= (:email profile)
(:email tdata)))
(ex/raise :type :validation
:code ::tokens/invalid-token))
(db/update! conn :profile
{:pending-email nil}
{:id (:id profile)})
tdata))]
(db/with-atomic [conn db/pool]
(let [tdata (tokens/retrieve conn token {:delete true})]
(tokens/delete! conn token)
(case (:type tdata)
:change-email (handle-email-change conn tdata)
:verify-email (handle-email-verify conn tdata)
:authentication tdata
(ex/raise :type :validation
:code ::tokens/invalid-token))))))
;; --- 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 [payload {:type :password-recovery-token
:profile-id id}
token (tokens/create! conn payload)]
(assoc profile :token token)))
(send-email-notification [conn profile]
(emails/send! conn emails/password-recovery
{:to (:email profile)
:public-url (:public-uri 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 +444,28 @@
(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 [tpayload (tokens/retrieve conn token)]
(when (not= (:type tpayload) :password-recovery-token)
(ex/raise :type :validation
:code ::tokens/invalid-token))
(:profile-id tpayload)))
(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 +510,6 @@
(let [rows (db/exec! conn [sql:teams-ownership-check profile-id])]
(when-not (empty? rows)
(ex/raise :type :validation
:code :owner-teams-with-people
:code ::owner-teams-with-people
:hint "The user need to transfer ownership of owned teams."
:context {:teams (mapv :team-id rows)}))))

View File

@ -73,8 +73,7 @@
(defn retrieve-profile-data
[conn id]
(let [sql "select * from profile where id=? and deleted_at is null"]
(db/exec-one! conn [sql id])))
(db/get-by-id conn :profile id))
(defn retrieve-profile
[conn id]
@ -92,5 +91,5 @@
(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]))
[row]
(dissoc row :password :deleted-at))

View File

@ -0,0 +1,80 @@
;; 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.services.tokens
(:refer-clojure :exclude [next])
(:require
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[sodi.prng]
[sodi.util]
[uxbox.common.exceptions :as ex]
[uxbox.common.spec :as us]
[uxbox.common.uuid :as uuid]
[uxbox.config :as cfg]
[uxbox.util.time :as dt]
[uxbox.util.blob :as blob]
[uxbox.db :as db]))
(defn next
([] (next 64))
([n]
(-> (sodi.prng/random-bytes n)
(sodi.util/bytes->b64s))))
(def default-duration
(dt/duration {:hours 48}))
(defn- decode-row
[{:keys [content] :as row}]
(when row
(cond-> row
content (assoc :content (blob/decode content)))))
(defn create!
([conn payload] (create! conn payload {}))
([conn payload {:keys [valid] :or {valid default-duration}}]
(let [token (next)
until (dt/plus (dt/now) (dt/duration valid))]
(db/insert! conn :generic-token
{:content (blob/encode payload)
:token token
:valid-until until})
token)))
(defn delete!
[conn token]
(db/delete! conn :generic-token {:token token}))
(defn retrieve
([conn token] (retrieve conn token {}))
([conn token {:keys [delete] :or {delete false}}]
(let [row (->> (db/query conn :generic-token {:token token})
(map decode-row)
(first))]
(when-not row
(ex/raise :type :validation
:code ::invalid-token))
;; Validate the token expiration
(when (> (inst-ms (dt/now))
(inst-ms (:valid-until row)))
(ex/raise :type :validation
:code ::invalid-token))
(when delete
(db/delete! conn :generic-token {:token token}))
(-> row
(dissoc :content)
(merge (:content row))))))

View File

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

View File

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

View File

@ -82,10 +82,12 @@
(defn create-profile
[conn i]
(#'profile/register-profile conn {:id (mk-uuid "profile" i)
:fullname (str "Profile " i)
:email (str "profile" i ".test@nodomain.com")
:password "123123"}))
(let [params {:id (mk-uuid "profile" i)
:fullname (str "Profile " i)
:email (str "profile" i ".test@nodomain.com")
:password "123123"}]
(->> (#'profile/create-profile conn params)
(#'profile/create-profile-relations conn))))
(defn create-team
[conn profile-id i]

View File

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

View File

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

View File

@ -94,8 +94,9 @@ function readLocales() {
}
function readConfig(data) {
const publicURL = process.env.UXBOX_PUBLIC_URL;
const backendURL = process.env.UXBOX_BACKEND_URL;
const googleClientID = process.env.UXBOX_GOOGLE_CLIENT_ID;
const publicURI = process.env.UXBOX_PUBLIC_URI;
const backendURI = process.env.UXBOX_BACKEND_URI;
const demoWarn = process.env.UXBOX_DEMO_WARNING;
const deployDate = process.env.UXBOX_DEPLOY_DATE;
const deployCommit = process.env.UXBOX_DEPLOY_COMMIT;
@ -104,12 +105,16 @@ function readConfig(data) {
demoWarning: demoWarn === "true"
};
if (publicURL !== undefined) {
cfg.publicURL = publicURL;
if (googleClientID !== undefined) {
cfg.googleClientID = googleClientID;
}
if (backendURL !== undefined) {
cfg.backendURL = backendURL;
if (publicURI !== undefined) {
cfg.publicURI = publicURI;
}
if (backendURI !== undefined) {
cfg.backendURI = backendURI;
}
if (deployDate !== undefined) {

View File

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

View File

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

View File

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

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

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

After

Width:  |  Height:  |  Size: 271 B

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -5,115 +5,56 @@
// 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;
}
.btn-google-auth {
margin-bottom: $medium;
text-decoration: none;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -12,12 +12,13 @@
(this-as global
(let [config (obj/get global "uxboxConfig")
purl (obj/get config "publicURL" "http://localhost:3449")
burl (obj/get config "backendURL" "http://localhost:6060")
puri (obj/get config "publicURI" "http://localhost:3449")
buri (obj/get config "backendURI" "http://localhost:6060")
gcid (obj/get config "googleClientID" true)
warn (obj/get config "demoWarning" true)]
(def default-language "en")
(def demo-warning warn)
(def url burl)
(def backend-url burl)
(def public-url purl)
(def backend-uri buri)
(def google-client-id gcid)
(def public-uri puri)
(def default-theme "default")))

View File

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

View File

@ -5,7 +5,7 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2015-2019 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.data.auth
(:require
@ -34,7 +34,7 @@
(watch [this state stream]
(let [team-id (:default-team-id data)]
(rx/of (du/profile-fetched data)
(rt/navigate :dashboard-team {:team-id team-id}))))))
(rt/nav :dashboard-team {:team-id team-id}))))))
;; --- Login
@ -51,13 +51,31 @@
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))))))
(defn login-from-token
[{:keys [profile] :as tdata}]
(ptk/reify ::login-from-token
ptk/UpdateEvent
(update [_ state]
(merge state (dissoc initial-state :route :router)))
ptk/WatchEvent
(watch [this state s]
(let [team-id (:default-team-id profile)]
(rx/of (du/profile-fetched profile)
(rt/nav' :dashboard-team {:team-id team-id}))))))
;; --- Logout
@ -81,8 +99,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 +111,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 +149,43 @@
(s/keys :req-un [::email]))
(defn request-profile-recovery
[data on-success]
[data]
(us/verify ::recovery-request data)
(us/verify fn? on-success)
(ptk/reify ::request-profile-recovery
ptk/WatchEvent
(watch [_ state stream]
(letfn [(on-error [{payload :payload}]
(rx/empty))]
(let [{:keys [on-error on-success]
:or {on-error identity
on-success identity}} (meta data)]
(->> (rp/mutation :request-profile-recovery data)
(rx/tap on-success)
(rx/catch rp/client-error? on-error))))))
(rx/catch (fn [err]
(on-error err)
(rx/empty))))))))
;; --- Recovery (Password)
(s/def ::token string?)
(s/def ::on-error fn?)
(s/def ::on-success fn?)
(s/def ::recover-profile
(s/keys :req-un [::password ::token ::on-error ::on-success]))
(s/keys :req-un [::password ::token]))
(defn recover-profile
[{:keys [token password on-error on-success] :as data}]
[{:keys [token password] :as data}]
(us/verify ::recover-profile data)
(ptk/reify ::recover-profile
ptk/WatchEvent
(watch [_ state stream]
(->> (rp/mutation :recover-profile {:token token :password password})
(rx/tap on-success)
(rx/catch (fn [err]
(on-error)
(rx/empty)))))))
(let [{:keys [on-error on-success]
:or {on-error identity
on-success identity}} (meta data)]
(->> (rp/mutation :recover-profile data)
(rx/tap on-success)
(rx/catch (fn [err]
(on-error)
(rx/empty))))))))
;; --- Create Demo Profile

View File

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

View File

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

View File

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

View File

@ -42,9 +42,9 @@
ptk/UpdateEvent
(update [_ state]
(let [sid (:session-id state)
url (ws/url "/ws/notifications" {:file-id file-id
uri (ws/uri "/ws/notifications" {:file-id file-id
:session-id sid})]
(assoc-in state [:ws file-id] (ws/open url))))
(assoc-in state [:ws file-id] (ws/open uri))))
ptk/WatchEvent
(watch [_ state stream]

View File

@ -11,7 +11,7 @@
(:require
[beicon.core :as rx]
[cuerdas.core :as str]
[uxbox.config :refer [url]]
[uxbox.config :as cfg]
[uxbox.util.http-api :as http]))
(defn- handle-response
@ -29,14 +29,14 @@
(defn send-query!
[id params]
(let [url (str url "/api/w/query/" (name id))]
(->> (http/send! {:method :get :url url :query params})
(let [uri (str cfg/backend-uri "/api/w/query/" (name id))]
(->> (http/send! {:method :get :uri uri :query params})
(rx/mapcat handle-response))))
(defn send-mutation!
[id params]
(let [url (str url "/api/w/mutation/" (name id))]
(->> (http/send! {:method :post :url url :body params})
(let [uri (str cfg/backend-uri "/api/w/mutation/" (name id))]
(->> (http/send! {:method :post :uri uri :body params})
(rx/mapcat handle-response))))
(defn- dispatch
@ -62,6 +62,12 @@
([id] (mutation id {}))
([id params] (mutation id params)))
(defmethod mutation :login-with-google
[id params]
(let [uri (str cfg/backend-uri "/api/oauth/google")]
(->> (http/send! {:method :post :uri uri})
(rx/mapcat handle-response))))
(defmethod mutation :upload-image
[id params]
(let [form (js/FormData.)]
@ -88,14 +94,14 @@
(defmethod mutation :login
[id params]
(let [url (str url "/api/login")]
(->> (http/send! {:method :post :url url :body params})
(let [uri (str cfg/backend-uri "/api/login")]
(->> (http/send! {:method :post :uri uri :body params})
(rx/mapcat handle-response))))
(defmethod mutation :logout
[id params]
(let [url (str url "/api/logout")]
(->> (http/send! {:method :post :url url :body params :auth false})
(let [uri (str cfg/backend-uri "/api/logout")]
(->> (http/send! {:method :post :uri uri :body params})
(rx/mapcat handle-response))))
(def client-error? http/client-error?)

View File

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

View File

@ -0,0 +1,99 @@
;; 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.auth :as da]
[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 {:href "/#/"} 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)))
(defn- handle-authentication
[tdata]
(st/emit! (da/login-from-token tdata)))
(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 [tdata]
(case (:type tdata)
:verify-email (handle-email-verified tdata)
:change-email (handle-email-changed tdata)
:authentication (handle-authentication tdata)
nil))
(fn [error]
(case (:code error)
:uxbox.services.mutations.profile/email-already-exists
(let [msg (tr "errors.email-already-exists")]
(ts/schedule 100 #(st/emit! (dm/error msg)))
(st/emit! (rt/nav :settings-profile)))
(let [msg (tr "errors.generic")]
(ts/schedule 100 #(st/emit! (dm/error msg)))
(st/emit! (rt/nav :settings-profile)))))))))
[:div.verify-token
i/loader-pencil]))

View File

@ -0,0 +1,103 @@
;; 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]
[beicon.core :as rx]
[rumext.alpha :as mf]
[uxbox.config :as cfg]
[uxbox.common.spec :as us]
[uxbox.main.ui.icons :as i]
[uxbox.main.data.auth :as da]
[uxbox.main.repo :as rp]
[uxbox.main.store :as st]
[uxbox.main.data.messages :as dm]
[uxbox.main.ui.components.forms :refer [input submit-button form]]
[uxbox.util.object :as obj]
[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))))
(defn- login-with-google
[event]
(dom/prevent-default event)
(->> (rp/mutation! :login-with-google {})
(rx/subs (fn [{:keys [redirect-uri] :as rsp}]
(.replace js/location redirect-uri)))))
(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}]
(when cfg/google-client-id
[:a.btn-secondary.btn-large.btn-google-auth
{:on-click login-with-google}
"Login with google"])
[:div.links
[:div.link-entry
[:a {:on-click #(st/emit! (rt/nav :auth-recovery-request))
:tab-index "5"}
(t locale "auth.forgot-password")]]
[:div.link-entry
[:span (t locale "auth.register-label") " "]
[:a {:on-click #(st/emit! (rt/nav :auth-register))
:tab-index "6"}
(t locale "auth.register")]]
[:div.link-entry
[:span (t locale "auth.create-demo-profile-label") " "]
[:a {:on-click #(st/emit! da/create-demo-profile)
:tab-index "6"}
(t locale "auth.create-demo-profile")]]]]])

View File

@ -0,0 +1,98 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.auth.recovery
(:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[rumext.alpha :as mf]
[uxbox.main.ui.icons :as i]
[uxbox.common.spec :as us]
[uxbox.main.data.auth :as uda]
[uxbox.main.data.messages :as dm]
[uxbox.main.store :as st]
[uxbox.main.ui.components.forms :refer [input submit-button form]]
[uxbox.main.ui.messages :refer [messages]]
[uxbox.main.ui.navigation :as nav]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :as i18n :refer [t tr]]
[uxbox.util.router :as rt]))
(s/def ::password-1 ::fm/not-empty-string)
(s/def ::password-2 ::fm/not-empty-string)
(s/def ::token ::fm/not-empty-string)
(s/def ::recovery-form
(s/keys :req-un [::password-1
::password-2]))
(defn- password-equality
[data]
(let [password-1 (:password-1 data)
password-2 (:password-2 data)]
(cond-> {}
(and password-1 password-2
(not= password-1 password-2))
(assoc :password-2 {:message "errors.password-invalid-confirmation"})
(and password-1 (> 8 (count password-1)))
(assoc :password-1 {:message "errors.password-too-short"}))))
(defn- on-error
[form error]
(st/emit! (dm/error (tr "auth.notifications.invalid-token-error"))))
(defn- on-success
[_]
(st/emit! (dm/info (tr "auth.notifications.password-changed-succesfully"))
(rt/nav :auth-login)))
(defn- on-submit
[form event]
(let [params (with-meta {:token (get-in form [:clean-data :token])
:password (get-in form [:clean-data :password-2])}
{:on-error (partial on-error form)
:on-success (partial on-success form)})]
(st/emit! (uda/recover-profile params))))
(mf/defc recovery-form
[{:keys [locale params] :as props}]
[:& form {:on-submit on-submit
:spec ::recovery-form
:validators [password-equality]
:initial params}
[:& input {:type "password"
:name :password-1
:label (t locale "auth.new-password-label")}]
[:& input {:type "password"
:name :password-2
:label (t locale "auth.confirm-password-label")}]
[:& submit-button
{:label (t locale "auth.recovery-submit-label")}]])
;; --- Recovery Request Page
(mf/defc recovery-page
[{:keys [locale params] :as props}]
[:section.generic-form
[:div.form-container
[:h1 "Forgot your password?"]
[:div.subtitle "Please enter your new password"]
[:& recovery-form {:locale locale :params params}]
[:div.links
[:div.link-entry
[:a {:on-click #(st/emit! (rt/nav :auth-login))}
(t locale "profile.recovery.go-to-login")]]]]])

View File

@ -0,0 +1,68 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.auth.recovery-request
(:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[lentes.core :as l]
[rumext.alpha :as mf]
[uxbox.main.ui.icons :as i]
[uxbox.common.spec :as us]
[uxbox.main.data.auth :as uda]
[uxbox.main.data.messages :as dm]
[uxbox.main.store :as st]
[uxbox.main.ui.components.forms :refer [input submit-button form]]
[uxbox.main.ui.messages :refer [messages]]
[uxbox.main.ui.navigation :as nav]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :as i18n :refer [tr t]]
[uxbox.util.router :as rt]))
(s/def ::email ::us/email)
(s/def ::recovery-request-form (s/keys :req-un [::email]))
(defn- on-submit
[form event]
(let [on-success #(st/emit! (dm/info (tr "auth.notifications.recovery-token-sent"))
(rt/nav :auth-login))
params (with-meta (:clean-data form)
{:on-success on-success})]
(st/emit! (uda/request-profile-recovery params))))
(mf/defc recovery-form
[{:keys [locale] :as props}]
[:& form {:on-submit on-submit
:spec ::recovery-request-form
:initial {}}
[:& input {:name :email
:label (t locale "auth.email-label")
:help-icon i/at
:type "text"}]
[:& submit-button
{:label (t locale "auth.recovery-request-submit-label")}]])
;; --- Recovery Request Page
(mf/defc recovery-request-page
[{:keys [locale] :as props}]
[:section.generic-form
[:div.form-container
[:h1 (t locale "auth.recovery-request-title")]
[:div.subtitle (t locale "auth.recovery-request-subtitle")]
[:& recovery-form {:locale locale}]
[:div.links
[:div.link-entry
[:a {:on-click #(st/emit! (rt/nav :auth-login))}
(t locale "auth.go-back-to-login")]]]]])

View File

@ -0,0 +1,120 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.auth.register
(:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[lentes.core :as l]
[rumext.alpha :as mf]
[uxbox.config :as cfg]
[uxbox.main.ui.icons :as i]
[uxbox.main.data.auth :as uda]
[uxbox.main.store :as st]
[uxbox.main.data.auth :as da]
[uxbox.main.ui.messages :refer [messages]]
[uxbox.main.ui.components.forms :refer [input submit-button form]]
[uxbox.main.ui.navigation :as nav]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :refer [tr t]]
[uxbox.util.router :as rt]))
(mf/defc demo-warning
[_]
[:div.featured-note.warning
[:span
[:strong "WARNING: "]
"This is a " [:strong "demo"] " service, "
[:strong "DO NOT USE"] " for real work, "
" the projects will be periodicaly wiped."]])
(s/def ::fullname ::fm/not-empty-string)
(s/def ::password ::fm/not-empty-string)
(s/def ::email ::fm/email)
(s/def ::register-form
(s/keys :req-un [::password
::fullname
::email]))
(defn- on-error
[form error]
(case (:code error)
:uxbox.services.mutations.profile/registration-disabled
(st/emit! (tr "errors.registration-disabled"))
:uxbox.services.mutations.profile/email-already-exists
(swap! form assoc-in [:errors :email]
{:message "errors.email-already-exists"})
(st/emit! (tr "errors.unexpected-error"))))
(defn- validate
[data]
(let [password (:password data)]
(when (> 8 (count password))
{:password {:message "errors.password-too-short"}})))
(defn- on-submit
[form event]
(let [data (with-meta (:clean-data form)
{:on-error (partial on-error form)})]
(st/emit! (uda/register data))))
(mf/defc register-form
[{:keys [locale] :as props}]
[:& form {:on-submit on-submit
:spec ::register-form
:validators [validate]
:initial {}}
[:& input {:name :fullname
:tab-index "1"
:label (t locale "auth.fullname-label")
:type "text"}]
[:& input {:type "email"
:name :email
:tab-index "2"
:help-icon i/at
:label (t locale "auth.email-label")}]
[:& input {:name :password
:tab-index "3"
:hint (t locale "auth.password-length-hint")
:label (t locale "auth.password-label")
:type "password"}]
[:& submit-button
{:label (t locale "auth.register-submit-label")}]])
;; --- Register Page
(mf/defc register-page
[{:keys [locale] :as props}]
[:section.generic-form
[:div.form-container
[:h1 (t locale "auth.register-title")]
[:div.subtitle (t locale "auth.register-subtitle")]
(when cfg/demo-warning
[:& demo-warning])
[:& register-form {:locale locale}]
[:div.links
[:div.link-entry
[:span (t locale "auth.already-have-account") " "]
[:a {:on-click #(st/emit! (rt/nav :auth-login))
:tab-index "4"}
(t locale "auth.login-here")]]
[:div.link-entry
[:span (t locale "auth.create-demo-profile-label") " "]
[:a {:on-click #(st/emit! da/create-demo-profile)
:tab-index "5"}
(t locale "auth.create-demo-profile")]]]]])

View File

@ -0,0 +1,150 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.components.forms
(:require
[rumext.alpha :as mf]
[cuerdas.core :as str]
[uxbox.common.data :as d]
[uxbox.main.ui.icons :as i]
[uxbox.util.object :as obj]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :as i18n :refer [t]]
["react" :as react]
[uxbox.util.dom :as dom]))
(def form-ctx (mf/create-context nil))
(mf/defc input
[{:keys [type label help-icon disabled name form hint] :as props}]
(let [form (mf/use-ctx form-ctx)
type' (mf/use-state type)
focus? (mf/use-state false)
locale (mf/deref i18n/locale)
touched? (get-in form [:touched name])
error (get-in form [:errors name])
klass (dom/classnames
:focus @focus?
:valid (and touched? (not error))
:invalid (and touched? error)
:disabled disabled)
swap-text-password
(fn []
(swap! type' (fn [type]
(if (= "password" type)
"text"
"password"))))
on-focus #(reset! focus? true)
on-change (fm/on-input-change form name)
on-blur
(fn [event]
(reset! focus? false)
(when-not (get-in form [:touched name])
(swap! form assoc-in [:touched name] true)))
value (get-in form [:data name] "")
props (-> props
(dissoc :help-icon :form)
(assoc :value value
:on-focus on-focus
:on-blur on-blur
:placeholder label
:on-change on-change
:type @type')
(obj/clj->props))]
[:div.custom-input
[:div.input-container {:class klass}
[:div.main-content
(when-not (str/empty? value)
[:label label])
[:> :input props]]
[:div.help-icon
{:style {:cursor "pointer"}
:on-click (when (= "password" type)
swap-text-password)}
(cond
(and (= type "password")
(= @type' "password"))
i/eye
(and (= type "password")
(= @type' "text"))
i/eye-closed
:else
help-icon)]]
(cond
(and touched? (:message error))
[:span.error (t locale (:message error))]
(string? hint)
[:span.hint hint])]))
(mf/defc select
[{:keys [options label name form default]
:or {default ""}}]
(let [form (mf/use-ctx form-ctx)
value (get-in form [:data name] default)
cvalue (d/seek #(= value (:value %)) options)
on-change (fm/on-input-change form name)]
[:div.custom-select
[:select {:value value
:on-change on-change}
(for [item options]
[:option {:key (:value item) :value (:value item)} (:label item)])]
[:div.input-container
[:div.main-content
[:label label]
[:span.value (:label cvalue "")]]
[:div.icon
i/arrow-slide]]]))
(mf/defc submit-button
[{:keys [label form] :as props}]
(let [form (mf/use-ctx form-ctx)]
[:input.btn-primary.btn-large
{:name "submit"
:class (when-not (:valid form) "btn-disabled")
:disabled (not (:valid form))
:value label
:type "submit"}]))
(mf/defc form
[{:keys [on-submit spec validators initial children class] :as props}]
(let [frm (fm/use-form :spec spec
:validators validators
:initial initial)]
(mf/use-effect
(mf/deps initial)
(fn []
(if (fn? initial)
(swap! frm update :data merge (initial))
(swap! frm update :data merge initial))))
[:& (mf/provider form-ctx) {:value frm}
[:form {:class class
:on-submit (fn [event]
(dom/prevent-default event)
(on-submit frm event))}
children]]))

View File

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

View File

@ -1,99 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2015-2020 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2015-2020 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(ns uxbox.main.ui.login
(:require
[cljs.spec.alpha :as s]
[rumext.alpha :as mf]
[uxbox.common.spec :as us]
[uxbox.main.ui.icons :as i]
[uxbox.config :as cfg]
[uxbox.main.data.auth :as da]
[uxbox.main.store :as st]
[uxbox.main.ui.messages :refer [messages]]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :refer [tr]]
[uxbox.util.router :as rt]))
(s/def ::email ::us/email)
(s/def ::password ::us/not-empty-string)
(s/def ::login-form
(s/keys :req-un [::email ::password]))
(defn- on-submit
[event form]
(dom/prevent-default event)
(let [{:keys [email password]} (:clean-data form)]
(st/emit! (da/login {:email email :password password}))))
(mf/defc demo-warning
[_]
[:div.message-inline
[:p
[:strong "WARNING: "]
"This is a " [:strong "demo"] " service, "
[:strong "DO NOT USE"] " for real work, "
" the projects will be periodicaly wiped."]])
(mf/defc login-form
[]
(let [{:keys [data] :as form} (fm/use-form ::login-form {})]
[:form {:on-submit #(on-submit % form)}
[:div.login-content
(when cfg/demo-warning
[:& demo-warning])
[:input.input-text
{:name "email"
:tab-index "2"
:value (:email data "")
:class (fm/error-class form :email)
:on-blur (fm/on-input-blur form :email)
:on-change (fm/on-input-change form :email)
:placeholder (tr "login.email")
:type "text"}]
[:input.input-text
{:name "password"
:tab-index "3"
:value (:password data "")
:class (fm/error-class form :password)
:on-blur (fm/on-input-blur form :password)
:on-change (fm/on-input-change form :password)
:placeholder (tr "login.password")
:type "password"}]
[:input.btn-primary.btn-large
{:name "login"
:tab-index "4"
:class (when-not (:valid form) "btn-disabled")
:disabled (not (:valid form))
:value (tr "login.submit")
:type "submit"}]
[:div.login-links
[:a {:on-click #(st/emit! (rt/nav :profile-recovery-request))
:tab-index "5"}
(tr "login.forgot-password")]
[:a {:on-click #(st/emit! (rt/nav :profile-register))
:tab-index "6"}
(tr "login.register")]]
[:a.btn-secondary.btn-small {:on-click #(st/emit! da/create-demo-profile)
:tab-index "7"
:title (tr "login.create-demo-profile-description")}
(tr "login.create-demo-profile")]]]))
(mf/defc login-page
[]
[:div.login
[:div.login-body
[:& messages]
[:a i/logo]
[:& login-form]]])

View File

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

View File

@ -1,91 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2015-2020 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2015-2020 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(ns uxbox.main.ui.profile.recovery
(:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[lentes.core :as l]
[rumext.alpha :as mf]
[uxbox.main.ui.icons :as i]
[uxbox.common.spec :as us]
[uxbox.main.data.auth :as uda]
[uxbox.main.data.messages :as dm]
[uxbox.main.store :as st]
[uxbox.main.ui.messages :refer [messages]]
[uxbox.main.ui.navigation :as nav]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :as i18n :refer [t]]
[uxbox.util.router :as rt]))
(s/def ::token ::us/not-empty-string)
(s/def ::password ::us/not-empty-string)
(s/def ::recovery-form (s/keys :req-un [::token ::password]))
(mf/defc recovery-form
[]
(let [{:keys [data] :as form} (fm/use-form ::recovery-form {})
locale (i18n/use-locale)
on-success
(fn []
(st/emit! (dm/info (t locale "profile.recovery.password-changed"))
(rt/nav :login)))
on-error
(fn []
(st/emit! (dm/error (t locale "profile.recovery.invalid-token"))))
on-submit
(fn [event]
(dom/prevent-default event)
(st/emit! (uda/recover-profile (assoc (:clean-data form)
:on-error on-error
:on-success on-success))))]
[:form {:on-submit on-submit}
[:div.login-content
[:input.input-text
{:name "token"
:value (:token data "")
:class (fm/error-class form :token)
:on-blur (fm/on-input-blur form :token)
:on-change (fm/on-input-change form :token)
:placeholder (t locale "profile.recovery.token")
:auto-complete "off"
:type "text"}]
[:input.input-text
{:name "password"
:value (:password data "")
:class (fm/error-class form :password)
:on-blur (fm/on-input-blur form :password)
:on-change (fm/on-input-change form :password)
:placeholder (t locale "profile.recovery.password")
:type "password"}]
[:input.btn-primary
{:name "recover"
:class (when-not (:valid form) "btn-disabled")
:disabled (not (:valid form))
:value (t locale "profile.recovery.submit-recover")
:type "submit"}]
[:div.login-links
[:a {:on-click #(st/emit! (rt/nav :login))}
(t locale "profile.recovery.go-to-login")]]]]))
;; --- Recovery Request Page
(mf/defc profile-recovery-page
[]
[:div.login
[:div.login-body
[:& messages]
[:a i/logo]
[:& recovery-form]]])

View File

@ -1,75 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2015-2020 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2015-2020 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(ns uxbox.main.ui.profile.recovery-request
(:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[lentes.core :as l]
[rumext.alpha :as mf]
[uxbox.main.ui.icons :as i]
[uxbox.common.spec :as us]
[uxbox.main.data.auth :as uda]
[uxbox.main.data.messages :as dm]
[uxbox.main.store :as st]
[uxbox.main.ui.messages :refer [messages]]
[uxbox.main.ui.navigation :as nav]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :as i18n :refer [t]]
[uxbox.util.router :as rt]))
(s/def ::email ::us/email)
(s/def ::recovery-request-form (s/keys :req-un [::email]))
(mf/defc recovery-form
[]
(let [{:keys [data] :as form} (fm/use-form ::recovery-request-form {})
locale (i18n/use-locale)
on-success
(fn []
(st/emit! (dm/info (t locale "profile.recovery.recovery-token-sent"))
(rt/nav :profile-recovery)))
on-submit
(fn [event]
(dom/prevent-default event)
(st/emit! (uda/request-profile-recovery (:clean-data form) on-success)))]
[:form {:on-submit on-submit}
[:div.login-content
[:input.input-text
{:name "email"
:value (:email data "")
:class (fm/error-class form :email)
:on-blur (fm/on-input-blur form :email)
:on-change (fm/on-input-change form :email)
:placeholder (t locale "profile.recovery.email")
:type "text"}]
[:input.btn-primary
{:name "login"
:class (when-not (:valid form) "btn-disabled")
:disabled (not (:valid form))
:value (t locale "profile.recovery.submit-request")
:type "submit"}]
[:div.login-links
[:a {:on-click #(st/emit! (rt/nav :login))}
(t locale "profile.recovery.go-to-login")]]]]))
;; --- Recovery Request Page
(mf/defc profile-recovery-request-page
[]
[:div.login
[:div.login-body
[:& messages]
[:a i/logo]
[:& recovery-form]]])

View File

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

View File

@ -2,8 +2,10 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) 2015-2016 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2015-2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.settings
(:require
@ -18,6 +20,7 @@
[uxbox.main.ui.messages :refer [messages]]
[uxbox.main.ui.settings.header :refer [header]]
[uxbox.main.ui.settings.password :refer [password-page]]
[uxbox.main.ui.settings.options :refer [options-page]]
[uxbox.main.ui.settings.profile :refer [profile-page]]))
(mf/defc settings
@ -30,7 +33,8 @@
[:& header {:section section :profile profile}]
(case section
:settings-profile (mf/element profile-page)
:settings-password (mf/element password-page))]]))
:settings-password (mf/element password-page)
:settings-options (mf/element options-page))]]))

View File

@ -0,0 +1,102 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.settings.change-email
(:require
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[lentes.core :as l]
[rumext.alpha :as mf]
[uxbox.main.ui.icons :as i]
[uxbox.main.data.auth :as da]
[uxbox.main.data.users :as du]
[uxbox.main.ui.components.forms :refer [input submit-button form]]
[uxbox.main.data.messages :as dm]
[uxbox.main.store :as st]
[uxbox.main.refs :as refs]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.main.ui.modal :as modal]
[uxbox.util.i18n :as i18n :refer [tr t]]))
(s/def ::email-1 ::fm/email)
(s/def ::email-2 ::fm/email)
(s/def ::email-change-form
(s/keys :req-un [::email-1 ::email-2]))
(defn- on-error
[form error]
(cond
(= (:code error) :uxbox.services.mutations.profile/email-already-exists)
(swap! form (fn [data]
(let [error {:message (tr "errors.email-already-exists")}]
(-> data
(assoc-in [:errors :email-1] error)
(assoc-in [:errors :email-2] error)))))
:else
(let [msg (tr "errors.unexpected-error")]
(st/emit! (dm/error msg)))))
(defn- on-submit
[form event]
(let [data (with-meta {:email (get-in form [:clean-data :email-1])}
{:on-error (partial on-error form)})]
(st/emit! (du/request-email-change data))))
(mf/defc change-email-form
[{:keys [locale profile] :as props}]
[:section.modal-content.generic-form
[:h2 (t locale "settings.change-email-title")]
[:span.featured-note
[:span.text
[:span "Well send you an email to your current email "]
[:strong (:email profile)]
[:span " to verify your identity."]]]
[:& form {:on-submit on-submit
:spec ::email-change-form
:initial {}}
[:& input {:type "text"
:name :email-1
:label (t locale "settings.new-email-label")}]
[:& input {:type "text"
:name :email-2
:label (t locale "settings.confirm-email-label")}]
[:& submit-button
{:label (t locale "settings.change-email-submit-label")}]]])
(mf/defc change-email-confirmation
[{:keys [locale profile] :as locale}]
[:section.modal-content.generic-form.confirmation
[:h2 (t locale "settings.verification-sent-title")]
[:span.featured-note
[:span.icon i/trash]
[:span.text
[:span (str/format "We have sent you an email to “")]
[:strong (:email profile)]
[:span "” Please follow the instructions to verify the email."]]]
[:button.btn-primary.btn-large
{:on-click #(modal/hide!)}
(t locale "settings.close-modal-label")]])
(mf/defc change-email-modal
[props]
(let [locale (mf/deref i18n/locale)
profile (mf/deref refs/profile)]
[:section.generic-modal.change-email-modal
[:span.close {:on-click #(modal/hide!)} i/close]
(if (:pending-email profile)
[:& change-email-confirmation {:locale locale :profile profile}]
[:& change-email-form {:locale locale :profile profile}])]))

View File

@ -0,0 +1,42 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.settings.delete-account
(:require
[cljs.spec.alpha :as s]
[rumext.alpha :as mf]
[uxbox.main.ui.icons :as i]
[uxbox.main.data.auth :as da]
[uxbox.main.data.users :as du]
[uxbox.main.store :as st]
[uxbox.main.ui.modal :as modal]
[uxbox.util.i18n :as i18n :refer [tr t]]))
(mf/defc delete-account-modal
[props]
(let [locale (mf/deref i18n/locale)]
[:section.generic-modal.change-email-modal
[:span.close {:on-click #(modal/hide!)} i/close]
[:section.modal-content.generic-form
[:h2 (t locale "settings.delete-account-title")]
[:span.featured-note
[:span.text
[:span (t locale "settings.delete-account-info")]]]
[:div.button-row
[:button.btn-warning.btn-large
{:on-click #(do
(modal/hide!)
(st/emit! da/request-account-deletion))}
(t locale "settings.yes-delete-my-account")]
[:button.btn-secondary.btn-large
{:on-click #(modal/hide!)}
(t locale "settings.cancel-and-keep-my-account")]]]]))

View File

@ -2,6 +2,9 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.settings.header
@ -16,22 +19,42 @@
(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")]]]))

View File

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

View File

@ -0,0 +1,75 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns uxbox.main.ui.settings.options
(:require
[rumext.alpha :as mf]
[cljs.spec.alpha :as s]
[uxbox.main.ui.icons :as i]
[uxbox.main.data.users :as udu]
[uxbox.main.data.messages :as dm]
[uxbox.main.ui.components.forms :refer [select submit-button form]]
[uxbox.main.refs :as refs]
[uxbox.main.store :as st]
[uxbox.util.dom :as dom]
[uxbox.util.forms :as fm]
[uxbox.util.i18n :as i18n :refer [t tr]]))
(s/def ::lang (s/nilable ::fm/not-empty-string))
(s/def ::theme (s/nilable ::fm/not-empty-string))
(s/def ::options-form
(s/keys :opt-un [::lang ::theme]))
(defn- on-error
[form error])
(defn- on-submit
[form event]
(dom/prevent-default event)
(let [data (:clean-data form)
on-success #(st/emit! (dm/info (tr "settings.notifications.profile-saved")))
on-error #(on-error % form)]
(st/emit! (udu/update-profile (with-meta data
{:on-success on-success
:on-error on-error})))))
(mf/defc options-form
[{:keys [locale profile] :as props}]
[:& form {:class "options-form"
:on-submit on-submit
:spec ::options-form
:initial profile}
[:h2 (t locale "settings.language-change-title")]
[:& select {:options [{:label "English" :value "en"}
{:label "Français" :value "fr"}]
:label (t locale "settings.language-label")
:default "en"
:name :lang}]
[:h2 (t locale "settings.theme-change-title")]
[:& select {:label (t locale "settings.theme-label")
:name :theme
:default "default"
:options [{:label "Default" :value "default"}]}]
[:& submit-button
{:label (t locale "settings.profile-submit-label")}]])
;; --- Password Page
(mf/defc options-page
[props]
(let [locale (mf/deref i18n/locale)
profile (mf/deref refs/profile)]
[:section.settings-options.generic-form
[:div.forms-container
[:& options-form {:locale locale :profile profile}]]]))

View File

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

View File

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

View File

@ -22,9 +22,9 @@
(defonce instance
(when (not= *target* "nodejs")
(let [uri (Uri. cfg/public-url)]
(let [uri (Uri. cfg/public-uri)]
(.setPath uri "js/worker.js")
(.setParameterValue uri "backendURL" cfg/backend-url)
(.setParameterValue uri "backendURI" cfg/backend-uri)
(uw/init (.toString uri) on-error))))
(defn ask!

View File

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

View File

@ -50,44 +50,25 @@
:else acc))
(defn use-form
[spec initial]
(let [[state update-state] (mf/useState {:data (if (fn? initial) (initial) initial)
:errors {}
:touched {}})
clean-data (s/conform spec (:data state))
problems (when (= ::s/invalid clean-data)
(::s/problems (s/explain-data spec (:data state))))
errors (merge (reduce interpret-problem {} problems)
(:errors state))]
(-> (assoc state
:errors errors
:clean-data (when (not= clean-data ::s/invalid) clean-data)
:valid (and (empty? errors)
(not= clean-data ::s/invalid)))
(impl-mutator update-state))))
(defn use-form2
[& {:keys [spec validators initial]}]
(let [[state update-state] (mf/useState {:data (if (fn? initial) (initial) initial)
:errors {}
:touched {}})
clean-data (s/conform spec (:data state))
problems (when (= ::s/invalid clean-data)
cleaned (s/conform spec (:data state))
problems (when (= ::s/invalid cleaned)
(::s/problems (s/explain-data spec (:data state))))
errors (merge (reduce interpret-problem {} problems)
(when (not= clean-data ::s/invalid)
errors (merge (reduce interpret-problem {} problems)
(reduce (fn [errors vf]
(merge errors (vf clean-data)))
{} validators))
(:errors state))]
(merge errors (vf (:data state))))
{} validators)
(:errors state))]
(-> (assoc state
:errors errors
:clean-data (when (not= clean-data ::s/invalid) clean-data)
:clean-data (when (not= cleaned ::s/invalid) cleaned)
:valid (and (empty? errors)
(not= clean-data ::s/invalid)))
(not= cleaned ::s/invalid)))
(impl-mutator update-state))))
(defn on-input-change

View File

@ -58,9 +58,9 @@
:blob ResponseType.BLOB
ResponseType.DEFAULT))
(defn- create-url
[url qs qp]
(let [uri (Uri. url)]
(defn- create-uri
[uri qs qp]
(let [uri (Uri. uri)]
(when qs (.setQuery uri qs))
(when qp
(let [dt (.createFromMap QueryData (clj->js qp))]
@ -68,10 +68,10 @@
(.toString uri)))
(defn- fetch
[{:keys [method url query-string query headers body] :as request}
[{:keys [method uri query-string query headers body] :as request}
{:keys [timeout credentials? response-type]
:or {timeout 0 credentials? false response-type :text}}]
(let [uri (create-url url query-string query)
(let [uri (create-uri uri query-string query)
headers (if headers (clj->js headers) #js {})
method (translate-method method)
xhr (doto (XhrIo.)

View File

@ -32,13 +32,13 @@
{"content-type" "application/transit+json"})
(defn- impl-send
[{:keys [body headers auth method query url response-type]
[{:keys [body headers auth method query uri response-type]
:or {auth true response-type :text}}]
(let [headers (merge {"Accept" "application/transit+json,*/*"}
(when (map? body) default-headers)
headers)
request {:method method
:url url
:uri uri
:headers headers
:query query
:body (if (map? body)

View File

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

View File

@ -24,10 +24,10 @@
(-send [_ message] "send a message")
(-close [_] "close websocket"))
(defn url
([path] (url path {}))
(defn uri
([path] (uri path {}))
([path params]
(let [uri (.parse Uri cfg/url)]
(let [uri (.parse Uri cfg/backend-uri)]
(.setPath uri path)
(if (= (.getScheme uri) "http")
(.setScheme uri "ws")

View File

@ -30,8 +30,8 @@
(this-as global
(let [location (obj/get global "location")
uri (Uri. (obj/get location "href"))
buri (.getParameterValue uri "backendURL")]
(swap! impl/config assoc :backend-url buri)))
buri (.getParameterValue uri "backendURI")]
(swap! impl/config assoc :backend-uri buri)))
;; --- Messages Handling

View File

@ -33,11 +33,11 @@
(defn- request-page
[id]
(let [url (get @impl/config :backend-url "http://localhost:6060")
url (str url "/api/w/query/page")]
(let [uri (get @impl/config :backend-uri "http://localhost:6060")
uri (str uri "/api/w/query/page")]
(p/create
(fn [resolve reject]
(->> (http/send! {:url url
(->> (http/send! {:uri uri
:query {:id id}
:method :get})
(rx/mapcat handle-response)