mirror of
https://github.com/penpot/penpot.git
synced 2025-03-13 08:11:30 -05:00
🚧 Initial work on password recovery and register refactor.
This commit is contained in:
parent
bd5f25eabf
commit
9e68041326
28 changed files with 607 additions and 561 deletions
|
@ -18,6 +18,7 @@
|
|||
instaparse/instaparse {:mvn/version "1.4.10"}
|
||||
com.cognitect/transit-clj {:mvn/version "0.8.319"}
|
||||
|
||||
;; TODO: vendorize pgclient under `vertx-clojure/vertx-pgclient`
|
||||
io.vertx/vertx-pg-client {:mvn/version "3.8.4"}
|
||||
|
||||
vertx-clojure/vertx
|
||||
|
|
42
backend/resources/emails/en/password-recovery.mustache
Normal file
42
backend/resources/emails/en/password-recovery.mustache
Normal file
|
@ -0,0 +1,42 @@
|
|||
-- begin :subject
|
||||
Password recovery.
|
||||
-- end
|
||||
|
||||
-- begin :body-text
|
||||
Hello {{name}}!
|
||||
|
||||
You have requested a password recovery.
|
||||
|
||||
The token is:
|
||||
|
||||
{{ token }}
|
||||
-- end
|
||||
|
||||
-- begin :body-html
|
||||
<html>
|
||||
<head>
|
||||
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
|
||||
<meta content="width=device-width" name="viewport" />
|
||||
<title>title</title>
|
||||
{{> ../partials/inline_style }}
|
||||
</head>
|
||||
<body bgcolor="#f6f6f6" cz-shortcut-listen="true">
|
||||
<table class="body-wrap">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td bgcolor="#FFFFFF" class="container">
|
||||
<div class="logo">
|
||||
<img alt="UXBOX" src="{{#static}}images/email/logo.png{{/static}}" />
|
||||
</div>
|
||||
<p>TODO</p>
|
||||
<p>{{ token }}</p>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{{> ../partials/en/footer }}
|
||||
</body>
|
||||
</html>
|
||||
-- end
|
|
@ -2,7 +2,10 @@
|
|||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2019-2020 Andrey Antukh <niwi@niwi.nz>
|
||||
|
||||
(ns uxbox.core
|
||||
(:require
|
||||
|
|
|
@ -23,16 +23,6 @@
|
|||
{:static media/resolve-asset
|
||||
:comment (constantly nil)})
|
||||
|
||||
;; --- Register Email
|
||||
|
||||
(s/def ::name ::us/string)
|
||||
(s/def ::register
|
||||
(s/keys :req-un [::name]))
|
||||
|
||||
(def register
|
||||
"A new profile registration welcome email."
|
||||
(emails/build ::register default-context))
|
||||
|
||||
;; --- Public API
|
||||
|
||||
(defn render
|
||||
|
@ -56,3 +46,22 @@
|
|||
values ($1, $2) returning *"]
|
||||
(-> (db/query-one db/pool [sql data priority])
|
||||
(p/then' (constantly nil)))))
|
||||
|
||||
;; --- Emails
|
||||
|
||||
(s/def ::name ::us/string)
|
||||
(s/def ::register
|
||||
(s/keys :req-un [::name]))
|
||||
|
||||
(def register
|
||||
"A new profile registration welcome email."
|
||||
(emails/build ::register default-context))
|
||||
|
||||
(s/def ::token ::us/string)
|
||||
(s/def ::password-recovery
|
||||
(s/keys :req-un [::name ::token]))
|
||||
|
||||
(def password-recovery
|
||||
"A password recovery notification email."
|
||||
(emails/build ::password-recovery default-context))
|
||||
|
||||
|
|
|
@ -51,12 +51,6 @@
|
|||
:timeout 200
|
||||
:name "login-handler"})
|
||||
|
||||
echo-handler (rl/ratelimit handlers/echo-handler
|
||||
{:limit 1
|
||||
:period 5000
|
||||
:timeout 10
|
||||
:name "echo-handler"})
|
||||
|
||||
routes [["/sub/:file-id" {:interceptors [(vxi/cookies)
|
||||
(vxi/cors cors-opts)
|
||||
interceptors/format-response-body
|
||||
|
@ -64,10 +58,9 @@
|
|||
:get ws/handler}]
|
||||
|
||||
["/api" {:interceptors interceptors}
|
||||
["/echo" {:all echo-handler}]
|
||||
["/echo" {:all handlers/echo-handler}]
|
||||
["/login" {:post login-handler}]
|
||||
["/logout" {:post handlers/logout-handler}]
|
||||
["/register" {:post handlers/register-handler}]
|
||||
["/debug"
|
||||
["/emails" {:get debug/emails-list}]
|
||||
["/emails/:id" {:get debug/email}]]
|
||||
|
|
|
@ -16,28 +16,49 @@
|
|||
[vertx.web :as vw]
|
||||
[vertx.eventbus :as ve]))
|
||||
|
||||
(def mutation-types-hierarchy
|
||||
(-> (make-hierarchy)
|
||||
(derive :login ::unauthenticated)
|
||||
(derive :logout ::unauthenticated)
|
||||
(derive :register-profile ::unauthenticated)
|
||||
(derive :request-profile-recovery ::unauthenticated)
|
||||
(derive :recover-profile ::unauthenticated)))
|
||||
|
||||
(def query-types-hierarchy
|
||||
(make-hierarchy))
|
||||
|
||||
(defn query-handler
|
||||
[req]
|
||||
(let [type (get-in req [:path-params :type])
|
||||
(let [type (keyword (get-in req [:path-params :type]))
|
||||
data (merge (:params req)
|
||||
{::sq/type (keyword type)
|
||||
{::sq/type type
|
||||
:user (:user req)})]
|
||||
(-> (sq/handle (with-meta data {:req req}))
|
||||
(p/then' (fn [result]
|
||||
{:status 200
|
||||
:body result})))))
|
||||
(if (or (:user req)
|
||||
(isa? query-types-hierarchy type ::unauthenticated))
|
||||
(-> (sq/handle (with-meta data {:req req}))
|
||||
(p/then' (fn [result]
|
||||
{:status 200
|
||||
:body result})))
|
||||
{:status 403
|
||||
:body {:type :authentication
|
||||
:code :unauthorized}})))
|
||||
|
||||
(defn mutation-handler
|
||||
[req]
|
||||
(let [type (get-in req [:path-params :type])
|
||||
(let [type (keyword (get-in req [:path-params :type]))
|
||||
data (merge (:params req)
|
||||
(:body-params req)
|
||||
(:uploads req)
|
||||
{::sm/type (keyword type)
|
||||
{::sm/type type
|
||||
:user (:user req)})]
|
||||
(-> (sm/handle (with-meta data {:req req}))
|
||||
(p/then' (fn [result]
|
||||
{:status 200 :body result})))))
|
||||
(if (or (:user req)
|
||||
(isa? mutation-types-hierarchy type ::unauthenticated))
|
||||
(-> (sm/handle (with-meta data {:req req}))
|
||||
(p/then' (fn [result]
|
||||
{:status 200 :body result})))
|
||||
{:status 403
|
||||
:body {:type :authentication
|
||||
:code :unauthorized}})))
|
||||
|
||||
(defn login-handler
|
||||
[req]
|
||||
|
@ -60,23 +81,20 @@
|
|||
:cookies {"auth-token" nil}
|
||||
:body ""})))))
|
||||
|
||||
(defn register-handler
|
||||
[req]
|
||||
(let [data (merge (:body-params req)
|
||||
{::sm/type :register-profile})
|
||||
user-agent (get-in req [:headers "user-agent"])]
|
||||
(-> (sm/handle (with-meta data {:req req}))
|
||||
(p/then (fn [{:keys [id] :as user}]
|
||||
(session/create id user-agent)))
|
||||
(p/then' (fn [token]
|
||||
{:status 204
|
||||
:cookies {"auth-token" {:value token}}
|
||||
:body ""})))))
|
||||
;; (defn register-handler
|
||||
;; [req]
|
||||
;; (let [data (merge (:body-params req)
|
||||
;; {::sm/type :register-profile})
|
||||
;; user-agent (get-in req [:headers "user-agent"])]
|
||||
;; (-> (sm/handle (with-meta data {:req req}))
|
||||
;; (p/then (fn [{:keys [id] :as user}]
|
||||
;; (session/create id user-agent)))
|
||||
;; (p/then' (fn [token]
|
||||
;; {:status 204
|
||||
;; :body ""})))))
|
||||
|
||||
(defn echo-handler
|
||||
[req]
|
||||
;; (locking echo-handler
|
||||
;; (prn "echo-handler" (Thread/currentThread)))
|
||||
{:status 200
|
||||
:body {:params (:params req)
|
||||
:cookies (:cookies req)
|
||||
|
|
|
@ -17,9 +17,10 @@
|
|||
(defn retrieve
|
||||
"Retrieves a user id associated with the provided auth token."
|
||||
[token]
|
||||
(let [sql "select user_id from sessions where id = $1"]
|
||||
(-> (db/query-one db/pool [sql token])
|
||||
(p/then' (fn [row] (when row (:user-id row)))))))
|
||||
(when token
|
||||
(let [sql "select user_id from sessions where id = $1"]
|
||||
(-> (db/query-one db/pool [sql token])
|
||||
(p/then' (fn [row] (when row (:user-id row))))))))
|
||||
|
||||
(defn create
|
||||
[user-id user-agent]
|
||||
|
@ -52,11 +53,5 @@
|
|||
(p/then' (fn [user-id]
|
||||
(if user-id
|
||||
(update data :request assoc :user user-id)
|
||||
(spx/terminate (assoc data ::unauthorized true)))))
|
||||
(vc/handle-on-context))))
|
||||
:leave (fn [data]
|
||||
(if (::unauthorized data)
|
||||
(update data :response
|
||||
assoc :status 403 :body {:type :authentication
|
||||
:code :unauthorized})
|
||||
data))})
|
||||
data)))
|
||||
(vc/handle-on-context))))})
|
||||
|
|
|
@ -75,7 +75,7 @@
|
|||
:pass (:smtp-password config)
|
||||
:ssl (:smtp-ssl config)
|
||||
:tls (:smtp-tls config)
|
||||
:noop (not (:smtp-enabled config))})
|
||||
:enabled (:smtp-enabled config)})
|
||||
|
||||
(defn- send-email-to-console
|
||||
[email]
|
||||
|
@ -98,9 +98,9 @@
|
|||
[email]
|
||||
(p/future
|
||||
(let [config (get-smtp-config cfg/config)
|
||||
result (if (:noop config)
|
||||
(send-email-to-console email)
|
||||
(postal/send-message config email))]
|
||||
result (if (:enabled config)
|
||||
(postal/send-message config email)
|
||||
(send-email-to-console email))]
|
||||
(when (not= (:error result) :SUCCESS)
|
||||
(ex/raise :type :sendmail-error
|
||||
:code :email-not-sent
|
||||
|
|
|
@ -2,7 +2,10 @@
|
|||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) 2016-2019 Andrey Antukh <niwi@niwi.nz>
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2016-2020 Andrey Antukh <niwi@niwi.nz>
|
||||
|
||||
(ns uxbox.main
|
||||
(:require [mount.core :as mount]
|
||||
|
|
|
@ -2,7 +2,10 @@
|
|||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2019-2020 Andrey Antukh <niwi@niwi.nz>
|
||||
|
||||
(ns uxbox.services.init
|
||||
"A initialization of services."
|
||||
|
@ -26,8 +29,7 @@
|
|||
(require 'uxbox.services.mutations.projects)
|
||||
(require 'uxbox.services.mutations.project-files)
|
||||
(require 'uxbox.services.mutations.project-pages)
|
||||
(require 'uxbox.services.mutations.auth)
|
||||
(require 'uxbox.services.mutations.users)
|
||||
(require 'uxbox.services.mutations.profile)
|
||||
(require 'uxbox.services.mutations.user-attrs))
|
||||
|
||||
(defstate query-services
|
||||
|
|
|
@ -2,7 +2,10 @@
|
|||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2019-2020 Andrey Antukh <niwi@niwi.nz>
|
||||
|
||||
(ns uxbox.services.mutations
|
||||
(:require
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
|
||||
|
||||
(ns uxbox.services.mutations.auth
|
||||
(:require
|
||||
[clojure.spec.alpha :as s]
|
||||
[sodi.pwhash :as pwhash]
|
||||
[promesa.core :as p]
|
||||
[uxbox.config :as cfg]
|
||||
[uxbox.common.exceptions :as ex]
|
||||
[uxbox.common.spec :as us]
|
||||
[uxbox.db :as db]
|
||||
[uxbox.services.mutations :as sm]))
|
||||
|
||||
(def ^:private user-by-username-sql
|
||||
"select id, password
|
||||
from users
|
||||
where username=$1 or email=$1
|
||||
and deleted_at is null")
|
||||
|
||||
(s/def ::username ::us/string)
|
||||
(s/def ::password ::us/string)
|
||||
(s/def ::scope ::us/string)
|
||||
|
||||
(s/def ::login
|
||||
(s/keys :req-un [::username ::password]
|
||||
:opt-un [::scope]))
|
||||
|
||||
(sm/defmutation ::login
|
||||
[{:keys [username password scope] :as params}]
|
||||
(letfn [(check-password [user password]
|
||||
(let [result (pwhash/verify password (:password user))]
|
||||
(:valid result)))
|
||||
|
||||
(check-user [user]
|
||||
(when-not user
|
||||
(ex/raise :type :validation
|
||||
:code ::wrong-credentials))
|
||||
(when-not (check-password user password)
|
||||
(ex/raise :type :validation
|
||||
:code ::wrong-credentials))
|
||||
|
||||
{:id (:id user)})]
|
||||
(-> (db/query-one db/pool [user-by-username-sql username])
|
||||
(p/then' check-user))))
|
|
@ -2,19 +2,24 @@
|
|||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) 2016 Andrey Antukh <niwi@niwi.nz>
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2016-2020 Andrey Antukh <niwi@niwi.nz>
|
||||
|
||||
(ns uxbox.services.mutations.users
|
||||
(ns uxbox.services.mutations.profile
|
||||
(:require
|
||||
[sodi.pwhash :as pwhash]
|
||||
[clojure.spec.alpha :as s]
|
||||
[datoteka.core :as fs]
|
||||
[datoteka.storages :as ds]
|
||||
[promesa.core :as p]
|
||||
[promesa.exec :as px]
|
||||
[uxbox.config :as cfg]
|
||||
[sodi.prng]
|
||||
[sodi.pwhash]
|
||||
[sodi.util]
|
||||
[uxbox.common.exceptions :as ex]
|
||||
[uxbox.common.spec :as us]
|
||||
[uxbox.config :as cfg]
|
||||
[uxbox.db :as db]
|
||||
[uxbox.emails :as emails]
|
||||
[uxbox.images :as images]
|
||||
|
@ -40,6 +45,46 @@
|
|||
(s/def ::user ::us/uuid)
|
||||
(s/def ::username ::us/string)
|
||||
|
||||
;; --- Utilities
|
||||
|
||||
(su/defstr sql:user-by-username-or-email
|
||||
"select u.*
|
||||
from users as u
|
||||
where u.username=$1 or u.email=$1
|
||||
and u.deleted_at is null")
|
||||
|
||||
(defn- retrieve-user
|
||||
[conn username]
|
||||
(db/query-one conn [sql:user-by-username-or-email username]))
|
||||
|
||||
;; --- Mutation: Login
|
||||
|
||||
(s/def ::username ::us/string)
|
||||
(s/def ::password ::us/string)
|
||||
(s/def ::scope ::us/string)
|
||||
|
||||
(s/def ::login
|
||||
(s/keys :req-un [::username ::password]
|
||||
:opt-un [::scope]))
|
||||
|
||||
(sm/defmutation ::login
|
||||
[{:keys [username password scope] :as params}]
|
||||
(letfn [(check-password [user password]
|
||||
(let [result (sodi.pwhash/verify password (:password user))]
|
||||
(:valid result)))
|
||||
|
||||
(check-user [user]
|
||||
(when-not user
|
||||
(ex/raise :type :validation
|
||||
:code ::wrong-credentials))
|
||||
(when-not (check-password user password)
|
||||
(ex/raise :type :validation
|
||||
:code ::wrong-credentials))
|
||||
|
||||
{:id (:id user)})]
|
||||
(-> (retrieve-user db/pool username)
|
||||
(p/then' check-user))))
|
||||
|
||||
;; --- Mutation: Update Profile (own)
|
||||
|
||||
(defn- check-username-and-email!
|
||||
|
@ -55,7 +100,7 @@
|
|||
and id != $1
|
||||
) as val"]
|
||||
(p/let [res1 (db/query-one conn [sql1 id username])
|
||||
res2 (db/query-one conn [sql2 id email])]
|
||||
res2 (db/query-one conn [sql2 id email])]
|
||||
(when (:val res1)
|
||||
(ex/raise :type :validation
|
||||
:code ::username-already-exists))
|
||||
|
@ -64,17 +109,21 @@
|
|||
:code ::email-already-exists))
|
||||
params)))
|
||||
|
||||
(su/defstr sql:update-profile
|
||||
"update users
|
||||
set username = $2,
|
||||
email = $3,
|
||||
fullname = $4,
|
||||
metadata = $5
|
||||
where id = $1
|
||||
and deleted_at is null
|
||||
returning *")
|
||||
|
||||
(defn- update-profile
|
||||
[conn {:keys [id username email fullname metadata] :as params}]
|
||||
(let [sql "update users
|
||||
set username = $2,
|
||||
email = $3,
|
||||
fullname = $4,
|
||||
metadata = $5
|
||||
where id = $1
|
||||
and deleted_at is null
|
||||
returning *"]
|
||||
(-> (db/query-one conn [sql id username email fullname (blob/encode metadata)])
|
||||
(let [sqlv [sql:update-profile id username
|
||||
email fullname (blob/encode metadata)]]
|
||||
(-> (db/query-one conn sqlv)
|
||||
(p/then' su/raise-not-found-if-nil)
|
||||
(p/then' decode-profile-row)
|
||||
(p/then' strip-private-attrs))))
|
||||
|
@ -94,7 +143,7 @@
|
|||
(defn- validate-password
|
||||
[conn {:keys [user old-password] :as params}]
|
||||
(p/let [profile (get-profile conn user)
|
||||
result (pwhash/verify old-password (:password profile))]
|
||||
result (sodi.pwhash/verify old-password (:password profile))]
|
||||
(when-not (:valid result)
|
||||
(ex/raise :type :validation
|
||||
:code ::old-password-not-match))
|
||||
|
@ -125,7 +174,6 @@
|
|||
|
||||
;; --- Mutation: Update Photo
|
||||
|
||||
|
||||
(s/def :uxbox$upload/name ::us/string)
|
||||
(s/def :uxbox$upload/size ::us/integer)
|
||||
(s/def :uxbox$upload/mtype ::us/string)
|
||||
|
@ -194,7 +242,7 @@
|
|||
[conn {:keys [id username fullname email password metadata] :as params}]
|
||||
(let [id (or id (uuid/next))
|
||||
metadata (blob/encode metadata)
|
||||
password (pwhash/derive password)
|
||||
password (sodi.pwhash/derive password)
|
||||
sqlv [create-user-sql
|
||||
id
|
||||
fullname
|
||||
|
@ -209,19 +257,15 @@
|
|||
[conn params]
|
||||
(-> (create-profile conn params)
|
||||
(p/then' strip-private-attrs)
|
||||
#_(p/then (fn [profile]
|
||||
(-> (emails/send! {::emails/id :users/register
|
||||
::emails/to (:email params)
|
||||
::emails/priority :high
|
||||
:name (:fullname params)})
|
||||
(p/then' (constantly profile)))))))
|
||||
(p/then (fn [profile]
|
||||
(-> (emails/send! emails/register {:to (:email params)
|
||||
:name (:fullname params)})
|
||||
(p/then' (constantly profile)))))))
|
||||
|
||||
(s/def ::register-profile
|
||||
(s/keys :req-un [::username ::email ::password ::fullname]))
|
||||
|
||||
(sm/defmutation :register-profile
|
||||
{:doc "Register new user."
|
||||
:spec ::register-profile}
|
||||
(sm/defmutation ::register-profile
|
||||
[params]
|
||||
(when-not (:registration-enabled cfg/config)
|
||||
(ex/raise :type :restriction
|
||||
|
@ -231,115 +275,56 @@
|
|||
(p/then (partial check-profile-existence! conn))
|
||||
(p/then (partial register-profile conn)))))
|
||||
|
||||
;; --- Password Recover
|
||||
;; --- Mutation: Request Profile Recovery
|
||||
|
||||
;; (defn- recovery-token-exists?
|
||||
;; "Checks if the token exists in the system. Just
|
||||
;; return `true` or `false`."
|
||||
;; [conn token]
|
||||
;; (let [sqlv (sql/recovery-token-exists? {:token token})
|
||||
;; result (db/fetch-one conn sqlv)]
|
||||
;; (:token_exists result)))
|
||||
(s/def ::request-profile-recovery
|
||||
(s/keys :req-un [::username]))
|
||||
|
||||
;; (defn- retrieve-user-for-recovery-token
|
||||
;; "Retrieve a user id (uuid) for the given token. If
|
||||
;; no user is found, an exception is raised."
|
||||
;; [conn token]
|
||||
;; (let [sqlv (sql/get-recovery-token {:token token})
|
||||
;; data (db/fetch-one conn sqlv)]
|
||||
;; (or (:user data)
|
||||
;; (ex/raise :type :validation
|
||||
;; :code ::invalid-token))))
|
||||
(su/defstr sql:insert-recovery-token
|
||||
"insert into tokens (user_id, token) values ($1, $2)")
|
||||
|
||||
;; (defn- mark-token-as-used
|
||||
;; [conn token]
|
||||
;; (let [sqlv (sql/mark-recovery-token-used {:token token})]
|
||||
;; (pos? (db/execute conn sqlv))))
|
||||
(sm/defmutation ::request-profile-recovery
|
||||
[{:keys [username] :as params}]
|
||||
(letfn [(create-recovery-token [conn {:keys [id] :as user}]
|
||||
(let [token (-> (sodi.prng/random-bytes 32)
|
||||
(sodi.util/bytes->b64s))
|
||||
sql sql:insert-recovery-token]
|
||||
(-> (db/query-one conn [sql id token])
|
||||
(p/then (constantly (assoc user :token token))))))
|
||||
(send-email-notification [conn user]
|
||||
(emails/send! emails/password-recovery
|
||||
{:to (:email user)
|
||||
:token (:token user)
|
||||
:name (:fullname user)}))]
|
||||
(db/with-atomic [conn db/pool]
|
||||
(-> (retrieve-user conn username)
|
||||
(p/then' su/raise-not-found-if-nil)
|
||||
(p/then #(create-recovery-token conn %))
|
||||
(p/then #(send-email-notification conn %))
|
||||
(p/then (constantly nil))))))
|
||||
|
||||
;; (defn- recover-password
|
||||
;; "Given a token and password, resets the password
|
||||
;; to corresponding user or raise an exception."
|
||||
;; [conn {:keys [token password]}]
|
||||
;; (let [user (retrieve-user-for-recovery-token conn token)]
|
||||
;; (update-password conn {:user user :password password})
|
||||
;; (mark-token-as-used conn token)
|
||||
;; nil))
|
||||
;; --- Mutation: Recover Profile
|
||||
|
||||
;; (defn- create-recovery-token
|
||||
;; "Creates a new recovery token for specified user and return it."
|
||||
;; [conn userid]
|
||||
;; (let [token (token/random)
|
||||
;; sqlv (sql/create-recovery-token {:user userid
|
||||
;; :token token})]
|
||||
;; (db/execute conn sqlv)
|
||||
;; token))
|
||||
(s/def ::token ::us/not-empty-string)
|
||||
(s/def ::recover-profile
|
||||
(s/keys :req-un [::token ::password]))
|
||||
|
||||
;; (defn- retrieve-user-for-password-recovery
|
||||
;; [conn username]
|
||||
;; (let [user (find-user-by-username-or-email conn username)]
|
||||
;; (when-not user
|
||||
;; (ex/raise :type :validation :code ::user-does-not-exists))
|
||||
;; user))
|
||||
|
||||
;; (defn- request-password-recovery
|
||||
;; "Creates a new recovery password token and sends it via email
|
||||
;; to the correspondig to the given username or email address."
|
||||
;; [conn username]
|
||||
;; (let [user (retrieve-user-for-password-recovery conn username)
|
||||
;; token (create-recovery-token conn (:id user))]
|
||||
;; (emails/send! {:email/name :users/password-recovery
|
||||
;; :email/to (:email user)
|
||||
;; :name (:fullname user)
|
||||
;; :token token})
|
||||
;; token))
|
||||
|
||||
;; (defmethod core/query :validate-profile-password-recovery-token
|
||||
;; [{:keys [token]}]
|
||||
;; (us/assert ::us/token token)
|
||||
;; (with-open [conn (db/connection)]
|
||||
;; (recovery-token-exists? conn token)))
|
||||
|
||||
;; (defmethod core/novelty :request-profile-password-recovery
|
||||
;; [{:keys [username]}]
|
||||
;; (us/assert ::us/username username)
|
||||
;; (with-open [conn (db/connection)]
|
||||
;; (db/atomic conn
|
||||
;; (request-password-recovery conn username))))
|
||||
|
||||
;; (s/def ::recover-password
|
||||
;; (s/keys :req-un [::us/token ::us/password]))
|
||||
|
||||
;; (defmethod core/novelty :recover-profile-password
|
||||
;; [params]
|
||||
;; (us/assert ::recover-password params)
|
||||
;; (with-open [conn (db/connection)]
|
||||
;; (db/apply-atomic conn recover-password params)))
|
||||
|
||||
;; --- Query Helpers
|
||||
|
||||
;; (defn find-full-user-by-id
|
||||
;; "Find user by its id. This function is for internal
|
||||
;; use only because it returns a lot of sensitive information.
|
||||
;; If no user is found, `nil` is returned."
|
||||
;; [conn id]
|
||||
;; (let [sqlv (sql/get-profile {:id id})]
|
||||
;; (some-> (db/fetch-one conn sqlv)
|
||||
;; (data/normalize-attrs))))
|
||||
|
||||
;; (defn find-user-by-id
|
||||
;; "Find user by its id. If no user is found, `nil` is returned."
|
||||
;; [conn id]
|
||||
;; (let [sqlv (sql/get-profile {:id id})]
|
||||
;; (some-> (db/fetch-one conn sqlv)
|
||||
;; (data/normalize-attrs)
|
||||
;; (trim-user-attrs)
|
||||
;; (dissoc :password))))
|
||||
|
||||
;; (defn find-user-by-username-or-email
|
||||
;; "Finds a user in the database by username and email. If no
|
||||
;; user is found, `nil` is returned."
|
||||
;; [conn username]
|
||||
;; (let [sqlv (sql/get-profile-by-username {:username username})]
|
||||
;; (some-> (db/fetch-one conn sqlv)
|
||||
;; (trim-user-attrs))))
|
||||
(su/defstr sql:remove-recovery-token
|
||||
"delete from tokenes where user_id=$1 and token=$2")
|
||||
|
||||
(sm/defmutation ::recover-profile
|
||||
[{:keys [token password]}]
|
||||
(letfn [(validate-token [conn token]
|
||||
(let [sql "delete from tokens where token=$1 returning *"
|
||||
sql "select * from tokens where token=$1"]
|
||||
(-> (db/query-one conn [sql token])
|
||||
(p/then' :user-id)
|
||||
(p/then' su/raise-not-found-if-nil))))
|
||||
(update-password [conn user-id]
|
||||
(let [sql "update users set password=$2 where id=$1"
|
||||
pwd (sodi.pwhash/derive password)]
|
||||
(-> (db/query-one conn [sql user-id pwd])
|
||||
(p/then' (constantly nil)))))]
|
||||
(db/with-atomic [conn db/pool]
|
||||
(-> (validate-token conn token)
|
||||
(p/then (fn [user-id] (update-password conn user-id)))))))
|
|
@ -2,7 +2,10 @@
|
|||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz>
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2019-2020 Andrey Antukh <niwi@niwi.nz>
|
||||
|
||||
(ns uxbox.services.mutations.project-files
|
||||
(:require
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
[cuerdas.core :as str]
|
||||
[mount.core :as mount]
|
||||
[datoteka.storages :as st]
|
||||
[uxbox.services.mutations.users :as users]
|
||||
[uxbox.services.mutations.profile :as profile]
|
||||
[uxbox.services.mutations.projects :as projects]
|
||||
[uxbox.services.mutations.project-files :as files]
|
||||
[uxbox.services.mutations.project-pages :as pages]
|
||||
|
@ -61,11 +61,11 @@
|
|||
[prefix & args]
|
||||
(uuid/namespaced uuid/oid (apply str prefix args)))
|
||||
|
||||
;; --- Users creation
|
||||
;; --- Profile creation
|
||||
|
||||
(defn create-user
|
||||
[conn i]
|
||||
(users/create-profile conn {:id (mk-uuid "user" i)
|
||||
(profile/create-profile conn {:id (mk-uuid "user" i)
|
||||
:fullname (str "User " i)
|
||||
:username (str "user" i)
|
||||
:email (str "user" i ".test@uxbox.io")
|
||||
|
|
|
@ -13,14 +13,58 @@
|
|||
"fr" : "Mot de passe oublié ?"
|
||||
}
|
||||
},
|
||||
"auth.message.password-recovered" : {
|
||||
|
||||
"profile.recovery.password-changed" : {
|
||||
"used-in" : [ "src/uxbox/main/data/auth.cljs:178" ],
|
||||
"translations" : {
|
||||
"en" : "Password successfully recovered.",
|
||||
"fr" : "Mot de passe récupéré avec succès."
|
||||
"en" : "Password successfully changed",
|
||||
"fr" : "TODO"
|
||||
}
|
||||
},
|
||||
"auth.message.recovery-token-sent" : {
|
||||
|
||||
"profile.recovery.username-or-email": {
|
||||
"translations" : {
|
||||
"en" : "Username or Email Address",
|
||||
"fr" : "adresse email ou nom d'utilisateur"
|
||||
}
|
||||
},
|
||||
|
||||
"profile.recovery.token": {
|
||||
"translations" : {
|
||||
"en" : "Recovery token (sent by email)",
|
||||
"fr" : null
|
||||
}
|
||||
},
|
||||
|
||||
"profile.recovery.password": {
|
||||
"translations" : {
|
||||
"en" : "Type a new password",
|
||||
"fr" : null
|
||||
}
|
||||
},
|
||||
|
||||
"profile.recovery.submit-request": {
|
||||
"translations" : {
|
||||
"en" : "Recover Password",
|
||||
"fr" : null
|
||||
}
|
||||
},
|
||||
|
||||
"profile.recovery.submit-recover": {
|
||||
"translations" : {
|
||||
"en" : "Change your password",
|
||||
"fr" : null
|
||||
}
|
||||
},
|
||||
|
||||
"profile.recovery.go-to-login": {
|
||||
"translations" : {
|
||||
"en" : "Go back!",
|
||||
"fr" : "Retour!"
|
||||
}
|
||||
},
|
||||
|
||||
"profile.recovery.recovery-token-sent" : {
|
||||
"used-in" : [ "src/uxbox/main/data/auth.cljs:141" ],
|
||||
"translations" : {
|
||||
"en" : "Password recovery link sent to your inbox.",
|
||||
|
@ -398,7 +442,7 @@
|
|||
"fr" : "Une erreur inattendue c'est produite"
|
||||
}
|
||||
},
|
||||
"errors.auth.invalid-recovery-token" : {
|
||||
"profile.recovery.invalid-token" : {
|
||||
"used-in" : [ "src/uxbox/main/data/auth.cljs:174", "src/uxbox/main/data/auth.cljs:151" ],
|
||||
"translations" : {
|
||||
"en" : "The recovery token is invalid.",
|
||||
|
@ -468,28 +512,28 @@
|
|||
"fr" : "Envoyer un fichier"
|
||||
}
|
||||
},
|
||||
"register.already-have-account" : {
|
||||
"profile.register.already-have-account" : {
|
||||
"used-in" : [ "src/uxbox/main/ui/auth/register.cljs:131" ],
|
||||
"translations" : {
|
||||
"en" : "Already have an account?",
|
||||
"fr" : "Vous avez déjà un compte ?"
|
||||
}
|
||||
},
|
||||
"register.fullname.placeholder" : {
|
||||
"profile.register.fullname" : {
|
||||
"used-in" : [ "src/uxbox/main/ui/auth/register.cljs:72" ],
|
||||
"translations" : {
|
||||
"en" : "Full Name",
|
||||
"fr" : "Nom complet"
|
||||
}
|
||||
},
|
||||
"register.get-started" : {
|
||||
"profile.register.get-started" : {
|
||||
"used-in" : [ "src/uxbox/main/ui/auth/register.cljs:127" ],
|
||||
"translations" : {
|
||||
"en" : "Get started",
|
||||
"fr" : "Commencer"
|
||||
}
|
||||
},
|
||||
"register.password.placeholder" : {
|
||||
"profile.register.password" : {
|
||||
"used-in" : [ "src/uxbox/main/ui/auth/register.cljs:115" ],
|
||||
"translations" : {
|
||||
"en" : "Password",
|
||||
|
@ -615,7 +659,7 @@
|
|||
"fr" : "Votre avatar"
|
||||
}
|
||||
},
|
||||
"settings.profile.your-email" : {
|
||||
"profile.register.email" : {
|
||||
"used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:111", "src/uxbox/main/ui/auth/register.cljs:101" ],
|
||||
"translations" : {
|
||||
"en" : "Your email",
|
||||
|
@ -629,7 +673,7 @@
|
|||
"fr" : "Votre nom complet"
|
||||
}
|
||||
},
|
||||
"settings.profile.your-username" : {
|
||||
"profile.register.username" : {
|
||||
"used-in" : [ "src/uxbox/main/ui/settings/profile.cljs:98", "src/uxbox/main/ui/auth/register.cljs:87" ],
|
||||
"translations" : {
|
||||
"en" : "Your username",
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2015-2019 Andrey Antukh <niwi@niwi.nz>
|
||||
|
||||
(ns ^:figwheel-hooks uxbox.main
|
||||
|
|
|
@ -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) 2015-2016 Andrey Antukh <niwi@niwi.nz>
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2015-2019 Andrey Antukh <niwi@niwi.nz>
|
||||
|
||||
(ns uxbox.main.data.auth
|
||||
(:require
|
||||
|
@ -10,7 +13,7 @@
|
|||
[beicon.core :as rx]
|
||||
[potok.core :as ptk]
|
||||
[uxbox.common.spec :as us]
|
||||
[uxbox.main.repo :as rp]
|
||||
[uxbox.main.repo.core :as rp]
|
||||
[uxbox.main.store :refer [initial-state]]
|
||||
[uxbox.main.data.users :as du]
|
||||
[uxbox.util.messages :as um]
|
||||
|
@ -57,8 +60,7 @@
|
|||
:password password
|
||||
:scope "webapp"}
|
||||
on-error #(rx/of (um/error (tr "errors.auth.unauthorized")))]
|
||||
(->> (rp/req :auth/login params)
|
||||
(rx/map :payload)
|
||||
(->> (rp/mutation :login params)
|
||||
(rx/map logged-in)
|
||||
(rx/catch rp/client-error? on-error))))))
|
||||
|
||||
|
@ -72,7 +74,7 @@
|
|||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state s]
|
||||
(->> (rp/req :auth/logout)
|
||||
(->> (rp/mutation :logout)
|
||||
(rx/ignore)))
|
||||
|
||||
ptk/EffectEvent
|
||||
|
@ -84,12 +86,12 @@
|
|||
(ptk/reify ::logout
|
||||
ptk/WatchEvent
|
||||
(watch [_ state s]
|
||||
(rx/of (rt/nav :auth/login)
|
||||
(rx/of (rt/nav :login)
|
||||
clear-user-data))))
|
||||
|
||||
;; --- Register
|
||||
|
||||
(s/def ::register-params
|
||||
(s/def ::register
|
||||
(s/keys :req-un [::fullname
|
||||
::username
|
||||
::password
|
||||
|
@ -98,84 +100,53 @@
|
|||
(defn register
|
||||
"Create a register event instance."
|
||||
[data on-error]
|
||||
(us/assert ::register-params data)
|
||||
(us/assert fn? on-error)
|
||||
(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))]
|
||||
(rx/merge
|
||||
(->> (rp/req :auth/register data)
|
||||
(rx/map :payload)
|
||||
(rx/map (constantly ::registered))
|
||||
(rx/catch rp/client-error? handle-error))
|
||||
(->> stream
|
||||
(rx/filter #(= % ::registered))
|
||||
(rx/take 1)
|
||||
(rx/map #(login data))))))))
|
||||
(->> (rp/mutation :register-profile data)
|
||||
(rx/map (fn [_] (login data)))
|
||||
(rx/catch rp/client-error? handle-error))))))
|
||||
|
||||
;; --- Recovery Request
|
||||
|
||||
(s/def ::recovery-request-params
|
||||
(s/def ::recovery-request
|
||||
(s/keys :req-un [::username]))
|
||||
|
||||
(defn recovery-request
|
||||
[data]
|
||||
(us/assert ::recovery-request-params data)
|
||||
(ptk/reify ::recover-request
|
||||
(defn request-profile-recovery
|
||||
[data on-success]
|
||||
(us/assert ::recovery-request data)
|
||||
(us/assert fn? on-success)
|
||||
(ptk/reify ::request-profile-recovery
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(letfn [(on-error [{payload :payload}]
|
||||
(println "on-error" payload)
|
||||
(rx/empty))]
|
||||
(rx/merge
|
||||
(->> (rp/req :auth/recovery-request data)
|
||||
(rx/map (constantly ::recovery-requested))
|
||||
(rx/catch rp/client-error? on-error))
|
||||
(->> stream
|
||||
(rx/filter #(= % ::recovery-requested))
|
||||
(rx/take 1)
|
||||
;; TODO: this should be moved to the UI part
|
||||
(rx/map #(um/info (tr "auth.message.recovery-token-sent")))))))))
|
||||
|
||||
;; --- Check Recovery Token
|
||||
|
||||
(defrecord ValidateRecoveryToken [token]
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(letfn [(on-error [{payload :payload}]
|
||||
(rx/of
|
||||
(rt/navigate :auth/login)
|
||||
(um/error (tr "errors.auth.invalid-recovery-token"))))]
|
||||
(->> (rp/req :auth/validate-recovery-token token)
|
||||
(rx/ignore)
|
||||
(rx/catch rp/client-error? on-error)))))
|
||||
|
||||
(defn validate-recovery-token
|
||||
[token]
|
||||
{:pre [(string? token)]}
|
||||
(ValidateRecoveryToken. token))
|
||||
(->> (rp/mutation :request-profile-recovery data)
|
||||
(rx/tap on-success)
|
||||
(rx/catch rp/client-error? on-error))))))
|
||||
|
||||
;; --- Recovery (Password)
|
||||
|
||||
(s/def ::token string?)
|
||||
(s/def ::recovery-params
|
||||
(s/keys :req-un [::username ::token]))
|
||||
(s/def ::on-error fn?)
|
||||
(s/def ::on-success fn?)
|
||||
|
||||
(defn recovery
|
||||
[{:keys [token password] :as data}]
|
||||
(us/assert ::recovery-params data)
|
||||
(ptk/reify ::recovery
|
||||
(s/def ::recover-profile
|
||||
(s/keys :req-un [::password ::token ::on-error ::on-success]))
|
||||
|
||||
(defn recover-profile
|
||||
[{:keys [token password on-error on-success] :as data}]
|
||||
(us/assert ::recover-profile data)
|
||||
(ptk/reify ::recover-profile
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(letfn [(on-error [{payload :payload}]
|
||||
(rx/of (um/error (tr "errors.auth.invalid-recovery-token"))))
|
||||
(on-success [{payload :payload}]
|
||||
(rx/of
|
||||
(rt/navigate :auth/login)
|
||||
(um/info (tr "auth.message.password-recovered"))))]
|
||||
(->> (rp/req :auth/recovery {:token token :password password})
|
||||
(rx/mapcat on-success)
|
||||
(rx/catch rp/client-error? on-error))))))
|
||||
(->> (rp/mutation :recover-profile {:token token :password password})
|
||||
(rx/tap on-success)
|
||||
(rx/catch (fn [err]
|
||||
(on-error)
|
||||
(rx/empty)))))))
|
||||
|
|
|
@ -133,3 +133,24 @@
|
|||
(.append form (name key) val))
|
||||
(seq params))
|
||||
(send-mutation! id form)))
|
||||
|
||||
(defmethod mutation :login
|
||||
[id params]
|
||||
(let [url (str url "/login")]
|
||||
(->> (impl-send {:method :post :url url :body params})
|
||||
(rx/map conditional-decode)
|
||||
(rx/mapcat handle-response))))
|
||||
|
||||
(defmethod mutation :logout
|
||||
[id params]
|
||||
(let [url (str url "/logout")]
|
||||
(->> (impl-send {:method :post :url url :body params :auth false})
|
||||
(rx/map conditional-decode)
|
||||
(rx/mapcat handle-response))))
|
||||
|
||||
;; (defmethod mutation :register-profile
|
||||
;; [id params]
|
||||
;; (let [url (str url "/register")]
|
||||
;; (->> (impl-send {:method :post :url url :body params :auth false})
|
||||
;; (rx/map conditional-decode)
|
||||
;; (rx/mapcat handle-response))))
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2015-2017 Juan de la Cruz <delacruzgarciajuan@gmail.com>
|
||||
;; Copyright (c) 2015-2019 Andrey Antukh <niwi@niwi.nz>
|
||||
|
||||
|
@ -16,7 +19,11 @@
|
|||
[uxbox.main.data.auth :refer [logout]]
|
||||
[uxbox.main.data.projects :as dp]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.ui.auth :as auth]
|
||||
[uxbox.main.ui.login :refer [login-page]]
|
||||
[uxbox.main.ui.profile.register :refer [profile-register-page]]
|
||||
[uxbox.main.ui.profile.recovery-request :refer [profile-recovery-request-page]]
|
||||
[uxbox.main.ui.profile.recovery :refer [profile-recovery-page]]
|
||||
|
||||
[uxbox.main.ui.dashboard :as dashboard]
|
||||
[uxbox.main.ui.settings :as settings]
|
||||
[uxbox.main.ui.shapes]
|
||||
|
@ -29,14 +36,18 @@
|
|||
[uxbox.util.router :as rt]
|
||||
[uxbox.util.timers :as ts]))
|
||||
|
||||
(def route-iref
|
||||
(-> (l/key :route)
|
||||
(l/derive st/state)))
|
||||
|
||||
;; --- Routes
|
||||
|
||||
(def routes
|
||||
[["/auth"
|
||||
["/login" :auth/login]
|
||||
["/register" :auth/register]
|
||||
["/recovery/request" :auth/recovery-request]
|
||||
["/recovery/token/:token" :auth/recovery]]
|
||||
[["/login" :login]
|
||||
["/profile"
|
||||
["/register" :profile-register]
|
||||
["/recovery/request" :profile-recovery-request]
|
||||
["/recovery" :profile-recovery]]
|
||||
|
||||
["/settings"
|
||||
["/profile" :settings/profile]
|
||||
|
@ -51,58 +62,15 @@
|
|||
|
||||
["/workspace/:file-id" :workspace]])
|
||||
|
||||
;; --- Error Handling
|
||||
|
||||
(defn- on-error
|
||||
"A default error handler."
|
||||
[{:keys [type code] :as error}]
|
||||
(reset! st/loader false)
|
||||
(cond
|
||||
(and (map? error)
|
||||
(= :validation type)
|
||||
(= :spec-validation code))
|
||||
(do
|
||||
(println "============ SERVER RESPONSE ERROR ================")
|
||||
(println (:explain error))
|
||||
(println "============ END SERVER RESPONSE ERROR ================"))
|
||||
|
||||
;; Unauthorized or Auth timeout
|
||||
(and (map? error)
|
||||
(= :authentication type)
|
||||
(= :unauthorized code))
|
||||
(ts/schedule 0 #(st/emit! (rt/nav :auth/login)))
|
||||
|
||||
;; Network error
|
||||
(and (map? error)
|
||||
(= :unexpected type)
|
||||
(= :abort code))
|
||||
(ts/schedule 100 #(st/emit! (uum/error (tr "errors.network"))))
|
||||
|
||||
;; Something else
|
||||
:else
|
||||
(do
|
||||
(js/console.error error)
|
||||
(ts/schedule 100 #(st/emit! (uum/error (tr "errors.generic")))))))
|
||||
|
||||
(set! st/*on-error* on-error)
|
||||
|
||||
;; --- Main App (Component)
|
||||
|
||||
(def route-iref
|
||||
(-> (l/key :route)
|
||||
(l/derive st/state)))
|
||||
|
||||
(mf/defc app
|
||||
[props]
|
||||
(let [route (mf/deref route-iref)]
|
||||
(case (get-in route [:data :name])
|
||||
:auth/login (mf/element auth/login-page)
|
||||
:auth/register (mf/element auth/register-page)
|
||||
:login (mf/element login-page)
|
||||
|
||||
;; :auth/recovery-request (auth/recovery-request-page)
|
||||
;; :auth/recovery
|
||||
;; (let [token (get-in route [:params :path :token])]
|
||||
;; (auth/recovery-page token))
|
||||
:profile-register (mf/element profile-register-page)
|
||||
:profile-recovery-request (mf/element profile-recovery-request-page)
|
||||
:profile-recovery (mf/element profile-recovery-page)
|
||||
|
||||
(:settings/profile
|
||||
:settings/password
|
||||
|
@ -125,3 +93,37 @@
|
|||
:key file-id}])
|
||||
nil)))
|
||||
|
||||
;; --- Error Handling
|
||||
|
||||
(defn- on-error
|
||||
"A default error handler."
|
||||
[{:keys [type code] :as error}]
|
||||
(reset! st/loader false)
|
||||
(cond
|
||||
(and (map? error)
|
||||
(= :validation type)
|
||||
(= :spec-validation code))
|
||||
(do
|
||||
(println "============ SERVER RESPONSE ERROR ================")
|
||||
(println (:explain error))
|
||||
(println "============ END SERVER RESPONSE ERROR ================"))
|
||||
|
||||
;; Unauthorized or Auth timeout
|
||||
(and (map? error)
|
||||
(= :authentication type)
|
||||
(= :unauthorized code))
|
||||
(ts/schedule 0 #(st/emit! (rt/nav :login)))
|
||||
|
||||
;; Network error
|
||||
(and (map? error)
|
||||
(= :unexpected type)
|
||||
(= :abort code))
|
||||
(ts/schedule 100 #(st/emit! (uum/error (tr "errors.network"))))
|
||||
|
||||
;; Something else
|
||||
:else
|
||||
(do
|
||||
(js/console.error error)
|
||||
(ts/schedule 100 #(st/emit! (uum/error (tr "errors.generic")))))))
|
||||
|
||||
(set! st/*on-error* on-error)
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) 2016 Andrey Antukh <niwi@niwi.nz>
|
||||
|
||||
(ns uxbox.main.ui.auth
|
||||
(:require [uxbox.main.ui.auth.login :as login]
|
||||
[uxbox.main.ui.auth.register :as register]
|
||||
#_[uxbox.main.ui.auth.recovery-request :as recovery-request]
|
||||
#_[uxbox.main.ui.auth.recovery :as recovery]))
|
||||
|
||||
(def login-page login/login-page)
|
||||
(def register-page register/register-page)
|
||||
;; (def recovery-page recovery/recovery-page)
|
||||
;; (def recovery-request-page recovery-request/recovery-request-page)
|
|
@ -1,82 +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 Juan de la Cruz <delacruzgarciajuan@gmail.com>
|
||||
;; Copyright (c) 2015-2019 Andrey Antukh <niwi@niwi.nz>
|
||||
|
||||
(ns uxbox.main.ui.auth.recovery
|
||||
(:require
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[lentes.core :as l]
|
||||
[rumext.core :as mx :include-macros true]
|
||||
[uxbox.builtins.icons :as i]
|
||||
[uxbox.main.data.auth :as uda]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.ui.messages :refer [messages-widget]]
|
||||
[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]))
|
||||
|
||||
;; (def form-data (fm/focus-data :recovery st/state))
|
||||
;; (def form-errors (fm/focus-errors :recovery st/state))
|
||||
|
||||
;; (def assoc-value (partial fm/assoc-value :recovery))
|
||||
;; (def assoc-errors (partial fm/assoc-errors :recovery))
|
||||
;; (def clear-form (partial fm/clear-form :recovery))
|
||||
|
||||
;; ;; --- Recovery Form
|
||||
|
||||
;; (s/def ::password ::fm/non-empty-string)
|
||||
;; (s/def ::recovery-form
|
||||
;; (s/keys :req-un [::password]))
|
||||
|
||||
;; (mx/defc recovery-form
|
||||
;; {:mixins [mx/static mx/reactive]}
|
||||
;; [token]
|
||||
;; (let [data (merge (mx/react form-data) {:token token})
|
||||
;; valid? (fm/valid? ::recovery-form data)]
|
||||
;; (letfn [(on-change [field event]
|
||||
;; (let [value (dom/event->value event)]
|
||||
;; (st/emit! (assoc-value field value))))
|
||||
;; (on-submit [event]
|
||||
;; (dom/prevent-default event)
|
||||
;; (st/emit! (uda/recovery data)
|
||||
;; (clear-form)))]
|
||||
;; [:form {:on-submit on-submit}
|
||||
;; [:div.login-content
|
||||
;; [:input.input-text
|
||||
;; {:name "password"
|
||||
;; :value (:password data "")
|
||||
;; :on-change (partial on-change :password)
|
||||
;; :placeholder (tr "recover.password.placeholder")
|
||||
;; :type "password"}]
|
||||
;; [:input.btn-primary
|
||||
;; {:name "login"
|
||||
;; :class (when-not valid? "btn-disabled")
|
||||
;; :disabled (not valid?)
|
||||
;; :value (tr "recover.recover-password")
|
||||
;; :type "submit"}]
|
||||
;; [:div.login-links
|
||||
;; [:a {:on-click #(st/emit! (rt/navigate :auth/login))} (tr "recover.go-back")]]]])))
|
||||
|
||||
;; ;; --- Recovery Page
|
||||
|
||||
;; (defn- recovery-page-init
|
||||
;; [own]
|
||||
;; (let [[token] (::mx/args own)]
|
||||
;; (st/emit! (uda/validate-recovery-token token))
|
||||
;; own))
|
||||
|
||||
;; (mx/defc recovery-page
|
||||
;; {:mixins [mx/static (fm/clear-mixin st/store :recovery)]
|
||||
;; :init recovery-page-init}
|
||||
;; [token]
|
||||
;; [:div.login
|
||||
;; [:div.login-body
|
||||
;; (messages-widget)
|
||||
;; [:a i/logo]
|
||||
;; (recovery-form token)]])
|
|
@ -1,71 +0,0 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) 2015-2017 Andrey Antukh <niwi@niwi.nz>
|
||||
;; Copyright (c) 2015-2017 Juan de la Cruz <delacruzgarciajuan@gmail.com>
|
||||
|
||||
(ns uxbox.main.ui.auth.recovery-request
|
||||
(:require [cljs.spec.alpha :as s :include-macros true]
|
||||
[lentes.core :as l]
|
||||
[cuerdas.core :as str]
|
||||
[uxbox.builtins.icons :as i]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.data.auth :as uda]
|
||||
[uxbox.main.ui.messages :refer [messages-widget]]
|
||||
[uxbox.main.ui.navigation :as nav]
|
||||
[uxbox.util.i18n :refer (tr)]
|
||||
[uxbox.util.dom :as dom]
|
||||
[uxbox.util.forms :as fm]
|
||||
[rumext.core :as mx :include-macros true]
|
||||
[uxbox.util.router :as rt]))
|
||||
|
||||
;; (def form-data (fm/focus-data :recovery-request st/state))
|
||||
;; (def form-errors (fm/focus-errors :recovery-request st/state))
|
||||
|
||||
;; (def assoc-value (partial fm/assoc-value :profile-password))
|
||||
;; (def assoc-errors (partial fm/assoc-errors :profile-password))
|
||||
;; (def clear-form (partial fm/clear-form :profile-password))
|
||||
|
||||
;; (s/def ::username ::fm/non-empty-string)
|
||||
;; (s/def ::recovery-request-form (s/keys :req-un [::username]))
|
||||
|
||||
;; (mx/defc recovery-request-form
|
||||
;; {:mixins [mx/static mx/reactive]}
|
||||
;; []
|
||||
;; (let [data (mx/react form-data)
|
||||
;; valid? (fm/valid? ::recovery-request-form data)]
|
||||
;; (letfn [(on-change [event]
|
||||
;; (let [value (dom/event->value event)]
|
||||
;; (st/emit! (assoc-value :username value))))
|
||||
;; (on-submit [event]
|
||||
;; (dom/prevent-default event)
|
||||
;; (st/emit! (uda/recovery-request data)
|
||||
;; (clear-form)))]
|
||||
;; [:form {:on-submit on-submit}
|
||||
;; [:div.login-content
|
||||
;; [:input.input-text
|
||||
;; {:name "username"
|
||||
;; :value (:username data "")
|
||||
;; :on-change on-change
|
||||
;; :placeholder (tr "recovery-request.username-or-email.placeholder")
|
||||
;; :type "text"}]
|
||||
;; [:input.btn-primary
|
||||
;; {:name "login"
|
||||
;; :class (when-not valid? "btn-disabled")
|
||||
;; :disabled (not valid?)
|
||||
;; :value (tr "recovery-request.recover-password")
|
||||
;; :type "submit"}]
|
||||
;; [:div.login-links
|
||||
;; [:a {:on-click #(st/emit! (rt/navigate :auth/login))} (tr "recovery-request.go-back")]]]])))
|
||||
|
||||
;; ;; --- Recovery Request Page
|
||||
|
||||
;; (mx/defc recovery-request-page
|
||||
;; {:mixins [mx/static (fm/clear-mixin st/store :recovery-request)]}
|
||||
;; []
|
||||
;; [:div.login
|
||||
;; [:div.login-body
|
||||
;; (messages-widget)
|
||||
;; [:a i/logo]
|
||||
;; (recovery-request-form)]])
|
|
@ -2,10 +2,13 @@
|
|||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) 2015-2016 Andrey Antukh <niwi@niwi.nz>
|
||||
;; Copyright (c) 2015-2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2015-2020 Andrey Antukh <niwi@niwi.nz>
|
||||
;; Copyright (c) 2015-2020 Juan de la Cruz <delacruzgarciajuan@gmail.com>
|
||||
|
||||
(ns uxbox.main.ui.auth.login
|
||||
(ns uxbox.main.ui.login
|
||||
(:require
|
||||
[cljs.spec.alpha :as s]
|
||||
[rumext.alpha :as mf]
|
||||
|
@ -78,10 +81,10 @@
|
|||
:type "submit"}]
|
||||
|
||||
[:div.login-links
|
||||
[:a {:on-click #(st/emit! (rt/nav :auth/recovery-request))
|
||||
[:a {:on-click #(st/emit! (rt/nav :profile-recovery-request))
|
||||
:tab-index "5"}
|
||||
(tr "auth.forgot-password")]
|
||||
[:a {:on-click #(st/emit! (rt/nav :auth/register))
|
||||
[:a {:on-click #(st/emit! (rt/nav :profile-register))
|
||||
:tab-index "6"}
|
||||
(tr "auth.no-account")]]]]))
|
||||
|
90
frontend/src/uxbox/main/ui/profile/recovery.cljs
Normal file
90
frontend/src/uxbox/main/ui/profile/recovery.cljs
Normal file
|
@ -0,0 +1,90 @@
|
|||
;; 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-2017 Andrey Antukh <niwi@niwi.nz>
|
||||
;; Copyright (c) 2015-2017 Juan de la Cruz <delacruzgarciajuan@gmail.com>
|
||||
|
||||
(ns uxbox.main.ui.profile.recovery
|
||||
(:require
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[lentes.core :as l]
|
||||
[rumext.alpha :as mf]
|
||||
[uxbox.builtins.icons :as i]
|
||||
[uxbox.common.spec :as us]
|
||||
[uxbox.main.data.auth :as uda]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.ui.messages :refer [messages-widget]]
|
||||
[uxbox.main.ui.navigation :as nav]
|
||||
[uxbox.util.messages :as um]
|
||||
[uxbox.util.dom :as dom]
|
||||
[uxbox.util.forms :as fm]
|
||||
[uxbox.util.i18n :as i18n]
|
||||
[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 {})
|
||||
tr (i18n/use-translations)
|
||||
on-success
|
||||
(fn []
|
||||
(st/emit! (um/info (tr "profile.recovery.password-changed"))
|
||||
(rt/nav :login)))
|
||||
|
||||
on-error
|
||||
(fn []
|
||||
(st/emit! (um/error (tr "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 (tr "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 (tr "profile.recovery.password")
|
||||
:type "password"}]
|
||||
[:input.btn-primary
|
||||
{:name "recover"
|
||||
:class (when-not (:valid form) "btn-disabled")
|
||||
:disabled (not (:valid form))
|
||||
:value (tr "profile.recovery.submit-recover")
|
||||
:type "submit"}]
|
||||
|
||||
[:div.login-links
|
||||
[:a {:on-click #(st/emit! (rt/nav :login))}
|
||||
(tr "profile.recovery.go-to-login")]]]]))
|
||||
|
||||
;; --- Recovery Request Page
|
||||
|
||||
(mf/defc profile-recovery-page
|
||||
[]
|
||||
[:div.login
|
||||
[:div.login-body
|
||||
[:& messages-widget]
|
||||
[:a i/logo]
|
||||
[:& recovery-form]]])
|
72
frontend/src/uxbox/main/ui/profile/recovery_request.cljs
Normal file
72
frontend/src/uxbox/main/ui/profile/recovery_request.cljs
Normal file
|
@ -0,0 +1,72 @@
|
|||
;; 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-2017 Andrey Antukh <niwi@niwi.nz>
|
||||
;; Copyright (c) 2015-2017 Juan de la Cruz <delacruzgarciajuan@gmail.com>
|
||||
|
||||
(ns uxbox.main.ui.profile.recovery-request
|
||||
(:require
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[lentes.core :as l]
|
||||
[rumext.alpha :as mf]
|
||||
[uxbox.builtins.icons :as i]
|
||||
[uxbox.common.spec :as us]
|
||||
[uxbox.main.data.auth :as uda]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.ui.messages :refer [messages-widget]]
|
||||
[uxbox.main.ui.navigation :as nav]
|
||||
[uxbox.util.messages :as um]
|
||||
[uxbox.util.dom :as dom]
|
||||
[uxbox.util.forms :as fm]
|
||||
[uxbox.util.i18n :as i18n]
|
||||
[uxbox.util.router :as rt]))
|
||||
|
||||
(s/def ::username ::us/not-empty-string)
|
||||
(s/def ::recovery-request-form (s/keys :req-un [::username]))
|
||||
|
||||
(mf/defc recovery-form
|
||||
[]
|
||||
(let [{:keys [data] :as form} (fm/use-form ::recovery-request-form {})
|
||||
tr (i18n/use-translations)
|
||||
on-success
|
||||
(fn []
|
||||
(st/emit! (um/info (tr "profile.recovery.recovery-token-sent"))))
|
||||
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 "username"
|
||||
:value (:username data "")
|
||||
:class (fm/error-class form :username)
|
||||
:on-blur (fm/on-input-blur form :username)
|
||||
:on-change (fm/on-input-change form :username)
|
||||
:placeholder (tr "profile.recovery.username-or-email")
|
||||
:type "text"}]
|
||||
[:input.btn-primary
|
||||
{:name "login"
|
||||
:class (when-not (:valid form) "btn-disabled")
|
||||
:disabled (not (:valid form))
|
||||
:value (tr "profile.recovery.submit-request")
|
||||
:type "submit"}]
|
||||
|
||||
[:div.login-links
|
||||
[:a {:on-click #(st/emit! (rt/nav :login))}
|
||||
(tr "profile.recovery.go-to-login")]]]]))
|
||||
|
||||
;; --- Recovery Request Page
|
||||
|
||||
(mf/defc profile-recovery-request-page
|
||||
[]
|
||||
[:div.login
|
||||
[:div.login-body
|
||||
[:& messages-widget]
|
||||
[:a i/logo]
|
||||
[:& recovery-form]]])
|
|
@ -5,7 +5,7 @@
|
|||
;; Copyright (c) 2015-2017 Andrey Antukh <niwi@niwi.nz>
|
||||
;; Copyright (c) 2015-2017 Juan de la Cruz <delacruzgarciajuan@gmail.com>
|
||||
|
||||
(ns uxbox.main.ui.auth.register
|
||||
(ns uxbox.main.ui.profile.register
|
||||
(:require
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
|
@ -69,7 +69,7 @@
|
|||
:class (fm/error-class form :fullname)
|
||||
:on-blur (fm/on-input-blur form :fullname)
|
||||
:on-change (fm/on-input-change form :fullname)
|
||||
:placeholder (tr "register.fullname.placeholder")
|
||||
:placeholder (tr "profile.register.fullname")
|
||||
:type "text"}]
|
||||
|
||||
[:& fm/field-error {:form form
|
||||
|
@ -84,7 +84,7 @@
|
|||
:on-blur (fm/on-input-blur form :username)
|
||||
:on-change (fm/on-input-change form :username)
|
||||
:value (:username data "")
|
||||
:placeholder (tr "settings.profile.your-username")}]
|
||||
:placeholder (tr "profile.register.username")}]
|
||||
|
||||
[:& fm/field-error {:form form
|
||||
:type #{::api}
|
||||
|
@ -98,7 +98,7 @@
|
|||
:on-blur (fm/on-input-blur form :email)
|
||||
:on-change (fm/on-input-change form :email)
|
||||
:value (:email data "")
|
||||
:placeholder (tr "settings.profile.your-email")}]
|
||||
:placeholder (tr "profile.register.email")}]
|
||||
|
||||
[:& fm/field-error {:form form
|
||||
:type #{::api}
|
||||
|
@ -112,7 +112,7 @@
|
|||
:class (fm/error-class form :password)
|
||||
:on-blur (fm/on-input-blur form :password)
|
||||
:on-change (fm/on-input-change form :password)
|
||||
:placeholder (tr "register.password.placeholder")
|
||||
:placeholder (tr "profile.register.password")
|
||||
:type "password"}]
|
||||
|
||||
[:& fm/field-error {:form form
|
||||
|
@ -124,15 +124,15 @@
|
|||
:tab-index "5"
|
||||
:class (when-not (:valid form) "btn-disabled")
|
||||
:disabled (not (:valid form))
|
||||
:value (tr "register.get-started")}]
|
||||
:value (tr "profile.register.get-started")}]
|
||||
|
||||
[:div.login-links
|
||||
[:a {:on-click #(st/emit! (rt/nav :auth/login))}
|
||||
(tr "register.already-have-account")]]]]))
|
||||
[:a {:on-click #(st/emit! (rt/nav :login))}
|
||||
(tr "profile.register.already-have-account")]]]]))
|
||||
|
||||
;; --- Register Page
|
||||
|
||||
(mf/defc register-page
|
||||
(mf/defc profile-register-page
|
||||
[props]
|
||||
[:div.login
|
||||
[:div.login-body
|
|
@ -42,7 +42,7 @@
|
|||
[:& header-link {:section :settings/notifications
|
||||
:content (tr "settings.notifications")}]]
|
||||
#_[:li {:on-click #(st/emit! (da/logout))}
|
||||
[:& header-link {:section :auth/login
|
||||
[:& header-link {:section :logout
|
||||
:content (tr "settings.exit")}]]]
|
||||
[:& user]]))
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue