diff --git a/backend/resources/emails/change-email/en.mustache b/backend/resources/emails/change-email/en.mustache new file mode 100644 index 0000000000..efae9b35f7 --- /dev/null +++ b/backend/resources/emails/change-email/en.mustache @@ -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 diff --git a/backend/resources/emails/password-recovery/en.mustache b/backend/resources/emails/password-recovery/en.mustache index df68f236a6..c08997c515 100644 --- a/backend/resources/emails/password-recovery/en.mustache +++ b/backend/resources/emails/password-recovery/en.mustache @@ -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 - - - - - title - {{> ../partials/inline_style }} - - - - - - - - - - -
- -

TODO

-

{{ token }}

-
- {{> ../partials/en/footer }} - - --- end \ No newline at end of file diff --git a/backend/resources/emails/register/en.mustache b/backend/resources/emails/register/en.mustache index 70c8fd99fa..23992f8740 100644 --- a/backend/resources/emails/register/en.mustache +++ b/backend/resources/emails/register/en.mustache @@ -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 - - - - - title - {{> ../partials/inline_style }} - - - - - - - - - - -
- -

Hello {{name}}!

-

Welcome to UXBOX.

-

UXBOX team.

-
- {{> ../partials/en/footer }} - - +Enjoy! +The UXBOX team. -- end \ No newline at end of file diff --git a/backend/resources/migrations/0001.main.sql b/backend/resources/migrations/0001-add-extensions.sql similarity index 100% rename from backend/resources/migrations/0001.main.sql rename to backend/resources/migrations/0001-add-extensions.sql diff --git a/backend/resources/migrations/0002.users.sql b/backend/resources/migrations/0002-add-profile-tables.sql similarity index 96% rename from backend/resources/migrations/0002.users.sql rename to backend/resources/migrations/0002-add-profile-tables.sql index cbd34cff15..230a35a5b7 100644 --- a/backend/resources/migrations/0002.users.sql +++ b/backend/resources/migrations/0002-add-profile-tables.sql @@ -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, diff --git a/backend/resources/migrations/0003.projects.sql b/backend/resources/migrations/0003-add-project-tables.sql similarity index 100% rename from backend/resources/migrations/0003.projects.sql rename to backend/resources/migrations/0003-add-project-tables.sql diff --git a/backend/resources/migrations/0004.tasks.sql b/backend/resources/migrations/0004-add-tasks-tables.sql similarity index 100% rename from backend/resources/migrations/0004.tasks.sql rename to backend/resources/migrations/0004-add-tasks-tables.sql diff --git a/backend/resources/migrations/0005.libraries.sql b/backend/resources/migrations/0005-add-libraries-tables.sql similarity index 100% rename from backend/resources/migrations/0005.libraries.sql rename to backend/resources/migrations/0005-add-libraries-tables.sql diff --git a/backend/resources/migrations/0006.presence.sql b/backend/resources/migrations/0006-add-presence-tables.sql similarity index 100% rename from backend/resources/migrations/0006.presence.sql rename to backend/resources/migrations/0006-add-presence-tables.sql diff --git a/backend/resources/migrations/0007.remove_version.sql b/backend/resources/migrations/0007-drop-version-field-from-page-table.sql similarity index 100% rename from backend/resources/migrations/0007.remove_version.sql rename to backend/resources/migrations/0007-drop-version-field-from-page-table.sql diff --git a/backend/resources/migrations/0008-add-generic-token-table.sql b/backend/resources/migrations/0008-add-generic-token-table.sql new file mode 100644 index 0000000000..2f2c7322ea --- /dev/null +++ b/backend/resources/migrations/0008-add-generic-token-table.sql @@ -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'; diff --git a/backend/resources/migrations/0009-drop-profile-email-table.sql b/backend/resources/migrations/0009-drop-profile-email-table.sql new file mode 100644 index 0000000000..1a62372abe --- /dev/null +++ b/backend/resources/migrations/0009-drop-profile-email-table.sql @@ -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; diff --git a/backend/resources/migrations/0010-add-http-session-table.sql b/backend/resources/migrations/0010-add-http-session-table.sql new file mode 100644 index 0000000000..d23ab67aea --- /dev/null +++ b/backend/resources/migrations/0010-add-http-session-table.sql @@ -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); diff --git a/backend/src/uxbox/config.clj b/backend/src/uxbox/config.clj index 0d160fb628..a500b1f87d 100644 --- a/backend/src/uxbox/config.clj +++ b/backend/src/uxbox/config.clj @@ -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 diff --git a/backend/src/uxbox/db.clj b/backend/src/uxbox/db.clj index af5940fe62..e77e88cba4 100644 --- a/backend/src/uxbox/db.clj +++ b/backend/src/uxbox/db.clj @@ -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)) diff --git a/backend/src/uxbox/emails.clj b/backend/src/uxbox/emails.clj index 2f7aeac72a..901e1fb9e9 100644 --- a/backend/src/uxbox/emails.clj +++ b/backend/src/uxbox/emails.clj @@ -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 +;; 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)) diff --git a/backend/src/uxbox/fixtures.clj b/backend/src/uxbox/fixtures.clj index a8a0e070ca..24dbd58aae 100644 --- a/backend/src/uxbox/fixtures.clj +++ b/backend/src/uxbox/fixtures.clj @@ -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] diff --git a/backend/src/uxbox/http.clj b/backend/src/uxbox/http.clj index 4ee8a44c06..5d9838c361 100644 --- a/backend/src/uxbox/http.clj +++ b/backend/src/uxbox/http.clj @@ -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 +;; 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]} diff --git a/backend/src/uxbox/http/auth.clj b/backend/src/uxbox/http/auth.clj new file mode 100644 index 0000000000..baf1b5d372 --- /dev/null +++ b/backend/src/uxbox/http/auth.clj @@ -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 ""}) diff --git a/backend/src/uxbox/http/auth/google.clj b/backend/src/uxbox/http/auth/google.clj new file mode 100644 index 0000000000..7ca6891f55 --- /dev/null +++ b/backend/src/uxbox/http/auth/google.clj @@ -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 + +(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 ""}))) + diff --git a/backend/src/uxbox/http/handlers.clj b/backend/src/uxbox/http/handlers.clj index 225eb46d49..1eb16f2721 100644 --- a/backend/src/uxbox/http/handlers.clj +++ b/backend/src/uxbox/http/handlers.clj @@ -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 +;; 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 diff --git a/backend/src/uxbox/http/session.clj b/backend/src/uxbox/http/session.clj index 35235b982b..4765900ca5 100644 --- a/backend/src/uxbox/http/session.clj +++ b/backend/src/uxbox/http/session.clj @@ -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 +;; 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)) diff --git a/backend/src/uxbox/main.clj b/backend/src/uxbox/main.clj index 5e8a9e806c..092c44bbdd 100644 --- a/backend/src/uxbox/main.clj +++ b/backend/src/uxbox/main.clj @@ -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 +;; Copyright (c) 2020 UXBOX Labs SL (ns uxbox.main (:require diff --git a/backend/src/uxbox/migrations.clj b/backend/src/uxbox/migrations.clj index ea1d09225a..a8b5415a3a 100644 --- a/backend/src/uxbox/migrations.clj +++ b/backend/src/uxbox/migrations.clj @@ -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 +;; 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 diff --git a/backend/src/uxbox/services/mutations/demo.clj b/backend/src/uxbox/services/mutations/demo.clj index aa319fa1f3..337b73cfc4 100644 --- a/backend/src/uxbox/services/mutations/demo.clj +++ b/backend/src/uxbox/services/mutations/demo.clj @@ -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" diff --git a/backend/src/uxbox/services/mutations/profile.clj b/backend/src/uxbox/services/mutations/profile.clj index e880bb77a0..42045ec8b0 100644 --- a/backend/src/uxbox/services/mutations/profile.clj +++ b/backend/src/uxbox/services/mutations/profile.clj @@ -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)})))) diff --git a/backend/src/uxbox/services/queries/profile.clj b/backend/src/uxbox/services/queries/profile.clj index 55737a2a2e..19779d7d23 100644 --- a/backend/src/uxbox/services/queries/profile.clj +++ b/backend/src/uxbox/services/queries/profile.clj @@ -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)) diff --git a/backend/src/uxbox/services/tokens.clj b/backend/src/uxbox/services/tokens.clj new file mode 100644 index 0000000000..1c48e01e81 --- /dev/null +++ b/backend/src/uxbox/services/tokens.clj @@ -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)))))) + + + diff --git a/backend/src/uxbox/util/template.clj b/backend/src/uxbox/util/template.clj index 36b4de1195..4260682bd4 100644 --- a/backend/src/uxbox/util/template.clj +++ b/backend/src/uxbox/util/template.clj @@ -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 diff --git a/backend/src/uxbox/util/time.clj b/backend/src/uxbox/util/time.clj index 9941774278..a0afc53dee 100644 --- a/backend/src/uxbox/util/time.clj +++ b/backend/src/uxbox/util/time.clj @@ -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)) diff --git a/backend/tests/uxbox/tests/helpers.clj b/backend/tests/uxbox/tests/helpers.clj index ef617ac3e8..bd5f1eb3a5 100644 --- a/backend/tests/uxbox/tests/helpers.clj +++ b/backend/tests/uxbox/tests/helpers.clj @@ -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] diff --git a/docs/02-Frontend-Developer-Guide.md b/docs/02-Frontend-Developer-Guide.md index 446351e26d..f5795b5f14 100644 --- a/docs/02-Frontend-Developer-Guide.md +++ b/docs/02-Frontend-Developer-Guide.md @@ -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) diff --git a/frontend/deps.edn b/frontend/deps.edn index db4f799613..55ea41dfbc 100644 --- a/frontend/deps.edn +++ b/frontend/deps.edn @@ -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 diff --git a/frontend/gulpfile.js b/frontend/gulpfile.js index 615855fa7d..1f720a3871 100644 --- a/frontend/gulpfile.js +++ b/frontend/gulpfile.js @@ -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) { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1e62dd2c69..37a1e50de0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index 9a24c4ced6..0855a243d9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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" diff --git a/frontend/resources/images/icons/at.svg b/frontend/resources/images/icons/at.svg new file mode 100644 index 0000000000..c9bb6aada2 --- /dev/null +++ b/frontend/resources/images/icons/at.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/logout.svg b/frontend/resources/images/icons/logout.svg new file mode 100644 index 0000000000..b5ae8c421e --- /dev/null +++ b/frontend/resources/images/icons/logout.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/locales.json b/frontend/resources/locales.json index f39842a84b..4183c8d8a2 100644 --- a/frontend/resources/locales.json +++ b/frontend/resources/locales.json @@ -1,6 +1,192 @@ { + "auth.already-have-account" : { + "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:111" ], + "translations" : { + "en" : "Already have an account?", + "fr" : "Vous avez déjà un compte ?" + } + }, + "auth.confirm-password-label" : { + "used-in" : [ "src/uxbox/main/ui/auth/recovery.cljs:78" ], + "translations" : { + "en" : "Confirm password", + "fr" : "Confirmez mot de passe" + } + }, + "auth.create-demo-profile" : { + "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:90", "src/uxbox/main/ui/auth/register.cljs:120" ], + "translations" : { + "en" : "Create demo account", + "fr" : null + } + }, + "auth.create-demo-profile-label" : { + "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:87", "src/uxbox/main/ui/auth/register.cljs:117" ], + "translations" : { + "en" : "Just wanna try it?" + } + }, + "auth.email-label" : { + "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:55", "src/uxbox/main/ui/auth/register.cljs:86", "src/uxbox/main/ui/auth/recovery_request.cljs:47" ], + "translations" : { + "en" : "Email", + "fr" : "adresse email" + } + }, + "auth.forgot-password" : { + "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:78" ], + "translations" : { + "en" : "Forgot your password?", + "fr" : "Mot de passe oublié ?" + } + }, + "auth.fullname-label" : { + "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:80" ], + "translations" : { + "en" : "Full Name", + "fr" : "Nom complet" + } + }, + "auth.go-back-to-login" : { + "used-in" : [ "src/uxbox/main/ui/auth/recovery_request.cljs:67" ], + "translations" : { + "en" : "Go back!", + "fr" : "Retour!" + } + }, + "auth.goodbye-title" : { + "used-in" : [ "src/uxbox/main/ui/auth.cljs:33" ], + "translations" : { + "en" : "Goodbye!" + } + }, + "auth.login-here" : { + "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:114" ], + "translations" : { + "en" : "Login here" + } + }, + "auth.login-submit-label" : { + "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:63" ], + "translations" : { + "en" : "Sign in", + "fr" : "Se connecter" + } + }, + "auth.login-subtitle" : { + "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:70" ], + "translations" : { + "en" : "Enter your details below" + } + }, + "auth.login-title" : { + "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:69" ], + "translations" : { + "en" : "Great to see you again!" + } + }, + "auth.new-password-label" : { + "used-in" : [ "src/uxbox/main/ui/auth/recovery.cljs:74" ], + "translations" : { + "en" : "Type a new password", + "fr" : null + } + }, + "auth.notifications.invalid-token-error" : { + "used-in" : [ "src/uxbox/main/ui/auth/recovery.cljs:50" ], + "translations" : { + "en" : "The recovery token is invalid.", + "fr" : "Le jeton de récupération n'est pas valide." + } + }, + "auth.notifications.password-changed-succesfully" : { + "used-in" : [ "src/uxbox/main/ui/auth/recovery.cljs:54" ], + "translations" : { + "en" : "Password successfully changed" + } + }, + "auth.notifications.recovery-token-sent" : { + "used-in" : [ "src/uxbox/main/ui/auth/recovery_request.cljs:34" ], + "translations" : { + "en" : "Password recovery link sent to your inbox.", + "fr" : "Lien de récupération de mot de passe envoyé." + } + }, + "auth.password-label" : { + "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:61", "src/uxbox/main/ui/auth/register.cljs:90" ], + "translations" : { + "en" : "Password", + "fr" : "Mot de passe" + } + }, + "auth.password-length-hint" : { + "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:89" ], + "translations" : { + "en" : "At least 8 characters" + } + }, + "auth.recovery-request-submit-label" : { + "used-in" : [ "src/uxbox/main/ui/auth/recovery_request.cljs:51" ], + "translations" : { + "en" : "Recover Password" + } + }, + "auth.recovery-request-subtitle" : { + "used-in" : [ "src/uxbox/main/ui/auth/recovery_request.cljs:60" ], + "translations" : { + "en" : "We'll send you an email with instructions" + } + }, + "auth.recovery-request-title" : { + "used-in" : [ "src/uxbox/main/ui/auth/recovery_request.cljs:59" ], + "translations" : { + "en" : "Forgot your password?" + } + }, + "auth.recovery-submit-label" : { + "used-in" : [ "src/uxbox/main/ui/auth/recovery.cljs:81" ], + "translations" : { + "en" : "Change your password" + } + }, + "auth.register" : { + "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:84" ], + "translations" : { + "en" : "Sign up here" + } + }, + "auth.register-label" : { + "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:81" ], + "translations" : { + "en" : "No account yet?" + } + }, + "auth.register-submit-label" : { + "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:94" ], + "translations" : { + "en" : "Create an account" + } + }, + "auth.register-subtitle" : { + "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:103" ], + "translations" : { + "en" : "It's free, it's Open Source" + } + }, + "auth.register-title" : { + "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:102" ], + "translations" : { + "en" : "Create an account" + } + }, + "auth.sidebar-tagline" : { + "used-in" : [ "src/uxbox/main/ui/auth.cljs:44" ], + "translations" : { + "en" : "The open-source solution for design and prototyping." + } + }, "dashboard.grid.delete" : { - "used-in" : [ "src/uxbox/main/ui/dashboard/grid.cljs:102", "src/uxbox/main/ui/dashboard/project.cljs:62" ], + "used-in" : [ "src/uxbox/main/ui/dashboard/project.cljs:62", "src/uxbox/main/ui/dashboard/grid.cljs:102" ], "translations" : { "en" : "Delete" } @@ -18,7 +204,7 @@ } }, "dashboard.grid.rename" : { - "used-in" : [ "src/uxbox/main/ui/dashboard/grid.cljs:101", "src/uxbox/main/ui/dashboard/project.cljs:61" ], + "used-in" : [ "src/uxbox/main/ui/dashboard/project.cljs:61", "src/uxbox/main/ui/dashboard/grid.cljs:101" ], "translations" : { "en" : "Rename" } @@ -320,40 +506,86 @@ } }, "errors.api.form.registration-disabled" : { - "used-in" : [ "src/uxbox/main/ui/profile/register.cljs:37" ], "translations" : { - "en" : "The registration is currently disabled.", - "fr" : "L'enregistrement est actuellement désactivé." - } + "en" : null, + "fr" : null + }, + "unused" : true }, "errors.api.form.unexpected-error" : { - "used-in" : [ "src/uxbox/main/ui/profile/register.cljs:44" ], "translations" : { - "en" : "An unexpected error occurred.", - "fr" : "Une erreur inattendue c'est produite" - } + "en" : null, + "fr" : null + }, + "unused" : true }, "errors.auth.unauthorized" : { - "used-in" : [ "src/uxbox/main/data/auth.cljs:57" ], + "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:37" ], "translations" : { "en" : "Username or password seems to be wrong.", "fr" : "Le nom d'utilisateur ou le mot de passe semble être faux." } }, + "errors.email-already-exists" : { + "used-in" : [ "src/uxbox/main/ui/settings/change_email.cljs:38", "src/uxbox/main/ui/auth.cljs:84" ], + "translations" : { + "en" : "Email already used" + } + }, "errors.generic" : { - "used-in" : [ "src/uxbox/main/ui.cljs:178" ], + "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:37", "src/uxbox/main/ui/auth.cljs:88", "src/uxbox/main/ui.cljs:179" ], "translations" : { "en" : "Something wrong has happened.", "fr" : "Quelque chose c'est mal passé." } }, "errors.network" : { - "used-in" : [ "src/uxbox/main/ui.cljs:172" ], + "used-in" : [ "src/uxbox/main/ui.cljs:173" ], "translations" : { "en" : "Unable to connect to backend server.", "fr" : "Impossible de se connecter au serveur principal." } }, + "errors.password-invalid-confirmation" : { + "used-in" : [ "src/uxbox/main/ui/settings/password.cljs:59" ], + "translations" : { + "en" : null, + "fr" : null + } + }, + "errors.password-too-short" : { + "used-in" : [ "src/uxbox/main/ui/settings/password.cljs:62" ], + "translations" : { + "en" : "Password should at least be 8 characters" + } + }, + "errors.registration-disabled" : { + "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:52" ], + "translations" : { + "en" : "The registration is currently disabled.", + "fr" : "L'enregistrement est actuellement désactivé." + } + }, + "errors.unexpected-error" : { + "used-in" : [ "src/uxbox/main/ui/settings/change_email.cljs:44", "src/uxbox/main/ui/auth/register.cljs:58" ], + "translations" : { + "en" : "An unexpected error occurred.", + "fr" : "Une erreur inattendue c'est produite" + } + }, + "errors.wrong-old-password" : { + "used-in" : [ "src/uxbox/main/ui/settings/password.cljs:28" ], + "translations" : { + "en" : "Old password is incorrect" + } + }, + "generic.error" : { + "used-in" : [ "src/uxbox/main/ui/settings/password.cljs:31" ], + "translations" : { + "en" : null, + "fr" : null + } + }, "header.sitemap" : { "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:67" ], "translations" : { @@ -396,299 +628,270 @@ "fr" : "Envoyer un fichier" } }, - "login.create-demo-profile" : { - "used-in" : [ "src/uxbox/main/ui/login.cljs:91" ], - "translations" : { - "en" : "Create demo account", - "fr" : null - } - }, - "login.create-demo-profile-description" : { - "used-in" : [ "src/uxbox/main/ui/login.cljs:90" ], - "translations" : { - "en" : "Will be deleted in 24 hours since creation", - "fr" : null - } - }, - "login.email" : { - "used-in" : [ "src/uxbox/main/ui/login.cljs:62" ], - "translations" : { - "en" : "Email", - "fr" : "adresse email" - } - }, - "login.forgot-password" : { - "used-in" : [ "src/uxbox/main/ui/login.cljs:84" ], - "translations" : { - "en" : "Forgot your password?", - "fr" : "Mot de passe oublié ?" - } - }, - "login.password" : { - "used-in" : [ "src/uxbox/main/ui/login.cljs:71" ], - "translations" : { - "en" : "Password", - "fr" : "Mot de passe" - } - }, - "login.register" : { - "used-in" : [ "src/uxbox/main/ui/login.cljs:87" ], - "translations" : { - "en" : "Don't have an account?", - "fr" : "Vous n'avez pas de compte ?" - } - }, - "login.submit" : { - "used-in" : [ "src/uxbox/main/ui/login.cljs:78" ], - "translations" : { - "en" : "Sign in", - "fr" : "Se connecter" - } - }, "modal.create-color.new-color" : { "used-in" : [ "src/uxbox/main/ui/dashboard/library.cljs:48" ], "translations" : { "en" : "New Color" } }, - "profile.recovery.email" : { - "used-in" : [ "src/uxbox/main/ui/profile/recovery_request.cljs:54" ], - "translations" : { - "en" : "Email Address", - "fr" : "adresse email" - } - }, "profile.recovery.go-to-login" : { - "used-in" : [ "src/uxbox/main/ui/profile/recovery.cljs:81", "src/uxbox/main/ui/profile/recovery_request.cljs:65" ], + "used-in" : [ "src/uxbox/main/ui/auth/recovery.cljs:97" ], "translations" : { - "en" : "Go back!", - "fr" : "Retour!" - } - }, - "profile.recovery.invalid-token" : { - "used-in" : [ "src/uxbox/main/ui/profile/recovery.cljs:45" ], - "translations" : { - "en" : "The recovery token is invalid.", - "fr" : "Le jeton de récupération n'est pas valide." - } - }, - "profile.recovery.password" : { - "used-in" : [ "src/uxbox/main/ui/profile/recovery.cljs:70" ], - "translations" : { - "en" : "Type a new password", + "en" : null, "fr" : null } }, - "profile.recovery.password-changed" : { - "used-in" : [ "src/uxbox/main/ui/profile/recovery.cljs:40" ], + "settings.cancel-and-keep-my-account" : { + "used-in" : [ "src/uxbox/main/ui/settings/delete_account.cljs:42" ], "translations" : { - "en" : "Password successfully changed", - "fr" : "TODO" + "en" : "Cancel and keep my account" } }, - "profile.recovery.recovery-token-sent" : { - "used-in" : [ "src/uxbox/main/ui/profile/recovery_request.cljs:39" ], + "settings.change-email-label" : { + "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:73" ], "translations" : { - "en" : "Password recovery link sent to your inbox.", - "fr" : "Lien de récupération de mot de passe envoyé." + "en" : "Change email" } }, - "profile.recovery.submit-recover" : { - "used-in" : [ "src/uxbox/main/ui/profile/recovery.cljs:76" ], + "settings.change-email-submit-label" : { + "used-in" : [ "src/uxbox/main/ui/settings/change_email.cljs:76" ], "translations" : { - "en" : "Change your password", - "fr" : null + "en" : "Change email" } }, - "profile.recovery.submit-request" : { - "used-in" : [ "src/uxbox/main/ui/profile/recovery_request.cljs:60" ], + "settings.change-email-title" : { + "used-in" : [ "src/uxbox/main/ui/settings/change_email.cljs:56" ], "translations" : { - "en" : "Recover Password", - "fr" : null + "en" : "Change your email" } }, - "profile.recovery.token" : { - "used-in" : [ "src/uxbox/main/ui/profile/recovery.cljs:61" ], + "settings.close-modal-label" : { + "used-in" : [ "src/uxbox/main/ui/settings/change_email.cljs:92" ], "translations" : { - "en" : "Recovery token (sent by email)", - "fr" : null + "en" : "Close" } }, - "profile.register.already-have-account" : { - "used-in" : [ "src/uxbox/main/ui/profile/register.cljs:110" ], + "settings.confirm-email-label" : { + "used-in" : [ "src/uxbox/main/ui/settings/change_email.cljs:73" ], "translations" : { - "en" : "Already have an account?", - "fr" : "Vous avez déjà un compte ?" + "en" : "Verify new email" } }, - "profile.register.email" : { - "used-in" : [ "src/uxbox/main/ui/profile/register.cljs:80" ], - "translations" : { - "en" : "Your email", - "fr" : "Votre adresse email" - } - }, - "profile.register.fullname" : { - "used-in" : [ "src/uxbox/main/ui/profile/register.cljs:65" ], - "translations" : { - "en" : "Full Name", - "fr" : "Nom complet" - } - }, - "profile.register.get-started" : { - "used-in" : [ "src/uxbox/main/ui/profile/register.cljs:106" ], - "translations" : { - "en" : "Get started", - "fr" : "Commencer" - } - }, - "profile.register.password" : { - "used-in" : [ "src/uxbox/main/ui/profile/register.cljs:94" ], - "translations" : { - "en" : "Password", - "fr" : "Mot de passe" - } - }, - "settings.notifications.description" : { - "used-in" : [ "src/uxbox/main/ui/settings/notifications.cljs:19" ], - "translations" : { - "en" : "Get a roll up of prototype changes in your inbox.", - "fr" : "Obtenez un résumé des modifications apportées aux prototypes à votre adresse email." - } - }, - "settings.notifications.every-day" : { - "used-in" : [ "src/uxbox/main/ui/settings/notifications.cljs:38", "src/uxbox/main/ui/settings/notifications.cljs:38" ], - "translations" : { - "en" : "Every day", - "fr" : "Chaque jour" - } - }, - "settings.notifications.every-hour" : { - "used-in" : [ "src/uxbox/main/ui/settings/notifications.cljs:32", "src/uxbox/main/ui/settings/notifications.cljs:32" ], - "translations" : { - "en" : "Every hour", - "fr" : "Chaque heure" - } - }, - "settings.notifications.none" : { - "used-in" : [ "src/uxbox/main/ui/settings/notifications.cljs:26", "src/uxbox/main/ui/settings/notifications.cljs:26" ], - "translations" : { - "en" : "None", - "fr" : "Aucune" - } - }, - "settings.notifications.notifications-saved" : { - "used-in" : [ "src/uxbox/main/ui/settings/notifications.cljs:18" ], - "translations" : { - "en" : "Notifications preferences saved successfully!", - "fr" : "Préférences de notifications enregistrées avec succès !" - } - }, - "settings.password" : { - "used-in" : [ "src/uxbox/main/ui/settings/header.cljs:36" ], - "translations" : { - "en" : "PASSWORD", - "fr" : "MOT DE PASSE" - } - }, - "settings.password.change-password" : { - "used-in" : [ "src/uxbox/main/ui/settings/password.cljs:64" ], - "translations" : { - "en" : "Change password", - "fr" : "Changement de mot de passe" - } - }, - "settings.password.confirm-password" : { - "used-in" : [ "src/uxbox/main/ui/settings/password.cljs:94" ], + "settings.confirm-password-label" : { + "used-in" : [ "src/uxbox/main/ui/settings/password.cljs:91" ], "translations" : { "en" : "Confirm password", "fr" : "Confirmez mot de passe" } }, - "settings.password.new-password" : { - "used-in" : [ "src/uxbox/main/ui/settings/password.cljs:83" ], + "settings.delete-account-info" : { + "used-in" : [ "src/uxbox/main/ui/settings/delete_account.cljs:32" ], + "translations" : { + "en" : "By removing your account you’ll lose all your current projects and archives." + } + }, + "settings.delete-account-title" : { + "used-in" : [ "src/uxbox/main/ui/settings/delete_account.cljs:28" ], + "translations" : { + "en" : "Are you sure you want to delete your account?" + } + }, + "settings.email-label" : { + "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:67" ], + "translations" : { + "en" : "Email" + } + }, + "settings.fullname-label" : { + "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:60" ], + "translations" : { + "en" : "Your name", + "fr" : "Votre nom complet" + } + }, + "settings.language-change-title" : { + "used-in" : [ "src/uxbox/main/ui/settings/options.cljs:50" ], + "translations" : { + "en" : "Language" + } + }, + "settings.language-label" : { + "used-in" : [ "src/uxbox/main/ui/settings/options.cljs:54" ], + "translations" : { + "en" : "Select UI language" + } + }, + "settings.new-email-label" : { + "used-in" : [ "src/uxbox/main/ui/settings/change_email.cljs:69" ], + "translations" : { + "en" : "New email" + } + }, + "settings.new-password-label" : { + "used-in" : [ "src/uxbox/main/ui/settings/password.cljs:86" ], "translations" : { "en" : "New password", "fr" : "Nouveau mot de passe" } }, - "settings.password.old-password" : { - "used-in" : [ "src/uxbox/main/ui/settings/password.cljs:72" ], + "settings.notifications.description" : { "translations" : { - "en" : "Old password", - "fr" : "Ancien mot de passe" + "en" : null, + "fr" : null + }, + "unused" : true + }, + "settings.notifications.email-changed-successfully" : { + "used-in" : [ "src/uxbox/main/ui/auth.cljs:64" ], + "translations" : { + "en" : "Your email address has been updated successfully" } }, - "settings.password.password-saved" : { + "settings.notifications.email-verified-successfully" : { + "used-in" : [ "src/uxbox/main/ui/auth.cljs:57" ], + "translations" : { + "en" : "Your email address has been verified successfully" + } + }, + "settings.notifications.every-day" : { + "translations" : { + "en" : null, + "fr" : null + }, + "unused" : true + }, + "settings.notifications.every-hour" : { + "translations" : { + "en" : null, + "fr" : null + }, + "unused" : true + }, + "settings.notifications.none" : { + "translations" : { + "en" : null, + "fr" : null + }, + "unused" : true + }, + "settings.notifications.notifications-saved" : { + "translations" : { + "en" : null, + "fr" : null + }, + "unused" : true + }, + "settings.notifications.password-saved" : { "used-in" : [ "src/uxbox/main/ui/settings/password.cljs:36" ], "translations" : { "en" : "Password saved successfully!", "fr" : "Mot de passe enregistré avec succès !" } }, - "settings.profile" : { - "used-in" : [ "src/uxbox/main/ui/settings/header.cljs:32" ], + "settings.notifications.profile-deletion-not-allowed" : { "translations" : { - "en" : "PROFILE", - "fr" : "PROFIL" - } + "en" : null, + "fr" : null + }, + "used-in" : [ "src/uxbox/main/data/auth.cljs:121" ] }, - "settings.profile.lang" : { - "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:88" ], - "translations" : { - "en" : "Default language", - "fr" : "Langue par défaut" - } - }, - "settings.profile.profile-saved" : { - "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:48" ], + "settings.notifications.profile-saved" : { + "used-in" : [ "src/uxbox/main/ui/settings/options.cljs:37", "src/uxbox/main/ui/settings/profile.cljs:42" ], "translations" : { "en" : "Profile saved successfully!", "fr" : "Profil enregistré avec succès !" } }, - "settings.profile.section-basic-data" : { - "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:62" ], + "settings.old-password-label" : { + "used-in" : [ "src/uxbox/main/ui/settings/password.cljs:81" ], "translations" : { - "en" : "Name, username and email", - "fr" : "Nom, nom d'utilisateur et adresse email" + "en" : "Old password", + "fr" : "Ancien mot de passe" } }, - "settings.profile.section-theme-data" : { - "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:97" ], + "settings.options" : { + "used-in" : [ "src/uxbox/main/ui/settings/header.cljs:52" ], "translations" : { - "en" : "Default theme", - "fr" : "Thème par défaut" + "en" : "OPTIONS" } }, - "settings.profile.your-avatar" : { - "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:142" ], + "settings.password" : { + "used-in" : [ "src/uxbox/main/ui/settings/header.cljs:47" ], "translations" : { - "en" : "Your avatar", - "fr" : "Votre avatar" + "en" : "PASSWORD", + "fr" : "MOT DE PASSE" } }, - "settings.profile.your-email" : { - "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:83" ], + "settings.password-change-title" : { + "used-in" : [ "src/uxbox/main/ui/settings/password.cljs:76" ], "translations" : { - "en" : null, - "fr" : null + "en" : "Change password", + "fr" : "Changement de mot de passe" } }, - "settings.profile.your-name" : { - "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:71" ], + "settings.profile" : { + "used-in" : [ "src/uxbox/main/ui/settings/header.cljs:42" ], "translations" : { - "en" : "Your name", - "fr" : "Votre nom complet" + "en" : "PROFILE", + "fr" : "PROFIL" } }, - "settings.update-settings" : { - "used-in" : [ "src/uxbox/main/ui/settings/notifications.cljs:42", "src/uxbox/main/ui/settings/password.cljs:102", "src/uxbox/main/ui/settings/profile.cljs:109" ], + "settings.profile-submit-label" : { + "used-in" : [ "src/uxbox/main/ui/settings/options.cljs:65", "src/uxbox/main/ui/settings/password.cljs:94", "src/uxbox/main/ui/settings/profile.cljs:92" ], "translations" : { "en" : "Update settings", "fr" : "Mettre à jour les paramètres" } }, + "settings.remove-account-label" : { + "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:97" ], + "translations" : { + "en" : "Want to remove your account?" + } + }, + "settings.teams" : { + "used-in" : [ "src/uxbox/main/ui/settings/header.cljs:57" ], + "translations" : { + "en" : "TEAMS" + } + }, + "settings.theme-change-title" : { + "used-in" : [ "src/uxbox/main/ui/settings/options.cljs:58" ], + "translations" : { + "en" : "UI theme" + } + }, + "settings.theme-label" : { + "used-in" : [ "src/uxbox/main/ui/settings/options.cljs:59" ], + "translations" : { + "en" : "Select theme" + } + }, + "settings.update-photo-label" : { + "used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:119" ], + "translations" : { + "en" : "UPDATE" + } + }, + "settings.update-settings" : { + "translations" : { + "en" : null, + "fr" : null + }, + "unused" : true + }, + "settings.verification-sent-title" : { + "used-in" : [ "src/uxbox/main/ui/settings/change_email.cljs:81" ], + "translations" : { + "en" : "Verification email sent" + } + }, + "settings.yes-delete-my-account" : { + "used-in" : [ "src/uxbox/main/ui/settings/delete_account.cljs:39" ], + "translations" : { + "en" : "Yes, delete my account" + } + }, "viewer.empty-state" : { "used-in" : [ "src/uxbox/main/ui/viewer.cljs:44" ], "translations" : { @@ -822,49 +1025,49 @@ } }, "workspace.header.menu.disable-dynamic-alignment" : { - "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:113" ], + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:119" ], "translations" : { "en" : "Disable dynamic alignment" } }, "workspace.header.menu.disable-snap-grid" : { - "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:89" ], + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:91" ], "translations" : { "en" : "Disable snap to grid" } }, "workspace.header.menu.enable-dynamic-alignment" : { - "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:114" ], + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:120" ], "translations" : { "en" : "Enable dynamic aligment" } }, "workspace.header.menu.enable-snap-grid" : { - "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:90" ], + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:92" ], "translations" : { "en" : "Snap to grid" } }, "workspace.header.menu.hide-grid" : { - "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:83" ], + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:84" ], "translations" : { "en" : "Hide grid" } }, "workspace.header.menu.hide-layers" : { - "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:95" ], + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:98" ], "translations" : { "en" : "Hide layers" } }, "workspace.header.menu.hide-libraries" : { - "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:107" ], + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:112" ], "translations" : { "en" : "Hide libraries" } }, "workspace.header.menu.hide-palette" : { - "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:101" ], + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:105" ], "translations" : { "en" : "Hide color palette" } @@ -876,25 +1079,25 @@ } }, "workspace.header.menu.show-grid" : { - "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:84" ], + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:85" ], "translations" : { "en" : "Show grid" } }, "workspace.header.menu.show-layers" : { - "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:96" ], + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:99" ], "translations" : { "en" : "Show layers" } }, "workspace.header.menu.show-libraries" : { - "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:108" ], + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:113" ], "translations" : { "en" : "Show libraries" } }, "workspace.header.menu.show-palette" : { - "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:102" ], + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:106" ], "translations" : { "en" : "Show color palette" } @@ -906,7 +1109,7 @@ } }, "workspace.header.viewer" : { - "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:151" ], + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:158" ], "translations" : { "en" : "View mode (Ctrl + P)", "fr" : "Mode visualisation (Ctrl + P)" @@ -1111,121 +1314,121 @@ } }, "workspace.options.grid.column" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:132" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:134" ], "translations" : { "en" : "Columns" } }, "workspace.options.grid.params.columns" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:171" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:175" ], "translations" : { "en" : "Columns" } }, "workspace.options.grid.params.gutter" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:202" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:208" ], "translations" : { "en" : "Gutter" } }, "workspace.options.grid.params.height" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:194" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:199" ], "translations" : { "en" : "Height" } }, "workspace.options.grid.params.margin" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:207" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:214" ], "translations" : { "en" : "Margin" } }, "workspace.options.grid.params.rows" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:163" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:166" ], "translations" : { "en" : "Rows" } }, "workspace.options.grid.params.set-default" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:219" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:227" ], "translations" : { "en" : "Set as default" } }, "workspace.options.grid.params.size" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:156" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:159" ], "translations" : { "en" : "Size" } }, "workspace.options.grid.params.type" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:179" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:184" ], "translations" : { "en" : "Type" } }, "workspace.options.grid.params.type.bottom" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:187" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:192" ], "translations" : { "en" : "Bottom" } }, "workspace.options.grid.params.type.center" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:185" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:190" ], "translations" : { "en" : "Center" } }, "workspace.options.grid.params.type.left" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:184" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:189" ], "translations" : { "en" : "Left" } }, "workspace.options.grid.params.type.right" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:188" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:193" ], "translations" : { "en" : "Right" } }, "workspace.options.grid.params.type.stretch" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:181" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:186" ], "translations" : { "en" : "Stretch" } }, "workspace.options.grid.params.type.top" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:183" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:188" ], "translations" : { "en" : "Top" } }, "workspace.options.grid.params.use-default" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:217" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:225" ], "translations" : { "en" : "Use default" } }, "workspace.options.grid.params.width" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:195" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:200" ], "translations" : { "en" : "Width" } }, "workspace.options.grid.row" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:133" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:135" ], "translations" : { "en" : "Rows" } }, "workspace.options.grid.square" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:131" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:133" ], "translations" : { "en" : "Square" } }, "workspace.options.grid.title" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:231" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/frame_grid.cljs:239" ], "translations" : { "en" : "Grid & Layouts" } @@ -1514,7 +1717,7 @@ } }, "workspace.viewport.click-to-close-path" : { - "used-in" : [ "src/uxbox/main/ui/workspace/drawarea.cljs:357" ], + "used-in" : [ "src/uxbox/main/ui/workspace/drawarea.cljs:363" ], "translations" : { "en" : "Click to close the path" } diff --git a/frontend/resources/styles/common/base.scss b/frontend/resources/styles/common/base.scss index e42defc32b..1d7d218869 100644 --- a/frontend/resources/styles/common/base.scss +++ b/frontend/resources/styles/common/base.scss @@ -55,10 +55,10 @@ svg { a { cursor: pointer; - color: $color-primary; + color: $color-primary-dark; &:hover { - color: $color-primary-dark; + color: $color-primary; } } diff --git a/frontend/resources/styles/main-default.scss b/frontend/resources/styles/main-default.scss index 245db4e8d2..e3409d409b 100644 --- a/frontend/resources/styles/main-default.scss +++ b/frontend/resources/styles/main-default.scss @@ -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'; diff --git a/frontend/resources/styles/main/layouts/login.scss b/frontend/resources/styles/main/layouts/login.scss index b5e4a5d4ea..279a5e1285 100644 --- a/frontend/resources/styles/main/layouts/login.scss +++ b/frontend/resources/styles/main/layouts/login.scss @@ -5,115 +5,56 @@ // Copyright (c) 2015-2020 Andrey Antukh // Copyright (c) 2015-2020 Juan de la Cruz -.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; + } } diff --git a/frontend/resources/styles/main/layouts/main-layout.scss b/frontend/resources/styles/main/layouts/main-layout.scss index b15a9bd4ea..b9b62ef03d 100644 --- a/frontend/resources/styles/main/layouts/main-layout.scss +++ b/frontend/resources/styles/main/layouts/main-layout.scss @@ -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; + } +} + diff --git a/frontend/resources/styles/main/partials/forms.scss b/frontend/resources/styles/main/partials/forms.scss index b5e1a04e6a..9ae5d45fc0 100644 --- a/frontend/resources/styles/main/partials/forms.scss +++ b/frontend/resources/styles/main/partials/forms.scss @@ -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; + } + } +} diff --git a/frontend/resources/styles/main/partials/login.scss b/frontend/resources/styles/main/partials/login.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/resources/styles/main/partials/modal.scss b/frontend/resources/styles/main/partials/modal.scss new file mode 100644 index 0000000000..9ed66e6a02 --- /dev/null +++ b/frontend/resources/styles/main/partials/modal.scss @@ -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; + } + + } +} diff --git a/frontend/resources/styles/main/partials/user-settings.scss b/frontend/resources/styles/main/partials/user-settings.scss index f31b47a734..c06d0fc0b4 100644 --- a/frontend/resources/styles/main/partials/user-settings.scss +++ b/frontend/resources/styles/main/partials/user-settings.scss @@ -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; + } } } - diff --git a/frontend/src/uxbox/config.cljs b/frontend/src/uxbox/config.cljs index 6b2bd60cf6..c7246e394c 100644 --- a/frontend/src/uxbox/config.cljs +++ b/frontend/src/uxbox/config.cljs @@ -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"))) diff --git a/frontend/src/uxbox/main.cljs b/frontend/src/uxbox/main.cljs index 4eccb6d62e..c7107ec4a5 100644 --- a/frontend/src/uxbox/main.cljs +++ b/frontend/src/uxbox/main.cljs @@ -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)})) diff --git a/frontend/src/uxbox/main/data/auth.cljs b/frontend/src/uxbox/main/data/auth.cljs index a16d62c562..4f79190353 100644 --- a/frontend/src/uxbox/main/data/auth.cljs +++ b/frontend/src/uxbox/main/data/auth.cljs @@ -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 +;; 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 diff --git a/frontend/src/uxbox/main/data/dashboard.cljs b/frontend/src/uxbox/main/data/dashboard.cljs index 0b366e9e30..c151486796 100644 --- a/frontend/src/uxbox/main/data/dashboard.cljs +++ b/frontend/src/uxbox/main/data/dashboard.cljs @@ -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] diff --git a/frontend/src/uxbox/main/data/messages.cljs b/frontend/src/uxbox/main/data/messages.cljs index fb57382060..2b2f50c9d4 100644 --- a/frontend/src/uxbox/main/data/messages.cljs +++ b/frontend/src/uxbox/main/data/messages.cljs @@ -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})) diff --git a/frontend/src/uxbox/main/data/users.cljs b/frontend/src/uxbox/main/data/users.cljs index 32b0a509e1..8367175395 100644 --- a/frontend/src/uxbox/main/data/users.cljs +++ b/frontend/src/uxbox/main/data/users.cljs @@ -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)))))) diff --git a/frontend/src/uxbox/main/data/workspace/notifications.cljs b/frontend/src/uxbox/main/data/workspace/notifications.cljs index a254a34f90..3ff79680e1 100644 --- a/frontend/src/uxbox/main/data/workspace/notifications.cljs +++ b/frontend/src/uxbox/main/data/workspace/notifications.cljs @@ -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] diff --git a/frontend/src/uxbox/main/repo.cljs b/frontend/src/uxbox/main/repo.cljs index e6702eef20..03c58e69c3 100644 --- a/frontend/src/uxbox/main/repo.cljs +++ b/frontend/src/uxbox/main/repo.cljs @@ -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?) diff --git a/frontend/src/uxbox/main/ui.cljs b/frontend/src/uxbox/main/ui.cljs index 61375c4d53..b418982c22 100644 --- a/frontend/src/uxbox/main/ui.cljs +++ b/frontend/src/uxbox/main/ui.cljs @@ -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 [] diff --git a/frontend/src/uxbox/main/ui/auth.cljs b/frontend/src/uxbox/main/ui/auth.cljs new file mode 100644 index 0000000000..f410a64ca8 --- /dev/null +++ b/frontend/src/uxbox/main/ui/auth.cljs @@ -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])) diff --git a/frontend/src/uxbox/main/ui/auth/login.cljs b/frontend/src/uxbox/main/ui/auth/login.cljs new file mode 100644 index 0000000000..882480656f --- /dev/null +++ b/frontend/src/uxbox/main/ui/auth/login.cljs @@ -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")]]]]]) diff --git a/frontend/src/uxbox/main/ui/auth/recovery.cljs b/frontend/src/uxbox/main/ui/auth/recovery.cljs new file mode 100644 index 0000000000..a021e5e8cd --- /dev/null +++ b/frontend/src/uxbox/main/ui/auth/recovery.cljs @@ -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")]]]]]) + diff --git a/frontend/src/uxbox/main/ui/auth/recovery_request.cljs b/frontend/src/uxbox/main/ui/auth/recovery_request.cljs new file mode 100644 index 0000000000..f7b8b0803a --- /dev/null +++ b/frontend/src/uxbox/main/ui/auth/recovery_request.cljs @@ -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")]]]]]) diff --git a/frontend/src/uxbox/main/ui/auth/register.cljs b/frontend/src/uxbox/main/ui/auth/register.cljs new file mode 100644 index 0000000000..d516503144 --- /dev/null +++ b/frontend/src/uxbox/main/ui/auth/register.cljs @@ -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")]]]]]) diff --git a/frontend/src/uxbox/main/ui/components/forms.cljs b/frontend/src/uxbox/main/ui/components/forms.cljs new file mode 100644 index 0000000000..c00d8668ff --- /dev/null +++ b/frontend/src/uxbox/main/ui/components/forms.cljs @@ -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]])) + + + diff --git a/frontend/src/uxbox/main/ui/icons.cljs b/frontend/src/uxbox/main/ui/icons.cljs index 85f11ea14b..ca1e58fdaa 100644 --- a/frontend/src/uxbox/main/ui/icons.cljs +++ b/frontend/src/uxbox/main/ui/icons.cljs @@ -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)) diff --git a/frontend/src/uxbox/main/ui/login.cljs b/frontend/src/uxbox/main/ui/login.cljs deleted file mode 100644 index d1743e4c4c..0000000000 --- a/frontend/src/uxbox/main/ui/login.cljs +++ /dev/null @@ -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 -;; Copyright (c) 2015-2020 Juan de la Cruz - -(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]]]) diff --git a/frontend/src/uxbox/main/ui/modal.cljs b/frontend/src/uxbox/main/ui/modal.cljs index 369df3aa40..cecb9b61bb 100644 --- a/frontend/src/uxbox/main/ui/modal.cljs +++ b/frontend/src/uxbox/main/ui/modal.cljs @@ -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 diff --git a/frontend/src/uxbox/main/ui/profile/recovery.cljs b/frontend/src/uxbox/main/ui/profile/recovery.cljs deleted file mode 100644 index 9b4ca3c4b1..0000000000 --- a/frontend/src/uxbox/main/ui/profile/recovery.cljs +++ /dev/null @@ -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 -;; Copyright (c) 2015-2020 Juan de la Cruz - -(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]]]) diff --git a/frontend/src/uxbox/main/ui/profile/recovery_request.cljs b/frontend/src/uxbox/main/ui/profile/recovery_request.cljs deleted file mode 100644 index 17abdf54c8..0000000000 --- a/frontend/src/uxbox/main/ui/profile/recovery_request.cljs +++ /dev/null @@ -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 -;; Copyright (c) 2015-2020 Juan de la Cruz - -(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]]]) diff --git a/frontend/src/uxbox/main/ui/profile/register.cljs b/frontend/src/uxbox/main/ui/profile/register.cljs deleted file mode 100644 index 1e7fd01854..0000000000 --- a/frontend/src/uxbox/main/ui/profile/register.cljs +++ /dev/null @@ -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 -;; Copyright (c) 2015-2017 Juan de la Cruz - -(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]]]) diff --git a/frontend/src/uxbox/main/ui/settings.cljs b/frontend/src/uxbox/main/ui/settings.cljs index 6cb275bcba..3d9bcf99ea 100644 --- a/frontend/src/uxbox/main/ui/settings.cljs +++ b/frontend/src/uxbox/main/ui/settings.cljs @@ -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 -;; Copyright (c) 2015-2016 Juan de la Cruz +;; 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))]])) diff --git a/frontend/src/uxbox/main/ui/settings/change_email.cljs b/frontend/src/uxbox/main/ui/settings/change_email.cljs new file mode 100644 index 0000000000..8a433abad9 --- /dev/null +++ b/frontend/src/uxbox/main/ui/settings/change_email.cljs @@ -0,0 +1,102 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns uxbox.main.ui.settings.change-email + (:require + [cljs.spec.alpha :as s] + [cuerdas.core :as str] + [lentes.core :as l] + [rumext.alpha :as mf] + [uxbox.main.ui.icons :as i] + [uxbox.main.data.auth :as da] + [uxbox.main.data.users :as du] + [uxbox.main.ui.components.forms :refer [input submit-button form]] + [uxbox.main.data.messages :as dm] + [uxbox.main.store :as st] + [uxbox.main.refs :as refs] + [uxbox.util.dom :as dom] + [uxbox.util.forms :as fm] + [uxbox.main.ui.modal :as modal] + [uxbox.util.i18n :as i18n :refer [tr t]])) + +(s/def ::email-1 ::fm/email) +(s/def ::email-2 ::fm/email) +(s/def ::email-change-form + (s/keys :req-un [::email-1 ::email-2])) + +(defn- on-error + [form error] + (cond + (= (:code error) :uxbox.services.mutations.profile/email-already-exists) + (swap! form (fn [data] + (let [error {:message (tr "errors.email-already-exists")}] + (-> data + (assoc-in [:errors :email-1] error) + (assoc-in [:errors :email-2] error))))) + + :else + (let [msg (tr "errors.unexpected-error")] + (st/emit! (dm/error msg))))) + +(defn- on-submit + [form event] + (let [data (with-meta {:email (get-in form [:clean-data :email-1])} + {:on-error (partial on-error form)})] + (st/emit! (du/request-email-change data)))) + +(mf/defc change-email-form + [{:keys [locale profile] :as props}] + [:section.modal-content.generic-form + [:h2 (t locale "settings.change-email-title")] + + [:span.featured-note + [:span.text + [:span "We’ll send you an email to your current email "] + [:strong (:email profile)] + [:span " to verify your identity."]]] + + [:& form {:on-submit on-submit + :spec ::email-change-form + :initial {}} + [:& input {:type "text" + :name :email-1 + :label (t locale "settings.new-email-label")}] + + [:& input {:type "text" + :name :email-2 + :label (t locale "settings.confirm-email-label")}] + + [:& submit-button + {:label (t locale "settings.change-email-submit-label")}]]]) + +(mf/defc change-email-confirmation + [{:keys [locale profile] :as locale}] + [:section.modal-content.generic-form.confirmation + [:h2 (t locale "settings.verification-sent-title")] + + [:span.featured-note + [:span.icon i/trash] + [:span.text + [:span (str/format "We have sent you an email to “")] + [:strong (:email profile)] + [:span "” Please follow the instructions to verify the email."]]] + + [:button.btn-primary.btn-large + {:on-click #(modal/hide!)} + (t locale "settings.close-modal-label")]]) + +(mf/defc change-email-modal + [props] + (let [locale (mf/deref i18n/locale) + profile (mf/deref refs/profile)] + [:section.generic-modal.change-email-modal + [:span.close {:on-click #(modal/hide!)} i/close] + (if (:pending-email profile) + [:& change-email-confirmation {:locale locale :profile profile}] + [:& change-email-form {:locale locale :profile profile}])])) diff --git a/frontend/src/uxbox/main/ui/settings/delete_account.cljs b/frontend/src/uxbox/main/ui/settings/delete_account.cljs new file mode 100644 index 0000000000..4ebfafd11d --- /dev/null +++ b/frontend/src/uxbox/main/ui/settings/delete_account.cljs @@ -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")]]]])) diff --git a/frontend/src/uxbox/main/ui/settings/header.cljs b/frontend/src/uxbox/main/ui/settings/header.cljs index e279d8e746..dfaa50a1f4 100644 --- a/frontend/src/uxbox/main/ui/settings/header.cljs +++ b/frontend/src/uxbox/main/ui/settings/header.cljs @@ -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")]]])) diff --git a/frontend/src/uxbox/main/ui/settings/notifications.cljs b/frontend/src/uxbox/main/ui/settings/notifications.cljs deleted file mode 100644 index 3662c836f6..0000000000 --- a/frontend/src/uxbox/main/ui/settings/notifications.cljs +++ /dev/null @@ -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 -;; Copyright (c) 2016 Juan de la Cruz - -(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")}] - ]]) diff --git a/frontend/src/uxbox/main/ui/settings/options.cljs b/frontend/src/uxbox/main/ui/settings/options.cljs new file mode 100644 index 0000000000..bf10f7d04a --- /dev/null +++ b/frontend/src/uxbox/main/ui/settings/options.cljs @@ -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}]]])) diff --git a/frontend/src/uxbox/main/ui/settings/password.cljs b/frontend/src/uxbox/main/ui/settings/password.cljs index f0ee7f3575..fa3ac3b667 100644 --- a/frontend/src/uxbox/main/ui/settings/password.cljs +++ b/frontend/src/uxbox/main/ui/settings/password.cljs @@ -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 -;; Copyright (c) 2016-2020 Juan de la Cruz +;; 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}]]])) diff --git a/frontend/src/uxbox/main/ui/settings/profile.cljs b/frontend/src/uxbox/main/ui/settings/profile.cljs index 583319af1f..146c4a646f 100644 --- a/frontend/src/uxbox/main/ui/settings/profile.cljs +++ b/frontend/src/uxbox/main/ui/settings/profile.cljs @@ -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 -;; Copyright (c) 2016-2017 Juan de la Cruz +;; 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}]]])) diff --git a/frontend/src/uxbox/main/worker.cljs b/frontend/src/uxbox/main/worker.cljs index 384be20d92..b8bde0c1b8 100644 --- a/frontend/src/uxbox/main/worker.cljs +++ b/frontend/src/uxbox/main/worker.cljs @@ -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! diff --git a/frontend/src/uxbox/util/dom.cljs b/frontend/src/uxbox/util/dom.cljs index 64c85d7877..a0c882b07e 100644 --- a/frontend/src/uxbox/util/dom.cljs +++ b/frontend/src/uxbox/util/dom.cljs @@ -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)) [] diff --git a/frontend/src/uxbox/util/forms.cljs b/frontend/src/uxbox/util/forms.cljs index 7e77d11e1a..5cf7227585 100644 --- a/frontend/src/uxbox/util/forms.cljs +++ b/frontend/src/uxbox/util/forms.cljs @@ -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 diff --git a/frontend/src/uxbox/util/http.cljs b/frontend/src/uxbox/util/http.cljs index 36cac6e846..24d1a74f38 100644 --- a/frontend/src/uxbox/util/http.cljs +++ b/frontend/src/uxbox/util/http.cljs @@ -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.) diff --git a/frontend/src/uxbox/util/http_api.cljs b/frontend/src/uxbox/util/http_api.cljs index b788717a7d..db63dad2ce 100644 --- a/frontend/src/uxbox/util/http_api.cljs +++ b/frontend/src/uxbox/util/http_api.cljs @@ -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) diff --git a/frontend/src/uxbox/util/object.cljs b/frontend/src/uxbox/util/object.cljs index 61d6527c1e..8e0f2a596e 100644 --- a/frontend/src/uxbox/util/object.cljs +++ b/frontend/src/uxbox/util/object.cljs @@ -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)) diff --git a/frontend/src/uxbox/util/websockets.cljs b/frontend/src/uxbox/util/websockets.cljs index f12a3ea0a0..28d981f14d 100644 --- a/frontend/src/uxbox/util/websockets.cljs +++ b/frontend/src/uxbox/util/websockets.cljs @@ -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") diff --git a/frontend/src/uxbox/worker.cljs b/frontend/src/uxbox/worker.cljs index 1e940c7f09..ba1651c690 100644 --- a/frontend/src/uxbox/worker.cljs +++ b/frontend/src/uxbox/worker.cljs @@ -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 diff --git a/frontend/src/uxbox/worker/thumbnails.cljs b/frontend/src/uxbox/worker/thumbnails.cljs index cff6157e51..74af402125 100644 --- a/frontend/src/uxbox/worker/thumbnails.cljs +++ b/frontend/src/uxbox/worker/thumbnails.cljs @@ -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)