mirror of
https://github.com/penpot/penpot.git
synced 2025-05-14 02:56:37 +02:00
♻️ Refactor profile registration flow.
This commit is contained in:
parent
c82d936e96
commit
9e3ba85b72
30 changed files with 717 additions and 581 deletions
|
@ -6,10 +6,13 @@
|
||||||
|
|
||||||
(ns app.http.oauth
|
(ns app.http.oauth
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.common.uri :as u]
|
[app.common.uri :as u]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
|
[app.db :as db]
|
||||||
|
[app.rpc.queries.profile :as profile]
|
||||||
[app.util.http :as http]
|
[app.util.http :as http]
|
||||||
[app.util.logging :as l]
|
[app.util.logging :as l]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
|
@ -25,11 +28,12 @@
|
||||||
:headers {"location" (str uri)}
|
:headers {"location" (str uri)}
|
||||||
:body ""})
|
:body ""})
|
||||||
|
|
||||||
(defn generate-error-redirect-uri
|
(defn generate-error-redirect
|
||||||
[cfg]
|
[cfg error]
|
||||||
(-> (u/uri (:public-uri cfg))
|
(let [uri (-> (u/uri (:public-uri cfg))
|
||||||
(assoc :path "/#/auth/login")
|
(assoc :path "/#/auth/login")
|
||||||
(assoc :query (u/map->query-string {:error "unable-to-auth"}))))
|
(assoc :query (u/map->query-string {:error "unable-to-auth" :hint (ex-message error)})))]
|
||||||
|
(redirect-response uri)))
|
||||||
|
|
||||||
(defn register-profile
|
(defn register-profile
|
||||||
[{:keys [rpc] :as cfg} info]
|
[{:keys [rpc] :as cfg} info]
|
||||||
|
@ -39,15 +43,33 @@
|
||||||
(some? (:invitation-token info))
|
(some? (:invitation-token info))
|
||||||
(assoc :invitation-token (:invitation-token info)))))
|
(assoc :invitation-token (:invitation-token info)))))
|
||||||
|
|
||||||
(defn generate-redirect-uri
|
(defn generate-redirect
|
||||||
[{:keys [tokens] :as cfg} profile]
|
[{:keys [tokens session] :as cfg} request info profile]
|
||||||
(let [token (or (:invitation-token profile)
|
(if profile
|
||||||
(tokens :generate {:iss :auth
|
(let [sxf ((:create session) (:id profile))
|
||||||
:exp (dt/in-future "15m")
|
token (or (:invitation-token info)
|
||||||
:profile-id (:id profile)}))]
|
(tokens :generate {:iss :auth
|
||||||
(-> (u/uri (:public-uri cfg))
|
:exp (dt/in-future "15m")
|
||||||
(assoc :path "/#/auth/verify-token")
|
:profile-id (:id profile)}))
|
||||||
(assoc :query (u/map->query-string {:token token})))))
|
params {:token token}
|
||||||
|
|
||||||
|
uri (-> (u/uri (:public-uri cfg))
|
||||||
|
(assoc :path "/#/auth/verify-token")
|
||||||
|
(assoc :query (u/map->query-string params)))]
|
||||||
|
(->> (redirect-response uri)
|
||||||
|
(sxf request)))
|
||||||
|
(let [info (assoc info
|
||||||
|
:iss :prepared-register
|
||||||
|
:exp (dt/in-future {:hours 48}))
|
||||||
|
token (tokens :generate info)
|
||||||
|
params (d/without-nils
|
||||||
|
{:token token
|
||||||
|
:fullname (:fullname info)})
|
||||||
|
uri (-> (u/uri (:public-uri cfg))
|
||||||
|
(assoc :path "/#/auth/register/validate")
|
||||||
|
(assoc :query (u/map->query-string params)))]
|
||||||
|
(redirect-response uri))))
|
||||||
|
|
||||||
|
|
||||||
(defn- build-redirect-uri
|
(defn- build-redirect-uri
|
||||||
[{:keys [provider] :as cfg}]
|
[{:keys [provider] :as cfg}]
|
||||||
|
@ -146,6 +168,7 @@
|
||||||
(string? roles) (into #{} (str/words roles))
|
(string? roles) (into #{} (str/words roles))
|
||||||
(vector? roles) (into #{} roles)
|
(vector? roles) (into #{} roles)
|
||||||
:else #{}))]
|
:else #{}))]
|
||||||
|
|
||||||
;; check if profile has a configured set of roles
|
;; check if profile has a configured set of roles
|
||||||
(when-not (set/subset? provider-roles profile-roles)
|
(when-not (set/subset? provider-roles profile-roles)
|
||||||
(ex/raise :type :internal
|
(ex/raise :type :internal
|
||||||
|
@ -188,18 +211,23 @@
|
||||||
{:status 200
|
{:status 200
|
||||||
:body {:redirect-uri uri}}))
|
:body {:redirect-uri uri}}))
|
||||||
|
|
||||||
|
(defn- retrieve-profile
|
||||||
|
[{:keys [pool] :as cfg} info]
|
||||||
|
(with-open [conn (db/open pool)]
|
||||||
|
(some->> (:email info)
|
||||||
|
(profile/retrieve-profile-data-by-email conn)
|
||||||
|
(profile/populate-additional-data conn))))
|
||||||
|
|
||||||
(defn- callback-handler
|
(defn- callback-handler
|
||||||
[{:keys [session] :as cfg} request]
|
[cfg request]
|
||||||
(try
|
(try
|
||||||
(let [info (retrieve-info cfg request)
|
(let [info (retrieve-info cfg request)
|
||||||
profile (register-profile cfg info)
|
profile (retrieve-profile cfg info)]
|
||||||
uri (generate-redirect-uri cfg profile)
|
(generate-redirect cfg request info profile))
|
||||||
sxf ((:create session) (:id profile))]
|
(catch Exception e
|
||||||
(->> (redirect-response uri)
|
(l/warn :hint "error on oauth process"
|
||||||
(sxf request)))
|
:cause e)
|
||||||
(catch Exception _e
|
(generate-error-redirect cfg e))))
|
||||||
(-> (generate-error-redirect-uri cfg)
|
|
||||||
(redirect-response)))))
|
|
||||||
|
|
||||||
;; --- INIT
|
;; --- INIT
|
||||||
|
|
||||||
|
@ -211,7 +239,7 @@
|
||||||
(s/def ::rpc map?)
|
(s/def ::rpc map?)
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec :app.http.oauth/handlers [_]
|
(defmethod ig/pre-init-spec :app.http.oauth/handlers [_]
|
||||||
(s/keys :req-un [::public-uri ::session ::tokens ::rpc]))
|
(s/keys :req-un [::public-uri ::session ::tokens ::rpc ::db/pool]))
|
||||||
|
|
||||||
(defn wrap-handler
|
(defn wrap-handler
|
||||||
[cfg handler]
|
[cfg handler]
|
||||||
|
|
|
@ -110,6 +110,7 @@
|
||||||
:app.http.oauth/handlers
|
:app.http.oauth/handlers
|
||||||
{:rpc (ig/ref :app.rpc/rpc)
|
{:rpc (ig/ref :app.rpc/rpc)
|
||||||
:session (ig/ref :app.http.session/session)
|
:session (ig/ref :app.http.session/session)
|
||||||
|
:pool (ig/ref :app.db/pool)
|
||||||
:tokens (ig/ref :app.tokens/tokens)
|
:tokens (ig/ref :app.tokens/tokens)
|
||||||
:public-uri (cf/get :public-uri)}
|
:public-uri (cf/get :public-uri)}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,10 @@
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.config :as cfg]
|
[app.config :as cfg]
|
||||||
[app.rpc.mutations.profile :refer [login-or-register]]
|
[app.db :as db]
|
||||||
|
[app.loggers.audit :as audit]
|
||||||
|
[app.rpc.mutations.profile :as profile-m]
|
||||||
|
[app.rpc.queries.profile :as profile-q]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[clj-ldap.client :as ldap]
|
[clj-ldap.client :as ldap]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
|
@ -34,6 +37,7 @@
|
||||||
;; --- Mutation: login-with-ldap
|
;; --- Mutation: login-with-ldap
|
||||||
|
|
||||||
(declare authenticate)
|
(declare authenticate)
|
||||||
|
(declare login-or-register)
|
||||||
|
|
||||||
(s/def ::email ::us/email)
|
(s/def ::email ::us/email)
|
||||||
(s/def ::password ::us/string)
|
(s/def ::password ::us/string)
|
||||||
|
@ -45,30 +49,36 @@
|
||||||
|
|
||||||
(sv/defmethod ::login-with-ldap {:auth false :rlimit :password}
|
(sv/defmethod ::login-with-ldap {:auth false :rlimit :password}
|
||||||
[{:keys [pool session tokens] :as cfg} {:keys [email password invitation-token] :as params}]
|
[{:keys [pool session tokens] :as cfg} {:keys [email password invitation-token] :as params}]
|
||||||
(let [info (authenticate params)
|
(db/with-atomic [conn pool]
|
||||||
cfg (assoc cfg :conn pool)]
|
(let [info (authenticate params)
|
||||||
(when-not info
|
cfg (assoc cfg :conn conn)]
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :wrong-credentials))
|
|
||||||
(let [profile (login-or-register cfg {:email (:email info)
|
|
||||||
:backend (:backend info)
|
|
||||||
:fullname (:fullname info)})]
|
|
||||||
(if-let [token (:invitation-token params)]
|
|
||||||
;; If invitation token comes in params, this is because the
|
|
||||||
;; user comes from team-invitation process; in this case,
|
|
||||||
;; regenerate token and send back to the user a new invitation
|
|
||||||
;; token (and mark current session as logged).
|
|
||||||
(let [claims (tokens :verify {:token token :iss :team-invitation})
|
|
||||||
claims (assoc claims
|
|
||||||
:member-id (:id profile)
|
|
||||||
:member-email (:email profile))
|
|
||||||
token (tokens :generate claims)]
|
|
||||||
(with-meta
|
|
||||||
{:invitation-token token}
|
|
||||||
{:transform-response ((:create session) (:id profile))}))
|
|
||||||
|
|
||||||
(with-meta profile
|
(when-not info
|
||||||
{:transform-response ((:create session) (:id profile))})))))
|
(ex/raise :type :validation
|
||||||
|
:code :wrong-credentials))
|
||||||
|
|
||||||
|
(let [profile (login-or-register cfg {:email (:email info)
|
||||||
|
:backend (:backend info)
|
||||||
|
:fullname (:fullname info)})]
|
||||||
|
(if-let [token (:invitation-token params)]
|
||||||
|
;; If invitation token comes in params, this is because the
|
||||||
|
;; user comes from team-invitation process; in this case,
|
||||||
|
;; regenerate token and send back to the user a new invitation
|
||||||
|
;; token (and mark current session as logged).
|
||||||
|
(let [claims (tokens :verify {:token token :iss :team-invitation})
|
||||||
|
claims (assoc claims
|
||||||
|
:member-id (:id profile)
|
||||||
|
:member-email (:email profile))
|
||||||
|
token (tokens :generate claims)]
|
||||||
|
(with-meta {:invitation-token token}
|
||||||
|
{:transform-response ((:create session) (:id profile))
|
||||||
|
::audit/props (:props profile)
|
||||||
|
::audit/profile-id (:id profile)}))
|
||||||
|
|
||||||
|
(with-meta profile
|
||||||
|
{:transform-response ((:create session) (:id profile))
|
||||||
|
::audit/props (:props profile)
|
||||||
|
::audit/profile-id (:id profile)}))))))
|
||||||
|
|
||||||
(defn- replace-several [s & {:as replacements}]
|
(defn- replace-several [s & {:as replacements}]
|
||||||
(reduce-kv clojure.string/replace s replacements))
|
(reduce-kv clojure.string/replace s replacements))
|
||||||
|
@ -88,11 +98,25 @@
|
||||||
(first (ldap/search cpool base-dn params))))
|
(first (ldap/search cpool base-dn params))))
|
||||||
|
|
||||||
(defn- authenticate
|
(defn- authenticate
|
||||||
[{:keys [password] :as params}]
|
[{:keys [password email] :as params}]
|
||||||
(with-open [conn (connect)]
|
(with-open [conn (connect)]
|
||||||
(when-let [{:keys [dn] :as luser} (get-ldap-user conn params)]
|
(when-let [{:keys [dn] :as luser} (get-ldap-user conn params)]
|
||||||
(when (ldap/bind? conn dn password)
|
(when (ldap/bind? conn dn password)
|
||||||
{:photo (get luser (keyword (cfg/get :ldap-attrs-photo)))
|
{:photo (get luser (keyword (cfg/get :ldap-attrs-photo)))
|
||||||
:fullname (get luser (keyword (cfg/get :ldap-attrs-fullname)))
|
:fullname (get luser (keyword (cfg/get :ldap-attrs-fullname)))
|
||||||
:email (get luser (keyword (cfg/get :ldap-attrs-email)))
|
:email email
|
||||||
:backend "ldap"}))))
|
:backend "ldap"}))))
|
||||||
|
|
||||||
|
(defn- login-or-register
|
||||||
|
[{:keys [conn] :as cfg} info]
|
||||||
|
(or (some->> (:email info)
|
||||||
|
(profile-q/retrieve-profile-data-by-email conn)
|
||||||
|
(profile-q/populate-additional-data conn)
|
||||||
|
(profile-q/decode-profile-row))
|
||||||
|
(let [params (-> info
|
||||||
|
(assoc :is-active true)
|
||||||
|
(assoc :is-demo false))]
|
||||||
|
(->> params
|
||||||
|
(profile-m/create-profile conn)
|
||||||
|
(profile-m/create-profile-relations conn)
|
||||||
|
(profile-q/strip-private-attrs)))))
|
||||||
|
|
|
@ -36,106 +36,14 @@
|
||||||
(s/def ::password ::us/not-empty-string)
|
(s/def ::password ::us/not-empty-string)
|
||||||
(s/def ::old-password ::us/not-empty-string)
|
(s/def ::old-password ::us/not-empty-string)
|
||||||
(s/def ::theme ::us/string)
|
(s/def ::theme ::us/string)
|
||||||
|
(s/def ::invitation-token ::us/not-empty-string)
|
||||||
;; --- Mutation: Register Profile
|
|
||||||
|
|
||||||
(declare annotate-profile-register)
|
(declare annotate-profile-register)
|
||||||
(declare check-profile-existence!)
|
(declare check-profile-existence!)
|
||||||
(declare create-profile)
|
(declare create-profile)
|
||||||
(declare create-profile-relations)
|
(declare create-profile-relations)
|
||||||
(declare email-domain-in-whitelist?)
|
|
||||||
(declare register-profile)
|
(declare register-profile)
|
||||||
|
|
||||||
(s/def ::invitation-token ::us/not-empty-string)
|
|
||||||
(s/def ::terms-privacy ::us/boolean)
|
|
||||||
|
|
||||||
(s/def ::register-profile
|
|
||||||
(s/keys :req-un [::email ::password ::fullname ::terms-privacy]
|
|
||||||
:opt-un [::invitation-token]))
|
|
||||||
|
|
||||||
(sv/defmethod ::register-profile {:auth false :rlimit :password}
|
|
||||||
[{:keys [pool tokens session] :as cfg} params]
|
|
||||||
(when-not (cfg/get :registration-enabled)
|
|
||||||
(ex/raise :type :restriction
|
|
||||||
:code :registration-disabled))
|
|
||||||
|
|
||||||
(when-let [domains (cfg/get :registration-domain-whitelist)]
|
|
||||||
(when-not (email-domain-in-whitelist? domains (:email params))
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :email-domain-is-not-allowed)))
|
|
||||||
|
|
||||||
(when-not (:terms-privacy params)
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :invalid-terms-and-privacy))
|
|
||||||
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(let [cfg (assoc cfg :conn conn)]
|
|
||||||
(register-profile cfg params))))
|
|
||||||
|
|
||||||
(defn- annotate-profile-register
|
|
||||||
"A helper for properly increase the profile-register metric once the
|
|
||||||
transaction is completed."
|
|
||||||
[metrics profile]
|
|
||||||
(fn []
|
|
||||||
(when (::created profile)
|
|
||||||
((get-in metrics [:definitions :profile-register]) :inc))))
|
|
||||||
|
|
||||||
(defn- register-profile
|
|
||||||
[{:keys [conn tokens session metrics] :as cfg} params]
|
|
||||||
(check-profile-existence! conn params)
|
|
||||||
(let [profile (->> (create-profile conn params)
|
|
||||||
(create-profile-relations conn))
|
|
||||||
profile (assoc profile ::created true)]
|
|
||||||
|
|
||||||
(sid/load-initial-project! conn profile)
|
|
||||||
|
|
||||||
(if-let [token (:invitation-token params)]
|
|
||||||
;; If invitation token comes in params, this is because the
|
|
||||||
;; user comes from team-invitation process; in this case,
|
|
||||||
;; regenerate token and send back to the user a new invitation
|
|
||||||
;; token (and mark current session as logged).
|
|
||||||
(let [claims (tokens :verify {:token token :iss :team-invitation})
|
|
||||||
claims (assoc claims
|
|
||||||
:member-id (:id profile)
|
|
||||||
:member-email (:email profile))
|
|
||||||
token (tokens :generate claims)
|
|
||||||
resp {:invitation-token token}]
|
|
||||||
(with-meta resp
|
|
||||||
{:transform-response ((:create session) (:id profile))
|
|
||||||
:before-complete (annotate-profile-register metrics profile)
|
|
||||||
::audit/props (:props profile)
|
|
||||||
::audit/profile-id (:id profile)}))
|
|
||||||
|
|
||||||
;; If no token is provided, send a verification email
|
|
||||||
(let [vtoken (tokens :generate
|
|
||||||
{:iss :verify-email
|
|
||||||
:exp (dt/in-future "48h")
|
|
||||||
:profile-id (:id profile)
|
|
||||||
:email (:email profile)})
|
|
||||||
ptoken (tokens :generate-predefined
|
|
||||||
{:iss :profile-identity
|
|
||||||
:profile-id (:id profile)})]
|
|
||||||
|
|
||||||
;; Don't allow proceed in register page if the email is
|
|
||||||
;; already reported as permanent bounced
|
|
||||||
(when (eml/has-bounce-reports? conn (:email profile))
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :email-has-permanent-bounces
|
|
||||||
:hint "looks like the email has one or many bounces reported"))
|
|
||||||
|
|
||||||
(eml/send! {::eml/conn conn
|
|
||||||
::eml/factory eml/register
|
|
||||||
:public-uri (:public-uri cfg)
|
|
||||||
:to (:email profile)
|
|
||||||
:name (:fullname profile)
|
|
||||||
:token vtoken
|
|
||||||
:extra-data ptoken})
|
|
||||||
|
|
||||||
(with-meta profile
|
|
||||||
{:before-complete (annotate-profile-register metrics profile)
|
|
||||||
::audit/props (:props profile)
|
|
||||||
::audit/profile-id (:id profile)})))))
|
|
||||||
|
|
||||||
(defn email-domain-in-whitelist?
|
(defn email-domain-in-whitelist?
|
||||||
"Returns true if email's domain is in the given whitelist or if
|
"Returns true if email's domain is in the given whitelist or if
|
||||||
given whitelist is an empty string."
|
given whitelist is an empty string."
|
||||||
|
@ -177,28 +85,171 @@
|
||||||
{:update false
|
{:update false
|
||||||
:valid false})))
|
:valid false})))
|
||||||
|
|
||||||
|
|
||||||
|
;; --- MUTATION: Prepare Register
|
||||||
|
|
||||||
|
(s/def ::prepare-register-profile
|
||||||
|
(s/keys :req-un [::email ::password]
|
||||||
|
:opt-un [::invitation-token]))
|
||||||
|
|
||||||
|
(sv/defmethod ::prepare-register-profile {:auth false}
|
||||||
|
[{:keys [pool tokens] :as cfg} params]
|
||||||
|
(when-not (cfg/get :registration-enabled)
|
||||||
|
(ex/raise :type :restriction
|
||||||
|
:code :registration-disabled))
|
||||||
|
|
||||||
|
(when-let [domains (cfg/get :registration-domain-whitelist)]
|
||||||
|
(when-not (email-domain-in-whitelist? domains (:email params))
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :email-domain-is-not-allowed)))
|
||||||
|
|
||||||
|
;; Don't allow proceed in preparing registration if the profile is
|
||||||
|
;; already reported as spamer.
|
||||||
|
(when (eml/has-bounce-reports? pool (:email params))
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :email-has-permanent-bounces
|
||||||
|
:hint "looks like the email has one or many bounces reported"))
|
||||||
|
|
||||||
|
(check-profile-existence! pool params)
|
||||||
|
|
||||||
|
(let [params (assoc params
|
||||||
|
:backend "penpot"
|
||||||
|
:iss :prepared-register
|
||||||
|
:exp (dt/in-future "48h"))
|
||||||
|
token (tokens :generate params)]
|
||||||
|
{:token token}))
|
||||||
|
|
||||||
|
;; --- MUTATION: Register Profile
|
||||||
|
|
||||||
|
(s/def ::accept-terms-and-privacy ::us/boolean)
|
||||||
|
(s/def ::accept-newsletter-subscription ::us/boolean)
|
||||||
|
(s/def ::token ::us/not-empty-string)
|
||||||
|
|
||||||
|
(s/def ::register-profile
|
||||||
|
(s/keys :req-un [::token ::fullname
|
||||||
|
::accept-terms-and-privacy]
|
||||||
|
:opt-un [::accept-newsletter-subscription]))
|
||||||
|
|
||||||
|
(sv/defmethod ::register-profile {:auth false :rlimit :password}
|
||||||
|
[{:keys [pool tokens session] :as cfg} params]
|
||||||
|
|
||||||
|
(when-not (:accept-terms-and-privacy params)
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :invalid-terms-and-privacy))
|
||||||
|
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(let [cfg (assoc cfg :conn conn)]
|
||||||
|
(register-profile cfg params))))
|
||||||
|
|
||||||
|
(defn- annotate-profile-register
|
||||||
|
"A helper for properly increase the profile-register metric once the
|
||||||
|
transaction is completed."
|
||||||
|
[metrics]
|
||||||
|
(fn []
|
||||||
|
((get-in metrics [:definitions :profile-register]) :inc)))
|
||||||
|
|
||||||
|
(defn register-profile
|
||||||
|
[{:keys [conn tokens session metrics] :as cfg} {:keys [token] :as params}]
|
||||||
|
(let [claims (tokens :verify {:token token :iss :prepared-register})
|
||||||
|
params (merge params claims)]
|
||||||
|
(check-profile-existence! conn params)
|
||||||
|
(let [profile (->> params
|
||||||
|
(create-profile conn)
|
||||||
|
(create-profile-relations conn))]
|
||||||
|
|
||||||
|
(sid/load-initial-project! conn profile)
|
||||||
|
|
||||||
|
(cond
|
||||||
|
;; If invitation token comes in params, this is because the
|
||||||
|
;; user comes from team-invitation process; in this case,
|
||||||
|
;; regenerate token and send back to the user a new invitation
|
||||||
|
;; token (and mark current session as logged).
|
||||||
|
(some? (:invitation-token params))
|
||||||
|
(let [token (:invitation-token params)
|
||||||
|
claims (tokens :verify {:token token :iss :team-invitation})
|
||||||
|
claims (assoc claims
|
||||||
|
:member-id (:id profile)
|
||||||
|
:member-email (:email profile))
|
||||||
|
token (tokens :generate claims)
|
||||||
|
resp {:invitation-token token}]
|
||||||
|
(with-meta resp
|
||||||
|
{:transform-response ((:create session) (:id profile))
|
||||||
|
:before-complete (annotate-profile-register metrics)
|
||||||
|
::audit/props (:props profile)
|
||||||
|
::audit/profile-id (:id profile)}))
|
||||||
|
|
||||||
|
;; If auth backend is different from "penpot" means user is
|
||||||
|
;; registring using third party auth mechanism; in this case
|
||||||
|
;; we need to mark this session as logged.
|
||||||
|
(not= "penpot" (:auth-backend profile))
|
||||||
|
(with-meta (profile/strip-private-attrs profile)
|
||||||
|
{:transform-response ((:create session) (:id profile))
|
||||||
|
:before-complete (annotate-profile-register metrics)
|
||||||
|
::audit/props (:props profile)
|
||||||
|
::audit/profile-id (:id profile)})
|
||||||
|
|
||||||
|
;; In all other cases, send a verification email.
|
||||||
|
:else
|
||||||
|
(let [vtoken (tokens :generate
|
||||||
|
{:iss :verify-email
|
||||||
|
:exp (dt/in-future "48h")
|
||||||
|
:profile-id (:id profile)
|
||||||
|
:email (:email profile)})
|
||||||
|
ptoken (tokens :generate-predefined
|
||||||
|
{:iss :profile-identity
|
||||||
|
:profile-id (:id profile)})]
|
||||||
|
|
||||||
|
(eml/send! {::eml/conn conn
|
||||||
|
::eml/factory eml/register
|
||||||
|
:public-uri (:public-uri cfg)
|
||||||
|
:to (:email profile)
|
||||||
|
:name (:fullname profile)
|
||||||
|
:token vtoken
|
||||||
|
:extra-data ptoken})
|
||||||
|
|
||||||
|
(with-meta profile
|
||||||
|
{:before-complete (annotate-profile-register metrics)
|
||||||
|
::audit/props (:props profile)
|
||||||
|
::audit/profile-id (:id profile)}))))))
|
||||||
|
|
||||||
(defn create-profile
|
(defn create-profile
|
||||||
"Create the profile entry on the database with limited input filling
|
"Create the profile entry on the database with limited input filling
|
||||||
all the other fields with defaults."
|
all the other fields with defaults."
|
||||||
[conn {:keys [id fullname email password is-active is-muted is-demo opts deleted-at]
|
[conn params]
|
||||||
:or {is-active false is-muted false is-demo false}
|
(let [id (or (:id params) (uuid/next))
|
||||||
:as params}]
|
|
||||||
(let [id (or id (uuid/next))
|
props (-> (extract-props params)
|
||||||
is-active (if is-demo true is-active)
|
(merge (:props params))
|
||||||
props (-> params extract-props db/tjson)
|
(assoc :accept-terms-and-privacy (:accept-terms-and-privacy params true))
|
||||||
password (derive-password password)
|
(assoc :accept-newsletter-subscription (:accept-newsletter-subscription params false))
|
||||||
|
(db/tjson))
|
||||||
|
|
||||||
|
password (if-let [password (:password params)]
|
||||||
|
(derive-password password)
|
||||||
|
"!")
|
||||||
|
|
||||||
|
locale (as-> (:locale params) locale
|
||||||
|
(and (string? locale) (not (str/blank? locale)) locale))
|
||||||
|
|
||||||
|
backend (:backend params "penpot")
|
||||||
|
is-demo (:is-demo params false)
|
||||||
|
is-muted (:is-muted params false)
|
||||||
|
is-active (:is-active params (or (not= "penpot" backend) is-demo))
|
||||||
|
email (str/lower (:email params))
|
||||||
|
|
||||||
params {:id id
|
params {:id id
|
||||||
:fullname fullname
|
:fullname (:fullname params)
|
||||||
:email (str/lower email)
|
:email email
|
||||||
:auth-backend "penpot"
|
:auth-backend backend
|
||||||
|
:lang locale
|
||||||
:password password
|
:password password
|
||||||
:deleted-at deleted-at
|
:deleted-at (:deleted-at params)
|
||||||
:props props
|
:props props
|
||||||
:is-active is-active
|
:is-active is-active
|
||||||
:is-muted is-muted
|
:is-muted is-muted
|
||||||
:is-demo is-demo}]
|
:is-demo is-demo}]
|
||||||
(try
|
(try
|
||||||
(-> (db/insert! conn :profile params opts)
|
(-> (db/insert! conn :profile params)
|
||||||
(update :props db/decode-transit-pgobject))
|
(update :props db/decode-transit-pgobject))
|
||||||
(catch org.postgresql.util.PSQLException e
|
(catch org.postgresql.util.PSQLException e
|
||||||
(let [state (.getSQLState e)]
|
(let [state (.getSQLState e)]
|
||||||
|
@ -231,7 +282,7 @@
|
||||||
(assoc :default-team-id (:id team))
|
(assoc :default-team-id (:id team))
|
||||||
(assoc :default-project-id (:id project)))))
|
(assoc :default-project-id (:id project)))))
|
||||||
|
|
||||||
;; --- Mutation: Login
|
;; --- MUTATION: Login
|
||||||
|
|
||||||
(s/def ::email ::us/email)
|
(s/def ::email ::us/email)
|
||||||
(s/def ::scope ::us/string)
|
(s/def ::scope ::us/string)
|
||||||
|
@ -286,7 +337,7 @@
|
||||||
{:transform-response ((:create session) (:id profile))
|
{:transform-response ((:create session) (:id profile))
|
||||||
::audit/profile-id (:id profile)}))))))
|
::audit/profile-id (:id profile)}))))))
|
||||||
|
|
||||||
;; --- Mutation: Logout
|
;; --- MUTATION: Logout
|
||||||
|
|
||||||
(s/def ::logout
|
(s/def ::logout
|
||||||
(s/keys :req-un [::profile-id]))
|
(s/keys :req-un [::profile-id]))
|
||||||
|
@ -296,74 +347,7 @@
|
||||||
(with-meta {}
|
(with-meta {}
|
||||||
{:transform-response (:delete session)}))
|
{:transform-response (:delete session)}))
|
||||||
|
|
||||||
|
;; --- MUTATION: Update Profile (own)
|
||||||
;; --- Mutation: Register if not exists
|
|
||||||
|
|
||||||
(declare login-or-register)
|
|
||||||
|
|
||||||
(s/def ::backend ::us/string)
|
|
||||||
(s/def ::login-or-register
|
|
||||||
(s/keys :req-un [::email ::fullname ::backend]))
|
|
||||||
|
|
||||||
(sv/defmethod ::login-or-register {:auth false}
|
|
||||||
[{:keys [pool metrics] :as cfg} params]
|
|
||||||
(db/with-atomic [conn pool]
|
|
||||||
(let [profile (-> (assoc cfg :conn conn)
|
|
||||||
(login-or-register params))
|
|
||||||
props (merge
|
|
||||||
(select-keys profile [:backend :fullname :email])
|
|
||||||
(:props profile))]
|
|
||||||
(with-meta profile
|
|
||||||
{:before-complete (annotate-profile-register metrics profile)
|
|
||||||
::audit/name (if (::created profile) "register" "login")
|
|
||||||
::audit/props props
|
|
||||||
::audit/profile-id (:id profile)}))))
|
|
||||||
|
|
||||||
(defn login-or-register
|
|
||||||
[{:keys [conn] :as cfg} {:keys [email] :as params}]
|
|
||||||
(letfn [(info->lang [{:keys [locale] :as info}]
|
|
||||||
(when (and (string? locale)
|
|
||||||
(not (str/blank? locale)))
|
|
||||||
locale))
|
|
||||||
|
|
||||||
(create-profile [conn {:keys [fullname backend email props] :as info}]
|
|
||||||
(let [params {:id (uuid/next)
|
|
||||||
:fullname fullname
|
|
||||||
:email (str/lower email)
|
|
||||||
:lang (info->lang props)
|
|
||||||
:auth-backend backend
|
|
||||||
:is-active true
|
|
||||||
:password "!"
|
|
||||||
:props (db/tjson props)
|
|
||||||
:is-demo false}]
|
|
||||||
(-> (db/insert! conn :profile params)
|
|
||||||
(update :props db/decode-transit-pgobject))))
|
|
||||||
|
|
||||||
(update-profile [conn info profile]
|
|
||||||
(let [props (merge (:props profile)
|
|
||||||
(:props info))]
|
|
||||||
(db/update! conn :profile
|
|
||||||
{:props (db/tjson props)
|
|
||||||
:modified-at (dt/now)}
|
|
||||||
{:id (:id profile)})
|
|
||||||
(assoc profile :props props)))
|
|
||||||
|
|
||||||
(register-profile [conn params]
|
|
||||||
(let [profile (->> (create-profile conn params)
|
|
||||||
(create-profile-relations conn))]
|
|
||||||
(sid/load-initial-project! conn profile)
|
|
||||||
(assoc profile ::created true)))]
|
|
||||||
|
|
||||||
(let [profile (profile/retrieve-profile-data-by-email conn email)
|
|
||||||
profile (if profile
|
|
||||||
(->> profile
|
|
||||||
(update-profile conn params)
|
|
||||||
(profile/populate-additional-data conn))
|
|
||||||
(register-profile conn params))]
|
|
||||||
(profile/strip-private-attrs profile))))
|
|
||||||
|
|
||||||
|
|
||||||
;; --- Mutation: Update Profile (own)
|
|
||||||
|
|
||||||
(defn- update-profile
|
(defn- update-profile
|
||||||
[conn {:keys [id fullname lang theme] :as params}]
|
[conn {:keys [id fullname lang theme] :as params}]
|
||||||
|
@ -383,7 +367,7 @@
|
||||||
(update-profile conn params)
|
(update-profile conn params)
|
||||||
nil))
|
nil))
|
||||||
|
|
||||||
;; --- Mutation: Update Password
|
;; --- MUTATION: Update Password
|
||||||
|
|
||||||
(declare validate-password!)
|
(declare validate-password!)
|
||||||
(declare update-profile-password!)
|
(declare update-profile-password!)
|
||||||
|
@ -412,7 +396,7 @@
|
||||||
{:password (derive-password password)}
|
{:password (derive-password password)}
|
||||||
{:id id}))
|
{:id id}))
|
||||||
|
|
||||||
;; --- Mutation: Update Photo
|
;; --- MUTATION: Update Photo
|
||||||
|
|
||||||
(declare update-profile-photo)
|
(declare update-profile-photo)
|
||||||
|
|
||||||
|
@ -447,7 +431,7 @@
|
||||||
nil)
|
nil)
|
||||||
|
|
||||||
|
|
||||||
;; --- Mutation: Request Email Change
|
;; --- MUTATION: Request Email Change
|
||||||
|
|
||||||
(declare request-email-change)
|
(declare request-email-change)
|
||||||
(declare change-email-inmediatelly)
|
(declare change-email-inmediatelly)
|
||||||
|
@ -515,7 +499,7 @@
|
||||||
[conn id]
|
[conn id]
|
||||||
(db/get-by-id conn :profile id {:for-update true}))
|
(db/get-by-id conn :profile id {:for-update true}))
|
||||||
|
|
||||||
;; --- Mutation: Request Profile Recovery
|
;; --- MUTATION: Request Profile Recovery
|
||||||
|
|
||||||
(s/def ::request-profile-recovery
|
(s/def ::request-profile-recovery
|
||||||
(s/keys :req-un [::email]))
|
(s/keys :req-un [::email]))
|
||||||
|
@ -564,7 +548,7 @@
|
||||||
(send-email-notification conn))))))
|
(send-email-notification conn))))))
|
||||||
|
|
||||||
|
|
||||||
;; --- Mutation: Recover Profile
|
;; --- MUTATION: Recover Profile
|
||||||
|
|
||||||
(s/def ::token ::us/not-empty-string)
|
(s/def ::token ::us/not-empty-string)
|
||||||
(s/def ::recover-profile
|
(s/def ::recover-profile
|
||||||
|
@ -585,7 +569,7 @@
|
||||||
(update-password conn))
|
(update-password conn))
|
||||||
nil)))
|
nil)))
|
||||||
|
|
||||||
;; --- Mutation: Update Profile Props
|
;; --- MUTATION: Update Profile Props
|
||||||
|
|
||||||
(s/def ::props map?)
|
(s/def ::props map?)
|
||||||
(s/def ::update-profile-props
|
(s/def ::update-profile-props
|
||||||
|
@ -607,7 +591,7 @@
|
||||||
nil)))
|
nil)))
|
||||||
|
|
||||||
|
|
||||||
;; --- Mutation: Delete Profile
|
;; --- MUTATION: Delete Profile
|
||||||
|
|
||||||
(declare check-can-delete-profile!)
|
(declare check-can-delete-profile!)
|
||||||
(declare mark-profile-as-deleted!)
|
(declare mark-profile-as-deleted!)
|
||||||
|
|
|
@ -73,7 +73,8 @@
|
||||||
(defn decode-profile-row
|
(defn decode-profile-row
|
||||||
[{:keys [props] :as row}]
|
[{:keys [props] :as row}]
|
||||||
(cond-> row
|
(cond-> row
|
||||||
(db/pgobject? props) (assoc :props (db/decode-transit-pgobject props))))
|
(db/pgobject? props "jsonb")
|
||||||
|
(assoc :props (db/decode-transit-pgobject props))))
|
||||||
|
|
||||||
(defn retrieve-profile-data
|
(defn retrieve-profile-data
|
||||||
[conn id]
|
[conn id]
|
||||||
|
|
|
@ -168,126 +168,95 @@
|
||||||
(t/testing "not allowed email domain"
|
(t/testing "not allowed email domain"
|
||||||
(t/is (false? (profile/email-domain-in-whitelist? whitelist "username@somedomain.com"))))))
|
(t/is (false? (profile/email-domain-in-whitelist? whitelist "username@somedomain.com"))))))
|
||||||
|
|
||||||
(t/deftest test-register-with-no-terms-and-privacy
|
(t/deftest prepare-register-and-register-profile
|
||||||
(let [data {::th/type :register-profile
|
(let [data {::th/type :prepare-register-profile
|
||||||
:email "user@example.com"
|
:email "user@example.com"
|
||||||
:password "foobar"
|
:password "foobar"}
|
||||||
:fullname "foobar"
|
|
||||||
:terms-privacy nil}
|
|
||||||
out (th/mutation! data)
|
out (th/mutation! data)
|
||||||
error (:error out)
|
token (get-in out [:result :token])]
|
||||||
edata (ex-data error)]
|
(t/is (string? token))
|
||||||
(t/is (th/ex-info? error))
|
|
||||||
(t/is (= (:type edata) :validation))
|
|
||||||
(t/is (= (:code edata) :spec-validation))))
|
|
||||||
|
|
||||||
(t/deftest test-register-with-bad-terms-and-privacy
|
|
||||||
(let [data {::th/type :register-profile
|
|
||||||
:email "user@example.com"
|
|
||||||
:password "foobar"
|
|
||||||
:fullname "foobar"
|
|
||||||
:terms-privacy false}
|
|
||||||
out (th/mutation! data)
|
|
||||||
error (:error out)
|
|
||||||
edata (ex-data error)]
|
|
||||||
(t/is (th/ex-info? error))
|
|
||||||
(t/is (= (:type edata) :validation))
|
|
||||||
(t/is (= (:code edata) :invalid-terms-and-privacy))))
|
|
||||||
|
|
||||||
(t/deftest test-register-when-registration-disabled
|
;; try register without accepting terms
|
||||||
|
(let [data {::th/type :register-profile
|
||||||
|
:token token
|
||||||
|
:fullname "foobar"
|
||||||
|
:accept-terms-and-privacy false}
|
||||||
|
out (th/mutation! data)]
|
||||||
|
(let [error (:error out)]
|
||||||
|
(t/is (th/ex-info? error))
|
||||||
|
(t/is (th/ex-of-type? error :validation))
|
||||||
|
(t/is (th/ex-of-code? error :invalid-terms-and-privacy))))
|
||||||
|
|
||||||
|
;; try register without token
|
||||||
|
(let [data {::th/type :register-profile
|
||||||
|
:fullname "foobar"
|
||||||
|
:accept-terms-and-privacy true}
|
||||||
|
out (th/mutation! data)]
|
||||||
|
(let [error (:error out)]
|
||||||
|
(t/is (th/ex-info? error))
|
||||||
|
(t/is (th/ex-of-type? error :validation))
|
||||||
|
(t/is (th/ex-of-code? error :spec-validation))))
|
||||||
|
|
||||||
|
;; try correct register
|
||||||
|
(let [data {::th/type :register-profile
|
||||||
|
:token token
|
||||||
|
:fullname "foobar"
|
||||||
|
:accept-terms-and-privacy true
|
||||||
|
:accept-newsletter-subscription true}]
|
||||||
|
(let [{:keys [result error]} (th/mutation! data)]
|
||||||
|
(t/is (nil? error))
|
||||||
|
(t/is (true? (get-in result [:props :accept-newsletter-subscription])))
|
||||||
|
(t/is (true? (get-in result [:props :accept-terms-and-privacy])))))
|
||||||
|
))
|
||||||
|
|
||||||
|
(t/deftest prepare-register-with-registration-disabled
|
||||||
(with-mocks [mock {:target 'app.config/get
|
(with-mocks [mock {:target 'app.config/get
|
||||||
:return (th/mock-config-get-with
|
:return (th/mock-config-get-with
|
||||||
{:registration-enabled false})}]
|
{:registration-enabled false})}]
|
||||||
(let [data {::th/type :register-profile
|
|
||||||
:email "user@example.com"
|
|
||||||
:password "foobar"
|
|
||||||
:fullname "foobar"
|
|
||||||
:terms-privacy true}
|
|
||||||
out (th/mutation! data)
|
|
||||||
error (:error out)
|
|
||||||
edata (ex-data error)]
|
|
||||||
(t/is (th/ex-info? error))
|
|
||||||
(t/is (= (:type edata) :restriction))
|
|
||||||
(t/is (= (:code edata) :registration-disabled)))))
|
|
||||||
|
|
||||||
(t/deftest test-register-existing-profile
|
(let [data {::th/type :prepare-register-profile
|
||||||
|
:email "user@example.com"
|
||||||
|
:password "foobar"}]
|
||||||
|
(let [{:keys [result error] :as out} (th/mutation! data)]
|
||||||
|
(t/is (th/ex-info? error))
|
||||||
|
(t/is (th/ex-of-type? error :restriction))
|
||||||
|
(t/is (th/ex-of-code? error :registration-disabled))))))
|
||||||
|
|
||||||
|
(t/deftest prepare-register-with-existing-user
|
||||||
(let [profile (th/create-profile* 1)
|
(let [profile (th/create-profile* 1)
|
||||||
data {::th/type :register-profile
|
data {::th/type :prepare-register-profile
|
||||||
:email (:email profile)
|
:email (:email profile)
|
||||||
:password "foobar"
|
:password "foobar"}]
|
||||||
:fullname "foobar"
|
(let [{:keys [result error] :as out} (th/mutation! data)]
|
||||||
:terms-privacy true}
|
|
||||||
out (th/mutation! data)
|
|
||||||
error (:error out)
|
|
||||||
edata (ex-data error)]
|
|
||||||
(t/is (th/ex-info? error))
|
|
||||||
(t/is (= (:type edata) :validation))
|
|
||||||
(t/is (= (:code edata) :email-already-exists))))
|
|
||||||
|
|
||||||
(t/deftest test-register-profile
|
|
||||||
(with-mocks [mock {:target 'app.emails/send!
|
|
||||||
:return nil}]
|
|
||||||
(let [pool (:app.db/pool th/*system*)
|
|
||||||
data {::th/type :register-profile
|
|
||||||
:email "user@example.com"
|
|
||||||
:password "foobar"
|
|
||||||
:fullname "foobar"
|
|
||||||
:terms-privacy true}
|
|
||||||
out (th/mutation! data)]
|
|
||||||
;; (th/print-result! out)
|
;; (th/print-result! out)
|
||||||
(let [mock (deref mock)
|
(t/is (th/ex-info? error))
|
||||||
[params] (:call-args mock)]
|
(t/is (th/ex-of-type? error :validation))
|
||||||
;; (clojure.pprint/pprint params)
|
(t/is (th/ex-of-code? error :email-already-exists)))))
|
||||||
(t/is (:called? mock))
|
|
||||||
(t/is (= (:email data) (:to params)))
|
|
||||||
(t/is (contains? params :extra-data))
|
|
||||||
(t/is (contains? params :token)))
|
|
||||||
|
|
||||||
(let [result (:result out)]
|
|
||||||
(t/is (false? (:is-demo result)))
|
|
||||||
(t/is (= (:email data) (:email result)))
|
|
||||||
(t/is (= "penpot" (:auth-backend result)))
|
|
||||||
(t/is (= "foobar" (:fullname result)))
|
|
||||||
(t/is (not (contains? result :password)))))))
|
|
||||||
|
|
||||||
(t/deftest test-register-profile-with-bounced-email
|
(t/deftest test-register-profile-with-bounced-email
|
||||||
(with-mocks [mock {:target 'app.emails/send!
|
(let [pool (:app.db/pool th/*system*)
|
||||||
:return nil}]
|
data {::th/type :prepare-register-profile
|
||||||
(let [pool (:app.db/pool th/*system*)
|
:email "user@example.com"
|
||||||
data {::th/type :register-profile
|
:password "foobar"}]
|
||||||
:email "user@example.com"
|
|
||||||
:password "foobar"
|
|
||||||
:fullname "foobar"
|
|
||||||
:terms-privacy true}
|
|
||||||
_ (th/create-global-complaint-for pool {:type :bounce :email "user@example.com"})
|
|
||||||
out (th/mutation! data)]
|
|
||||||
;; (th/print-result! out)
|
|
||||||
|
|
||||||
(let [mock (deref mock)]
|
(th/create-global-complaint-for pool {:type :bounce :email "user@example.com"})
|
||||||
(t/is (false? (:called? mock))))
|
|
||||||
|
|
||||||
(let [error (:error out)
|
(let [{:keys [result error] :as out} (th/mutation! data)]
|
||||||
edata (ex-data error)]
|
(t/is (th/ex-info? error))
|
||||||
(t/is (th/ex-info? error))
|
(t/is (th/ex-of-type? error :validation))
|
||||||
(t/is (= (:type edata) :validation))
|
(t/is (th/ex-of-code? error :email-has-permanent-bounces)))))
|
||||||
(t/is (= (:code edata) :email-has-permanent-bounces))))))
|
|
||||||
|
|
||||||
(t/deftest test-register-profile-with-complained-email
|
(t/deftest test-register-profile-with-complained-email
|
||||||
(with-mocks [mock {:target 'app.emails/send! :return nil}]
|
(let [pool (:app.db/pool th/*system*)
|
||||||
(let [pool (:app.db/pool th/*system*)
|
data {::th/type :prepare-register-profile
|
||||||
data {::th/type :register-profile
|
:email "user@example.com"
|
||||||
:email "user@example.com"
|
:password "foobar"}]
|
||||||
:password "foobar"
|
|
||||||
:fullname "foobar"
|
|
||||||
:terms-privacy true}
|
|
||||||
_ (th/create-global-complaint-for pool {:type :complaint :email "user@example.com"})
|
|
||||||
out (th/mutation! data)]
|
|
||||||
|
|
||||||
(let [mock (deref mock)]
|
(th/create-global-complaint-for pool {:type :complaint :email "user@example.com"})
|
||||||
(t/is (true? (:called? mock))))
|
(let [{:keys [result error] :as out} (th/mutation! data)]
|
||||||
|
(t/is (nil? error))
|
||||||
(let [result (:result out)]
|
(t/is (string? (:token result))))))
|
||||||
(t/is (= (:email data) (:email result)))))))
|
|
||||||
|
|
||||||
(t/deftest test-email-change-request
|
(t/deftest test-email-change-request
|
||||||
(with-mocks [email-send-mock {:target 'app.emails/send! :return nil}
|
(with-mocks [email-send-mock {:target 'app.emails/send! :return nil}
|
||||||
|
|
|
@ -10,3 +10,4 @@
|
||||||
//var penpotLoginWithLDAP = <true|false>;
|
//var penpotLoginWithLDAP = <true|false>;
|
||||||
//var penpotRegistrationEnabled = <true|false>;
|
//var penpotRegistrationEnabled = <true|false>;
|
||||||
//var penpotAnalyticsEnabled = <true|false>;
|
//var penpotAnalyticsEnabled = <true|false>;
|
||||||
|
//var penpotFlags = "";
|
||||||
|
|
|
@ -105,6 +105,14 @@ update_analytics_enabled() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
update_flags() {
|
||||||
|
if [ -n "$PENPOT_FLAGS" ]; then
|
||||||
|
sed -i \
|
||||||
|
-e "s|^//var penpotFlags = .*;|var penpotFlags = \"$PENPOT_FLAGS\";|g" \
|
||||||
|
"$1"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
update_public_uri /var/www/app/js/config.js
|
update_public_uri /var/www/app/js/config.js
|
||||||
update_demo_warning /var/www/app/js/config.js
|
update_demo_warning /var/www/app/js/config.js
|
||||||
update_allow_demo_users /var/www/app/js/config.js
|
update_allow_demo_users /var/www/app/js/config.js
|
||||||
|
@ -115,5 +123,5 @@ update_oidc_client_id /var/www/app/js/config.js
|
||||||
update_login_with_ldap /var/www/app/js/config.js
|
update_login_with_ldap /var/www/app/js/config.js
|
||||||
update_registration_enabled /var/www/app/js/config.js
|
update_registration_enabled /var/www/app/js/config.js
|
||||||
update_analytics_enabled /var/www/app/js/config.js
|
update_analytics_enabled /var/www/app/js/config.js
|
||||||
|
update_flags /var/www/app/js/config.js
|
||||||
exec "$@";
|
exec "$@";
|
||||||
|
|
|
@ -54,6 +54,20 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
input {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons-stack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
*:not(:last-child) {
|
||||||
|
margin-bottom: $medium;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.form-container {
|
.form-container {
|
||||||
width: 412px;
|
width: 412px;
|
||||||
|
|
||||||
|
@ -83,15 +97,47 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-github-auth {
|
.btn-github-auth {
|
||||||
margin-bottom: $medium;
|
margin-bottom: $medium;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.links {
|
||||||
|
display: flex;
|
||||||
|
font-size: $fs14;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: $medium;
|
||||||
|
margin-bottom: $medium;
|
||||||
|
|
||||||
|
|
||||||
|
&.demo {
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: $big;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-entry {
|
||||||
|
font-size: $fs14;
|
||||||
|
color: $color-gray-40;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
a {
|
||||||
|
font-size: $fs14;
|
||||||
|
color: $color-primary-dark;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.terms-login {
|
.terms-login {
|
||||||
|
|
|
@ -109,30 +109,6 @@ textarea {
|
||||||
hr {
|
hr {
|
||||||
border-color: $color-gray-20;
|
border-color: $color-gray-20;
|
||||||
}
|
}
|
||||||
|
|
||||||
.links {
|
|
||||||
display: flex;
|
|
||||||
font-size: $fs14;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: $medium;
|
|
||||||
|
|
||||||
&.demo {
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: $big;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-entry {
|
|
||||||
font-size: $fs14;
|
|
||||||
color: $color-gray-40;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-entry a {
|
|
||||||
font-size: $fs14;
|
|
||||||
color: $color-primary-dark;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-input {
|
.custom-input {
|
||||||
|
|
|
@ -54,6 +54,11 @@
|
||||||
:browser
|
:browser
|
||||||
:webworker))
|
:webworker))
|
||||||
|
|
||||||
|
(defn- parse-flags
|
||||||
|
[global]
|
||||||
|
(let [flags (obj/get global "penpotFlags" "")]
|
||||||
|
(into #{} (map keyword) (str/words flags))))
|
||||||
|
|
||||||
(defn- parse-version
|
(defn- parse-version
|
||||||
[global]
|
[global]
|
||||||
(-> (obj/get global "penpotVersion")
|
(-> (obj/get global "penpotVersion")
|
||||||
|
@ -78,6 +83,8 @@
|
||||||
(def themes (obj/get global "penpotThemes"))
|
(def themes (obj/get global "penpotThemes"))
|
||||||
(def analytics (obj/get global "penpotAnalyticsEnabled" false))
|
(def analytics (obj/get global "penpotAnalyticsEnabled" false))
|
||||||
|
|
||||||
|
(def flags (delay (parse-flags global)))
|
||||||
|
|
||||||
(def version (delay (parse-version global)))
|
(def version (delay (parse-version global)))
|
||||||
(def target (delay (parse-target global)))
|
(def target (delay (parse-target global)))
|
||||||
(def browser (delay (parse-browser)))
|
(def browser (delay (parse-browser)))
|
||||||
|
|
|
@ -221,6 +221,7 @@
|
||||||
|
|
||||||
;; --- EVENT: register
|
;; --- EVENT: register
|
||||||
|
|
||||||
|
;; TODO: remove
|
||||||
(s/def ::invitation-token ::us/not-empty-string)
|
(s/def ::invitation-token ::us/not-empty-string)
|
||||||
|
|
||||||
(s/def ::register
|
(s/def ::register
|
||||||
|
|
|
@ -62,6 +62,8 @@
|
||||||
["/login" :auth-login]
|
["/login" :auth-login]
|
||||||
(when cfg/registration-enabled
|
(when cfg/registration-enabled
|
||||||
["/register" :auth-register])
|
["/register" :auth-register])
|
||||||
|
(when cfg/registration-enabled
|
||||||
|
["/register/validate" :auth-register-validate])
|
||||||
(when cfg/registration-enabled
|
(when cfg/registration-enabled
|
||||||
["/register/success" :auth-register-success])
|
["/register/success" :auth-register-success])
|
||||||
["/recovery/request" :auth-recovery-request]
|
["/recovery/request" :auth-recovery-request]
|
||||||
|
@ -112,6 +114,7 @@
|
||||||
(case (:name data)
|
(case (:name data)
|
||||||
(:auth-login
|
(:auth-login
|
||||||
:auth-register
|
:auth-register
|
||||||
|
:auth-register-validate
|
||||||
:auth-register-success
|
:auth-register-success
|
||||||
:auth-recovery-request
|
:auth-recovery-request
|
||||||
:auth-recovery)
|
:auth-recovery)
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
[app.main.ui.auth.login :refer [login-page]]
|
[app.main.ui.auth.login :refer [login-page]]
|
||||||
[app.main.ui.auth.recovery :refer [recovery-page]]
|
[app.main.ui.auth.recovery :refer [recovery-page]]
|
||||||
[app.main.ui.auth.recovery-request :refer [recovery-request-page]]
|
[app.main.ui.auth.recovery-request :refer [recovery-request-page]]
|
||||||
[app.main.ui.auth.register :refer [register-page register-success-page]]
|
[app.main.ui.auth.register :refer [register-page register-success-page register-validate-page]]
|
||||||
[app.main.ui.icons :as i]
|
[app.main.ui.icons :as i]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.forms :as fm]
|
[app.util.forms :as fm]
|
||||||
|
@ -36,13 +36,16 @@
|
||||||
|
|
||||||
[:div.auth
|
[:div.auth
|
||||||
[:section.auth-sidebar
|
[:section.auth-sidebar
|
||||||
[:a.logo {:href "https://penpot.app"} i/logo]
|
[:a.logo {:href "#/"} i/logo]
|
||||||
[:span.tagline (t locale "auth.sidebar-tagline")]]
|
[:span.tagline (t locale "auth.sidebar-tagline")]]
|
||||||
|
|
||||||
[:section.auth-content
|
[:section.auth-content
|
||||||
(case section
|
(case section
|
||||||
:auth-register
|
:auth-register
|
||||||
[:& register-page {:locale locale :params params}]
|
[:& register-page {:params params}]
|
||||||
|
|
||||||
|
:auth-register-validate
|
||||||
|
[:& register-validate-page {:params params}]
|
||||||
|
|
||||||
:auth-register-success
|
:auth-register-success
|
||||||
[:& register-success-page {:params params}]
|
[:& register-success-page {:params params}]
|
||||||
|
@ -55,6 +58,7 @@
|
||||||
|
|
||||||
:auth-recovery
|
:auth-recovery
|
||||||
[:& recovery-page {:locale locale :params params}])
|
[:& recovery-page {:locale locale :params params}])
|
||||||
|
|
||||||
[:div.terms-login
|
[:div.terms-login
|
||||||
[:a {:href "https://penpot.app/terms.html" :target "_blank"} "Terms of service"]
|
[:a {:href "https://penpot.app/terms.html" :target "_blank"} "Terms of service"]
|
||||||
[:span "and"]
|
[:span "and"]
|
||||||
|
|
|
@ -23,6 +23,12 @@
|
||||||
[cljs.spec.alpha :as s]
|
[cljs.spec.alpha :as s]
|
||||||
[rumext.alpha :as mf]))
|
[rumext.alpha :as mf]))
|
||||||
|
|
||||||
|
(def show-alt-login-buttons?
|
||||||
|
(or cfg/google-client-id
|
||||||
|
cfg/gitlab-client-id
|
||||||
|
cfg/github-client-id
|
||||||
|
cfg/oidc-client-id))
|
||||||
|
|
||||||
(s/def ::email ::us/email)
|
(s/def ::email ::us/email)
|
||||||
(s/def ::password ::us/not-empty-string)
|
(s/def ::password ::us/not-empty-string)
|
||||||
|
|
||||||
|
@ -103,13 +109,15 @@
|
||||||
:tab-index "3"
|
:tab-index "3"
|
||||||
:help-icon i/eye
|
:help-icon i/eye
|
||||||
:label (tr "auth.password")}]]
|
:label (tr "auth.password")}]]
|
||||||
[:& fm/submit-button
|
|
||||||
{:label (tr "auth.login-submit")}]
|
|
||||||
|
|
||||||
(when cfg/login-with-ldap
|
[:div.buttons-stack
|
||||||
[:& fm/submit-button
|
[:& fm/submit-button
|
||||||
{:label (tr "auth.login-with-ldap-submit")
|
{:label (tr "auth.login-submit")}]
|
||||||
:on-click on-submit-ldap}])]]))
|
|
||||||
|
(when cfg/login-with-ldap
|
||||||
|
[:& fm/submit-button
|
||||||
|
{:label (tr "auth.login-with-ldap-submit")
|
||||||
|
:on-click on-submit-ldap}])]]]))
|
||||||
|
|
||||||
(mf/defc login-buttons
|
(mf/defc login-buttons
|
||||||
[{:keys [params] :as props}]
|
[{:keys [params] :as props}]
|
||||||
|
@ -147,6 +155,13 @@
|
||||||
|
|
||||||
[:& login-form {:params params}]
|
[:& login-form {:params params}]
|
||||||
|
|
||||||
|
(when show-alt-login-buttons?
|
||||||
|
[:*
|
||||||
|
[:span.separator (tr "labels.or")]
|
||||||
|
|
||||||
|
[:div.buttons
|
||||||
|
[:& login-buttons {:params params}]]])
|
||||||
|
|
||||||
[:div.links
|
[:div.links
|
||||||
[:div.link-entry
|
[:div.link-entry
|
||||||
[:a {:on-click #(st/emit! (rt/nav :auth-recovery-request))}
|
[:a {:on-click #(st/emit! (rt/nav :auth-recovery-request))}
|
||||||
|
@ -158,7 +173,6 @@
|
||||||
[:a {:on-click #(st/emit! (rt/nav :auth-register {} params))}
|
[:a {:on-click #(st/emit! (rt/nav :auth-register {} params))}
|
||||||
(tr "auth.register-submit")]])]
|
(tr "auth.register-submit")]])]
|
||||||
|
|
||||||
[:& login-buttons {:params params}]
|
|
||||||
|
|
||||||
(when cfg/allow-demo-users
|
(when cfg/allow-demo-users
|
||||||
[:div.links.demo
|
[:div.links.demo
|
||||||
|
|
|
@ -86,4 +86,4 @@
|
||||||
[:div.links
|
[:div.links
|
||||||
[:div.link-entry
|
[:div.link-entry
|
||||||
[:a {:on-click #(st/emit! (rt/nav :auth-login))}
|
[:a {:on-click #(st/emit! (rt/nav :auth-login))}
|
||||||
(tr "auth.go-back-to-login")]]]]])
|
(tr "labels.go-back")]]]]])
|
||||||
|
|
|
@ -7,10 +7,11 @@
|
||||||
(ns app.main.ui.auth.register
|
(ns app.main.ui.auth.register
|
||||||
(:require
|
(:require
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.config :as cfg]
|
[app.config :as cf]
|
||||||
[app.main.data.users :as du]
|
[app.main.data.users :as du]
|
||||||
[app.main.data.messages :as dm]
|
[app.main.data.messages :as dm]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
|
[app.main.repo :as rp]
|
||||||
[app.main.ui.components.forms :as fm]
|
[app.main.ui.components.forms :as fm]
|
||||||
[app.main.ui.icons :as i]
|
[app.main.ui.icons :as i]
|
||||||
[app.main.ui.messages :as msgs]
|
[app.main.ui.messages :as msgs]
|
||||||
|
@ -30,6 +31,8 @@
|
||||||
{:type :warning
|
{:type :warning
|
||||||
:content (tr "auth.demo-warning")}])
|
:content (tr "auth.demo-warning")}])
|
||||||
|
|
||||||
|
;; --- PAGE: Register
|
||||||
|
|
||||||
(defn- validate
|
(defn- validate
|
||||||
[data]
|
[data]
|
||||||
(let [password (:password data)
|
(let [password (:password data)
|
||||||
|
@ -48,9 +51,29 @@
|
||||||
(s/def ::terms-privacy ::us/boolean)
|
(s/def ::terms-privacy ::us/boolean)
|
||||||
|
|
||||||
(s/def ::register-form
|
(s/def ::register-form
|
||||||
(s/keys :req-un [::password ::fullname ::email ::terms-privacy]
|
(s/keys :req-un [::password ::email]
|
||||||
:opt-un [::invitation-token]))
|
:opt-un [::invitation-token]))
|
||||||
|
|
||||||
|
(defn- handle-prepare-register-error
|
||||||
|
[form error]
|
||||||
|
(case (:code error)
|
||||||
|
:registration-disabled
|
||||||
|
(st/emit! (dm/error (tr "errors.registration-disabled")))
|
||||||
|
|
||||||
|
:email-has-permanent-bounces
|
||||||
|
(let [email (get @form [:data :email])]
|
||||||
|
(st/emit! (dm/error (tr "errors.email-has-permanent-bounces" email))))
|
||||||
|
|
||||||
|
:email-already-exists
|
||||||
|
(swap! form assoc-in [:errors :email]
|
||||||
|
{:message "errors.email-already-exists"})
|
||||||
|
|
||||||
|
(st/emit! (dm/error (tr "errors.generic")))))
|
||||||
|
|
||||||
|
(defn- handle-prepare-register-success
|
||||||
|
[form {:keys [token] :as result}]
|
||||||
|
(st/emit! (rt/nav :auth-register-validate {} {:token token})))
|
||||||
|
|
||||||
(mf/defc register-form
|
(mf/defc register-form
|
||||||
[{:keys [params] :as props}]
|
[{:keys [params] :as props}]
|
||||||
(let [initial (mf/use-memo (mf/deps params) (constantly params))
|
(let [initial (mf/use-memo (mf/deps params) (constantly params))
|
||||||
|
@ -59,49 +82,20 @@
|
||||||
:initial initial)
|
:initial initial)
|
||||||
submitted? (mf/use-state false)
|
submitted? (mf/use-state false)
|
||||||
|
|
||||||
on-error
|
|
||||||
(mf/use-callback
|
|
||||||
(fn [form error]
|
|
||||||
(reset! submitted? false)
|
|
||||||
(case (:code error)
|
|
||||||
:registration-disabled
|
|
||||||
(rx/of (dm/error (tr "errors.registration-disabled")))
|
|
||||||
|
|
||||||
:email-has-permanent-bounces
|
|
||||||
(let [email (get @form [:data :email])]
|
|
||||||
(rx/of (dm/error (tr "errors.email-has-permanent-bounces" email))))
|
|
||||||
|
|
||||||
:email-already-exists
|
|
||||||
(swap! form assoc-in [:errors :email]
|
|
||||||
{:message "errors.email-already-exists"})
|
|
||||||
|
|
||||||
(rx/throw error))))
|
|
||||||
|
|
||||||
on-success
|
|
||||||
(mf/use-callback
|
|
||||||
(fn [form data]
|
|
||||||
(reset! submitted? false)
|
|
||||||
(if-let [token (:invitation-token data)]
|
|
||||||
(st/emit! (rt/nav :auth-verify-token {} {:token token}))
|
|
||||||
(st/emit! (rt/nav :auth-register-success {} {:email (:email data)})))))
|
|
||||||
|
|
||||||
on-submit
|
on-submit
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(fn [form event]
|
(fn [form event]
|
||||||
(reset! submitted? true)
|
(reset! submitted? true)
|
||||||
(let [data (with-meta (:clean-data @form)
|
(let [params (:clean-data @form)]
|
||||||
{:on-error (partial on-error form)
|
(->> (rp/mutation :prepare-register-profile params)
|
||||||
:on-success (partial on-success form)})]
|
(rx/finalize #(reset! submitted? false))
|
||||||
(st/emit! (du/register data)))))]
|
(rx/subs (partial handle-prepare-register-success form)
|
||||||
|
(partial handle-prepare-register-error form))))))
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
[:& fm/form {:on-submit on-submit
|
[:& fm/form {:on-submit on-submit
|
||||||
:form form}
|
:form form}
|
||||||
[:div.fields-row
|
|
||||||
[:& fm/input {:name :fullname
|
|
||||||
:tab-index "1"
|
|
||||||
:label (tr "auth.fullname")
|
|
||||||
:type "text"}]]
|
|
||||||
[:div.fields-row
|
[:div.fields-row
|
||||||
[:& fm/input {:type "email"
|
[:& fm/input {:type "email"
|
||||||
:name :email
|
:name :email
|
||||||
|
@ -115,18 +109,145 @@
|
||||||
:label (tr "auth.password")
|
:label (tr "auth.password")
|
||||||
:type "password"}]]
|
:type "password"}]]
|
||||||
|
|
||||||
|
[:& fm/submit-button
|
||||||
|
{:label (tr "auth.register-submit")
|
||||||
|
:disabled @submitted?}]]))
|
||||||
|
|
||||||
|
(mf/defc register-page
|
||||||
|
[{:keys [params] :as props}]
|
||||||
|
[:div.form-container
|
||||||
|
[:h1 (tr "auth.register-title")]
|
||||||
|
[:div.subtitle (tr "auth.register-subtitle")]
|
||||||
|
|
||||||
|
(when cf/demo-warning
|
||||||
|
[:& demo-warning])
|
||||||
|
|
||||||
|
[:& register-form {:params params}]
|
||||||
|
|
||||||
|
(when login/show-alt-login-buttons?
|
||||||
|
[:*
|
||||||
|
[:span.separator (tr "labels.or")]
|
||||||
|
|
||||||
|
[:div.buttons
|
||||||
|
[:& login/login-buttons {:params params}]]])
|
||||||
|
|
||||||
|
[:div.links
|
||||||
|
[:div.link-entry
|
||||||
|
[:span (tr "auth.already-have-account") " "]
|
||||||
|
[:a {:on-click #(st/emit! (rt/nav :auth-login {} params))
|
||||||
|
:tab-index "4"}
|
||||||
|
(tr "auth.login-here")]]
|
||||||
|
|
||||||
|
(when cf/allow-demo-users
|
||||||
|
[:div.link-entry
|
||||||
|
[:span (tr "auth.create-demo-profile") " "]
|
||||||
|
[:a {:on-click #(st/emit! (du/create-demo-profile))
|
||||||
|
:tab-index "5"}
|
||||||
|
(tr "auth.create-demo-account")]])]])
|
||||||
|
|
||||||
|
;; --- PAGE: register validation
|
||||||
|
|
||||||
|
(defn- handle-register-error
|
||||||
|
[form error]
|
||||||
|
(case (:code error)
|
||||||
|
:registration-disabled
|
||||||
|
(st/emit! (dm/error (tr "errors.registration-disabled")))
|
||||||
|
|
||||||
|
:email-has-permanent-bounces
|
||||||
|
(let [email (get @form [:data :email])]
|
||||||
|
(st/emit! (dm/error (tr "errors.email-has-permanent-bounces" email))))
|
||||||
|
|
||||||
|
:email-already-exists
|
||||||
|
(swap! form assoc-in [:errors :email]
|
||||||
|
{:message "errors.email-already-exists"})
|
||||||
|
|
||||||
|
(do
|
||||||
|
(println (:explain error))
|
||||||
|
(st/emit! (dm/error (tr "errors.generic"))))))
|
||||||
|
|
||||||
|
(defn- handle-register-success
|
||||||
|
[form data]
|
||||||
|
(cond
|
||||||
|
(some? (:invitation-token data))
|
||||||
|
(let [token (:invitation-token data)]
|
||||||
|
(st/emit! (rt/nav :auth-verify-token {} {:token token})))
|
||||||
|
|
||||||
|
|
||||||
|
(not= "penpot" (:auth-backend data))
|
||||||
|
(st/emit!
|
||||||
|
(du/fetch-profile)
|
||||||
|
(rt/nav :dashboard-projects {:team-id (:default-team-id data)}))
|
||||||
|
|
||||||
|
:else
|
||||||
|
(st/emit! (rt/nav :auth-register-success {} {:email (:email data)}))))
|
||||||
|
|
||||||
|
(s/def ::accept-terms-and-privacy ::us/boolean)
|
||||||
|
(s/def ::accept-newsletter-subscription ::us/boolean)
|
||||||
|
|
||||||
|
(s/def ::register-validate-form
|
||||||
|
(s/keys :req-un [::token ::fullname ::accept-terms-and-privacy]
|
||||||
|
:opt-un [::accept-newsletter-subscription]))
|
||||||
|
|
||||||
|
(mf/defc register-validate-form
|
||||||
|
[{:keys [params] :as props}]
|
||||||
|
(let [initial (mf/use-memo
|
||||||
|
(mf/deps params)
|
||||||
|
(fn []
|
||||||
|
(assoc params :accept-newsletter-subscription false)))
|
||||||
|
form (fm/use-form :spec ::register-validate-form
|
||||||
|
:initial initial)
|
||||||
|
submitted? (mf/use-state false)
|
||||||
|
|
||||||
|
on-submit
|
||||||
|
(mf/use-callback
|
||||||
|
(fn [form event]
|
||||||
|
(reset! submitted? true)
|
||||||
|
(let [params (:clean-data @form)]
|
||||||
|
(->> (rp/mutation :register-profile params)
|
||||||
|
(rx/finalize #(reset! submitted? false))
|
||||||
|
(rx/subs (partial handle-register-success form)
|
||||||
|
(partial handle-register-error form))))))
|
||||||
|
]
|
||||||
|
|
||||||
|
[:& fm/form {:on-submit on-submit
|
||||||
|
:form form}
|
||||||
[:div.fields-row
|
[:div.fields-row
|
||||||
[:& fm/input {:name :terms-privacy
|
[:& fm/input {:name :fullname
|
||||||
|
:tab-index "1"
|
||||||
|
:label (tr "auth.fullname")
|
||||||
|
:type "text"}]]
|
||||||
|
[:div.fields-row
|
||||||
|
[:& fm/input {:name :accept-terms-and-privacy
|
||||||
:class "check-primary"
|
:class "check-primary"
|
||||||
:tab-index "4"
|
|
||||||
:label (tr "auth.terms-privacy-agreement")
|
:label (tr "auth.terms-privacy-agreement")
|
||||||
:type "checkbox"}]]
|
:type "checkbox"}]]
|
||||||
|
|
||||||
|
(when (contains? @cf/flags :show-newsletter-check-on-register-validation)
|
||||||
|
[:div.fields-row
|
||||||
|
[:& fm/input {:name :accept-newsletter-subscription
|
||||||
|
:class "check-primary"
|
||||||
|
:label (tr "auth.terms-privacy-agreement")
|
||||||
|
:type "checkbox"}]])
|
||||||
|
|
||||||
[:& fm/submit-button
|
[:& fm/submit-button
|
||||||
{:label (tr "auth.register-submit")
|
{:label (tr "auth.register-submit")
|
||||||
:disabled @submitted?}]]))
|
:disabled @submitted?}]]))
|
||||||
|
|
||||||
;; --- Register Page
|
|
||||||
|
(mf/defc register-validate-page
|
||||||
|
[{:keys [params] :as props}]
|
||||||
|
(prn "register-validate-page" params)
|
||||||
|
[:div.form-container
|
||||||
|
[:h1 (tr "auth.register-title")]
|
||||||
|
[:div.subtitle (tr "auth.register-subtitle")]
|
||||||
|
|
||||||
|
[:& register-validate-form {:params params}]
|
||||||
|
|
||||||
|
[:div.links
|
||||||
|
[:div.link-entry
|
||||||
|
[:a {:on-click #(st/emit! (rt/nav :auth-register {} {}))
|
||||||
|
:tab-index "4"}
|
||||||
|
(tr "labels.go-back")]]]])
|
||||||
|
|
||||||
(mf/defc register-success-page
|
(mf/defc register-success-page
|
||||||
[{:keys [params] :as props}]
|
[{:keys [params] :as props}]
|
||||||
|
@ -136,32 +257,3 @@
|
||||||
[:div.notification-text-email (:email params "")]
|
[:div.notification-text-email (:email params "")]
|
||||||
[:div.notification-text (tr "auth.check-your-email")]])
|
[:div.notification-text (tr "auth.check-your-email")]])
|
||||||
|
|
||||||
(mf/defc register-page
|
|
||||||
[{:keys [params] :as props}]
|
|
||||||
[:div.form-container
|
|
||||||
[:h1 (tr "auth.register-title")]
|
|
||||||
[:div.subtitle (tr "auth.register-subtitle")]
|
|
||||||
|
|
||||||
(when cfg/demo-warning
|
|
||||||
[:& demo-warning])
|
|
||||||
|
|
||||||
[:& register-form {:params params}]
|
|
||||||
|
|
||||||
[:div.links
|
|
||||||
[:div.link-entry
|
|
||||||
[:span (tr "auth.already-have-account") " "]
|
|
||||||
[:a {:on-click #(st/emit! (rt/nav :auth-login {} params))
|
|
||||||
:tab-index "4"}
|
|
||||||
(tr "auth.login-here")]]
|
|
||||||
|
|
||||||
(when cfg/allow-demo-users
|
|
||||||
[:div.link-entry
|
|
||||||
[:span (tr "auth.create-demo-profile") " "]
|
|
||||||
[:a {:on-click #(st/emit! (du/create-demo-profile))
|
|
||||||
:tab-index "5"}
|
|
||||||
(tr "auth.create-demo-account")]])
|
|
||||||
|
|
||||||
[:& login/login-buttons {:params params}]]])
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -48,10 +48,6 @@ msgstr "هل نسيت كلمة السر؟"
|
||||||
msgid "auth.fullname"
|
msgid "auth.fullname"
|
||||||
msgstr "الاسم بالكامل"
|
msgstr "الاسم بالكامل"
|
||||||
|
|
||||||
#: src/app/main/ui/auth/recovery_request.cljs
|
|
||||||
msgid "auth.go-back-to-login"
|
|
||||||
msgstr "الرجوع للخلف!"
|
|
||||||
|
|
||||||
#: src/app/main/ui/auth/register.cljs
|
#: src/app/main/ui/auth/register.cljs
|
||||||
msgid "auth.login-here"
|
msgid "auth.login-here"
|
||||||
msgstr "تسجيل الدخول هنا"
|
msgstr "تسجيل الدخول هنا"
|
||||||
|
@ -190,6 +186,9 @@ msgstr "نمط"
|
||||||
msgid "labels.fonts"
|
msgid "labels.fonts"
|
||||||
msgstr "الخطوط"
|
msgstr "الخطوط"
|
||||||
|
|
||||||
|
msgid "labels.go-back"
|
||||||
|
msgstr "الرجوع للخلف"
|
||||||
|
|
||||||
msgid "labels.images"
|
msgid "labels.images"
|
||||||
msgstr "الصور"
|
msgstr "الصور"
|
||||||
|
|
||||||
|
|
|
@ -46,10 +46,6 @@ msgstr "Has oblidat la contrasenya?"
|
||||||
msgid "auth.fullname"
|
msgid "auth.fullname"
|
||||||
msgstr "Nom complet"
|
msgstr "Nom complet"
|
||||||
|
|
||||||
#: src/app/main/ui/auth/recovery_request.cljs
|
|
||||||
msgid "auth.go-back-to-login"
|
|
||||||
msgstr "Tornar"
|
|
||||||
|
|
||||||
#: src/app/main/ui/auth/register.cljs
|
#: src/app/main/ui/auth/register.cljs
|
||||||
msgid "auth.login-here"
|
msgid "auth.login-here"
|
||||||
msgstr "Inicia sessió aquí"
|
msgstr "Inicia sessió aquí"
|
||||||
|
@ -467,6 +463,9 @@ msgstr "S'ha produït un error"
|
||||||
msgid "labels.accept"
|
msgid "labels.accept"
|
||||||
msgstr "Acceptar"
|
msgstr "Acceptar"
|
||||||
|
|
||||||
|
msgid "labels.go-back"
|
||||||
|
msgstr "Tornar"
|
||||||
|
|
||||||
msgid "labels.recent"
|
msgid "labels.recent"
|
||||||
msgstr "Recent"
|
msgstr "Recent"
|
||||||
|
|
||||||
|
|
|
@ -51,10 +51,6 @@ msgstr "Glemt adgangskode?"
|
||||||
msgid "auth.fullname"
|
msgid "auth.fullname"
|
||||||
msgstr "Fulde Navn"
|
msgstr "Fulde Navn"
|
||||||
|
|
||||||
#: src/app/main/ui/auth/recovery_request.cljs
|
|
||||||
msgid "auth.go-back-to-login"
|
|
||||||
msgstr "Gå tilbage!"
|
|
||||||
|
|
||||||
#: src/app/main/ui/auth/register.cljs
|
#: src/app/main/ui/auth/register.cljs
|
||||||
msgid "auth.login-here"
|
msgid "auth.login-here"
|
||||||
msgstr "Log på her"
|
msgstr "Log på her"
|
||||||
|
@ -452,6 +448,9 @@ msgstr "Stil"
|
||||||
msgid "labels.fonts"
|
msgid "labels.fonts"
|
||||||
msgstr "Skrifttyper"
|
msgstr "Skrifttyper"
|
||||||
|
|
||||||
|
msgid "labels.go-back"
|
||||||
|
msgstr "Gå tilbage!"
|
||||||
|
|
||||||
msgid "labels.installed-fonts"
|
msgid "labels.installed-fonts"
|
||||||
msgstr "Installeret skrifttyper"
|
msgstr "Installeret skrifttyper"
|
||||||
|
|
||||||
|
|
|
@ -51,10 +51,6 @@ msgstr "Passwort vergessen?"
|
||||||
msgid "auth.fullname"
|
msgid "auth.fullname"
|
||||||
msgstr "Vollständiger Name"
|
msgstr "Vollständiger Name"
|
||||||
|
|
||||||
#: src/app/main/ui/auth/recovery_request.cljs
|
|
||||||
msgid "auth.go-back-to-login"
|
|
||||||
msgstr "Zurück!"
|
|
||||||
|
|
||||||
#: src/app/main/ui/auth/register.cljs
|
#: src/app/main/ui/auth/register.cljs
|
||||||
msgid "auth.login-here"
|
msgid "auth.login-here"
|
||||||
msgstr "Hier einloggen"
|
msgstr "Hier einloggen"
|
||||||
|
@ -823,6 +819,9 @@ msgstr "Feedback gesendet"
|
||||||
msgid "labels.give-feedback"
|
msgid "labels.give-feedback"
|
||||||
msgstr "Feedback geben"
|
msgstr "Feedback geben"
|
||||||
|
|
||||||
|
msgid "labels.go-back"
|
||||||
|
msgstr "Zurück!"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs
|
#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs
|
||||||
msgid "labels.hide-resolved-comments"
|
msgid "labels.hide-resolved-comments"
|
||||||
msgstr "Erledigte Kommentare ausblenden"
|
msgstr "Erledigte Kommentare ausblenden"
|
||||||
|
@ -1306,6 +1305,10 @@ msgstr "Seite bearbeiten"
|
||||||
msgid "viewer.header.fullscreen"
|
msgid "viewer.header.fullscreen"
|
||||||
msgstr "Vollbildmodus"
|
msgstr "Vollbildmodus"
|
||||||
|
|
||||||
|
#: src/app/main/ui/viewer/header.cljs
|
||||||
|
msgid "viewer.header.interactions"
|
||||||
|
msgstr "Interaktionen"
|
||||||
|
|
||||||
#: src/app/main/ui/viewer/header.cljs
|
#: src/app/main/ui/viewer/header.cljs
|
||||||
msgid "viewer.header.share.copy-link"
|
msgid "viewer.header.share.copy-link"
|
||||||
msgstr "Link kopieren"
|
msgstr "Link kopieren"
|
||||||
|
@ -1330,10 +1333,6 @@ msgstr "Jeder mit dem Link hat Zugriff"
|
||||||
msgid "viewer.header.share.title"
|
msgid "viewer.header.share.title"
|
||||||
msgstr "Prototyp teilen"
|
msgstr "Prototyp teilen"
|
||||||
|
|
||||||
#: src/app/main/ui/viewer/header.cljs
|
|
||||||
msgid "viewer.header.interactions"
|
|
||||||
msgstr "Interaktionen"
|
|
||||||
|
|
||||||
#: src/app/main/ui/viewer/header.cljs
|
#: src/app/main/ui/viewer/header.cljs
|
||||||
msgid "viewer.header.show-interactions"
|
msgid "viewer.header.show-interactions"
|
||||||
msgstr "Interaktionen anzeigen"
|
msgstr "Interaktionen anzeigen"
|
||||||
|
|
|
@ -46,10 +46,6 @@ msgstr "Ξεχάσατε τον κωδικό;"
|
||||||
msgid "auth.fullname"
|
msgid "auth.fullname"
|
||||||
msgstr "Πλήρες όνομα"
|
msgstr "Πλήρες όνομα"
|
||||||
|
|
||||||
#: src/app/main/ui/auth/recovery_request.cljs
|
|
||||||
msgid "auth.go-back-to-login"
|
|
||||||
msgstr "Πίσω"
|
|
||||||
|
|
||||||
#: src/app/main/ui/auth/register.cljs
|
#: src/app/main/ui/auth/register.cljs
|
||||||
msgid "auth.login-here"
|
msgid "auth.login-here"
|
||||||
msgstr "Συνδεθείτε εδώ"
|
msgstr "Συνδεθείτε εδώ"
|
||||||
|
@ -823,6 +819,9 @@ msgstr "Εστάλη γνώμη"
|
||||||
msgid "labels.give-feedback"
|
msgid "labels.give-feedback"
|
||||||
msgstr "Δώστε μας τη γνώμη σας"
|
msgstr "Δώστε μας τη γνώμη σας"
|
||||||
|
|
||||||
|
msgid "labels.go-back"
|
||||||
|
msgstr "Πίσω"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs
|
#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs
|
||||||
msgid "labels.hide-resolved-comments"
|
msgid "labels.hide-resolved-comments"
|
||||||
msgstr "Απόκρυψη επιλυμένων σχολίων"
|
msgstr "Απόκρυψη επιλυμένων σχολίων"
|
||||||
|
|
|
@ -49,10 +49,6 @@ msgstr "Forgot password?"
|
||||||
msgid "auth.fullname"
|
msgid "auth.fullname"
|
||||||
msgstr "Full Name"
|
msgstr "Full Name"
|
||||||
|
|
||||||
#: src/app/main/ui/auth/recovery_request.cljs
|
|
||||||
msgid "auth.go-back-to-login"
|
|
||||||
msgstr "Go back!"
|
|
||||||
|
|
||||||
#: src/app/main/ui/auth/register.cljs
|
#: src/app/main/ui/auth/register.cljs
|
||||||
msgid "auth.login-here"
|
msgid "auth.login-here"
|
||||||
msgstr "Login here"
|
msgstr "Login here"
|
||||||
|
@ -206,6 +202,12 @@ msgstr "Duplicate %s files"
|
||||||
msgid "dashboard.empty-files"
|
msgid "dashboard.empty-files"
|
||||||
msgstr "You still have no files here"
|
msgstr "You still have no files here"
|
||||||
|
|
||||||
|
msgid "dashboard.export-multi"
|
||||||
|
msgstr "Export %s files"
|
||||||
|
|
||||||
|
msgid "dashboard.export-single"
|
||||||
|
msgstr "Export file"
|
||||||
|
|
||||||
msgid "dashboard.fonts.deleted-placeholder"
|
msgid "dashboard.fonts.deleted-placeholder"
|
||||||
msgstr "Font deleted"
|
msgstr "Font deleted"
|
||||||
|
|
||||||
|
@ -229,6 +231,9 @@ msgstr ""
|
||||||
"Service](https://penpot.app/terms.html). You also might want to read about "
|
"Service](https://penpot.app/terms.html). You also might want to read about "
|
||||||
"[font licensing](https://www.typography.com/faq)."
|
"[font licensing](https://www.typography.com/faq)."
|
||||||
|
|
||||||
|
msgid "dashboard.import"
|
||||||
|
msgstr "Import files"
|
||||||
|
|
||||||
#: src/app/main/ui/dashboard/team.cljs
|
#: src/app/main/ui/dashboard/team.cljs
|
||||||
msgid "dashboard.invite-profile"
|
msgid "dashboard.invite-profile"
|
||||||
msgstr "Invite to team"
|
msgstr "Invite to team"
|
||||||
|
@ -304,6 +309,9 @@ msgstr "%s members"
|
||||||
msgid "dashboard.open-in-new-tab"
|
msgid "dashboard.open-in-new-tab"
|
||||||
msgstr "Open file in a new tab"
|
msgstr "Open file in a new tab"
|
||||||
|
|
||||||
|
msgid "dashboard.options"
|
||||||
|
msgstr "Options"
|
||||||
|
|
||||||
#: src/app/main/ui/settings/password.cljs
|
#: src/app/main/ui/settings/password.cljs
|
||||||
msgid "dashboard.password-change"
|
msgid "dashboard.password-change"
|
||||||
msgstr "Change password"
|
msgstr "Change password"
|
||||||
|
@ -913,6 +921,9 @@ msgstr "Fonts"
|
||||||
msgid "labels.give-feedback"
|
msgid "labels.give-feedback"
|
||||||
msgstr "Give feedback"
|
msgstr "Give feedback"
|
||||||
|
|
||||||
|
msgid "labels.go-back"
|
||||||
|
msgstr "Go back"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs
|
#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs
|
||||||
msgid "labels.hide-resolved-comments"
|
msgid "labels.hide-resolved-comments"
|
||||||
msgstr "Hide resolved comments"
|
msgstr "Hide resolved comments"
|
||||||
|
@ -1000,6 +1011,9 @@ msgstr "Old password"
|
||||||
msgid "labels.only-yours"
|
msgid "labels.only-yours"
|
||||||
msgstr "Only yours"
|
msgstr "Only yours"
|
||||||
|
|
||||||
|
msgid "labels.or"
|
||||||
|
msgstr "or"
|
||||||
|
|
||||||
#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs
|
#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs
|
||||||
msgid "labels.owner"
|
msgid "labels.owner"
|
||||||
msgstr "Owner"
|
msgstr "Owner"
|
||||||
|
@ -1461,6 +1475,10 @@ msgstr "Edit file"
|
||||||
msgid "viewer.header.fullscreen"
|
msgid "viewer.header.fullscreen"
|
||||||
msgstr "Full Screen"
|
msgstr "Full Screen"
|
||||||
|
|
||||||
|
#: src/app/main/ui/viewer/header.cljs
|
||||||
|
msgid "viewer.header.interactions"
|
||||||
|
msgstr "Interactions"
|
||||||
|
|
||||||
#: src/app/main/ui/viewer/header.cljs
|
#: src/app/main/ui/viewer/header.cljs
|
||||||
msgid "viewer.header.share.copy-link"
|
msgid "viewer.header.share.copy-link"
|
||||||
msgstr "Copy link"
|
msgstr "Copy link"
|
||||||
|
@ -1485,10 +1503,6 @@ msgstr "Anyone with the link will have access"
|
||||||
msgid "viewer.header.share.title"
|
msgid "viewer.header.share.title"
|
||||||
msgstr "Share prototype"
|
msgstr "Share prototype"
|
||||||
|
|
||||||
#: src/app/main/ui/viewer/header.cljs
|
|
||||||
msgid "viewer.header.interactions"
|
|
||||||
msgstr "Interactions"
|
|
||||||
|
|
||||||
#: src/app/main/ui/viewer/header.cljs
|
#: src/app/main/ui/viewer/header.cljs
|
||||||
msgid "viewer.header.show-interactions"
|
msgid "viewer.header.show-interactions"
|
||||||
msgstr "Show interactions"
|
msgstr "Show interactions"
|
||||||
|
@ -1889,20 +1903,28 @@ msgid "workspace.options.constraints"
|
||||||
msgstr "Constraints"
|
msgstr "Constraints"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
||||||
msgid "workspace.options.constraints.left"
|
msgid "workspace.options.constraints.bottom"
|
||||||
msgstr "Left"
|
msgstr "Bottom"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
||||||
msgid "workspace.options.constraints.right"
|
msgid "workspace.options.constraints.center"
|
||||||
msgstr "Right"
|
msgstr "Center"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
||||||
|
msgid "workspace.options.constraints.fix-when-scrolling"
|
||||||
|
msgstr "Fix when scrolling"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
||||||
|
msgid "workspace.options.constraints.left"
|
||||||
|
msgstr "Left"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
||||||
msgid "workspace.options.constraints.leftright"
|
msgid "workspace.options.constraints.leftright"
|
||||||
msgstr "Left & Right"
|
msgstr "Left & Right"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
||||||
msgid "workspace.options.constraints.center"
|
msgid "workspace.options.constraints.right"
|
||||||
msgstr "Center"
|
msgstr "Right"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
||||||
msgid "workspace.options.constraints.scale"
|
msgid "workspace.options.constraints.scale"
|
||||||
|
@ -1912,26 +1934,10 @@ msgstr "Scale"
|
||||||
msgid "workspace.options.constraints.top"
|
msgid "workspace.options.constraints.top"
|
||||||
msgstr "Top"
|
msgstr "Top"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
|
||||||
msgid "workspace.options.constraints.bottom"
|
|
||||||
msgstr "Bottom"
|
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
||||||
msgid "workspace.options.constraints.topbottom"
|
msgid "workspace.options.constraints.topbottom"
|
||||||
msgstr "Top & Bottom"
|
msgstr "Top & Bottom"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
|
||||||
msgid "workspace.options.constraints.center"
|
|
||||||
msgstr "Center"
|
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
|
||||||
msgid "workspace.options.constraints.scale"
|
|
||||||
msgstr "Scale"
|
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
|
||||||
msgid "workspace.options.constraints.fix-when-scrolling"
|
|
||||||
msgstr "Fix when scrolling"
|
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/options.cljs
|
#: src/app/main/ui/workspace/sidebar/options.cljs
|
||||||
msgid "workspace.options.design"
|
msgid "workspace.options.design"
|
||||||
msgstr "Design"
|
msgstr "Design"
|
||||||
|
@ -2686,16 +2692,4 @@ msgid "workspace.updates.update"
|
||||||
msgstr "Update"
|
msgstr "Update"
|
||||||
|
|
||||||
msgid "workspace.viewport.click-to-close-path"
|
msgid "workspace.viewport.click-to-close-path"
|
||||||
msgstr "Click to close the path"
|
msgstr "Click to close the path"
|
||||||
|
|
||||||
msgid "dashboard.export-single"
|
|
||||||
msgstr "Export file"
|
|
||||||
|
|
||||||
msgid "dashboard.export-multi"
|
|
||||||
msgstr "Export %s files"
|
|
||||||
|
|
||||||
msgid "dashboard.import"
|
|
||||||
msgstr "Import files"
|
|
||||||
|
|
||||||
msgid "dashboard.options"
|
|
||||||
msgstr "Options"
|
|
|
@ -51,10 +51,6 @@ msgstr "¿Olvidaste tu contraseña?"
|
||||||
msgid "auth.fullname"
|
msgid "auth.fullname"
|
||||||
msgstr "Nombre completo"
|
msgstr "Nombre completo"
|
||||||
|
|
||||||
#: src/app/main/ui/auth/recovery_request.cljs
|
|
||||||
msgid "auth.go-back-to-login"
|
|
||||||
msgstr "Volver"
|
|
||||||
|
|
||||||
#: src/app/main/ui/auth/register.cljs
|
#: src/app/main/ui/auth/register.cljs
|
||||||
msgid "auth.login-here"
|
msgid "auth.login-here"
|
||||||
msgstr "Entra aquí"
|
msgstr "Entra aquí"
|
||||||
|
@ -915,6 +911,9 @@ msgstr "Fuentes"
|
||||||
msgid "labels.give-feedback"
|
msgid "labels.give-feedback"
|
||||||
msgstr "Danos tu opinión"
|
msgstr "Danos tu opinión"
|
||||||
|
|
||||||
|
msgid "labels.go-back"
|
||||||
|
msgstr "Volver"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs
|
#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs
|
||||||
msgid "labels.hide-resolved-comments"
|
msgid "labels.hide-resolved-comments"
|
||||||
msgstr "Ocultar comentarios resueltos"
|
msgstr "Ocultar comentarios resueltos"
|
||||||
|
@ -1002,6 +1001,9 @@ msgstr "Contraseña anterior"
|
||||||
msgid "labels.only-yours"
|
msgid "labels.only-yours"
|
||||||
msgstr "Sólo los tuyos"
|
msgstr "Sólo los tuyos"
|
||||||
|
|
||||||
|
msgid "labels.or"
|
||||||
|
msgstr "o"
|
||||||
|
|
||||||
#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs
|
#: src/app/main/ui/dashboard/team.cljs, src/app/main/ui/dashboard/team.cljs
|
||||||
msgid "labels.owner"
|
msgid "labels.owner"
|
||||||
msgstr "Dueño"
|
msgstr "Dueño"
|
||||||
|
@ -1451,6 +1453,10 @@ msgstr "Editar archivo"
|
||||||
msgid "viewer.header.fullscreen"
|
msgid "viewer.header.fullscreen"
|
||||||
msgstr "Pantalla completa"
|
msgstr "Pantalla completa"
|
||||||
|
|
||||||
|
#: src/app/main/ui/viewer/header.cljs
|
||||||
|
msgid "viewer.header.interactions"
|
||||||
|
msgstr "Interacciones"
|
||||||
|
|
||||||
#: src/app/main/ui/viewer/header.cljs
|
#: src/app/main/ui/viewer/header.cljs
|
||||||
msgid "viewer.header.share.copy-link"
|
msgid "viewer.header.share.copy-link"
|
||||||
msgstr "Copiar enlace"
|
msgstr "Copiar enlace"
|
||||||
|
@ -1475,10 +1481,6 @@ msgstr "Cualquiera con el enlace podrá acceder"
|
||||||
msgid "viewer.header.share.title"
|
msgid "viewer.header.share.title"
|
||||||
msgstr "Compartir prototipo"
|
msgstr "Compartir prototipo"
|
||||||
|
|
||||||
#: src/app/main/ui/viewer/header.cljs
|
|
||||||
msgid "viewer.header.interactions"
|
|
||||||
msgstr "Interacciones"
|
|
||||||
|
|
||||||
#: src/app/main/ui/viewer/header.cljs
|
#: src/app/main/ui/viewer/header.cljs
|
||||||
msgid "viewer.header.show-interactions"
|
msgid "viewer.header.show-interactions"
|
||||||
msgstr "Mostrar interacciones"
|
msgstr "Mostrar interacciones"
|
||||||
|
@ -1881,20 +1883,28 @@ msgid "workspace.options.constraints"
|
||||||
msgstr "Restricciones"
|
msgstr "Restricciones"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
||||||
msgid "workspace.options.constraints.left"
|
msgid "workspace.options.constraints.bottom"
|
||||||
msgstr "Izquierda"
|
msgstr "Abajo"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
||||||
msgid "workspace.options.constraints.right"
|
msgid "workspace.options.constraints.center"
|
||||||
msgstr "Derecha"
|
msgstr "Centro"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
||||||
|
msgid "workspace.options.constraints.fix-when-scrolling"
|
||||||
|
msgstr "Fijo al desplazar"
|
||||||
|
|
||||||
|
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
||||||
|
msgid "workspace.options.constraints.left"
|
||||||
|
msgstr "Izquierda"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
||||||
msgid "workspace.options.constraints.leftright"
|
msgid "workspace.options.constraints.leftright"
|
||||||
msgstr "Izq. y Der."
|
msgstr "Izq. y Der."
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
||||||
msgid "workspace.options.constraints.center"
|
msgid "workspace.options.constraints.right"
|
||||||
msgstr "Centro"
|
msgstr "Derecha"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
||||||
msgid "workspace.options.constraints.scale"
|
msgid "workspace.options.constraints.scale"
|
||||||
|
@ -1904,26 +1914,10 @@ msgstr "Escalar"
|
||||||
msgid "workspace.options.constraints.top"
|
msgid "workspace.options.constraints.top"
|
||||||
msgstr "Arriba"
|
msgstr "Arriba"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
|
||||||
msgid "workspace.options.constraints.bottom"
|
|
||||||
msgstr "Abajo"
|
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
||||||
msgid "workspace.options.constraints.topbottom"
|
msgid "workspace.options.constraints.topbottom"
|
||||||
msgstr "Arriba y Abajo"
|
msgstr "Arriba y Abajo"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
|
||||||
msgid "workspace.options.constraints.center"
|
|
||||||
msgstr "Centro"
|
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
|
||||||
msgid "workspace.options.constraints.scale"
|
|
||||||
msgstr "Escalar"
|
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs
|
|
||||||
msgid "workspace.options.constraints.fix-when-scrolling"
|
|
||||||
msgstr "Fijo al desplazar"
|
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/options.cljs
|
#: src/app/main/ui/workspace/sidebar/options.cljs
|
||||||
msgid "workspace.options.design"
|
msgid "workspace.options.design"
|
||||||
msgstr "Diseño"
|
msgstr "Diseño"
|
||||||
|
@ -2680,4 +2674,4 @@ msgid "workspace.updates.update"
|
||||||
msgstr "Actualizar"
|
msgstr "Actualizar"
|
||||||
|
|
||||||
msgid "workspace.viewport.click-to-close-path"
|
msgid "workspace.viewport.click-to-close-path"
|
||||||
msgstr "Pulsar para cerrar la ruta"
|
msgstr "Pulsar para cerrar la ruta"
|
|
@ -51,10 +51,6 @@ msgstr "Mot de passe oublié ?"
|
||||||
msgid "auth.fullname"
|
msgid "auth.fullname"
|
||||||
msgstr "Nom complet"
|
msgstr "Nom complet"
|
||||||
|
|
||||||
#: src/app/main/ui/auth/recovery_request.cljs
|
|
||||||
msgid "auth.go-back-to-login"
|
|
||||||
msgstr "Retour !"
|
|
||||||
|
|
||||||
#: src/app/main/ui/auth/register.cljs
|
#: src/app/main/ui/auth/register.cljs
|
||||||
msgid "auth.login-here"
|
msgid "auth.login-here"
|
||||||
msgstr "Se connecter ici"
|
msgstr "Se connecter ici"
|
||||||
|
@ -737,6 +733,9 @@ msgstr "Adresse e‑mail"
|
||||||
msgid "labels.give-feedback"
|
msgid "labels.give-feedback"
|
||||||
msgstr "Donnez votre avis"
|
msgstr "Donnez votre avis"
|
||||||
|
|
||||||
|
msgid "labels.go-back"
|
||||||
|
msgstr "Retour"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs
|
#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs
|
||||||
msgid "labels.hide-resolved-comments"
|
msgid "labels.hide-resolved-comments"
|
||||||
msgstr "Masquer les commentaires résolus"
|
msgstr "Masquer les commentaires résolus"
|
||||||
|
@ -1202,6 +1201,10 @@ msgstr "Modifier la page"
|
||||||
msgid "viewer.header.fullscreen"
|
msgid "viewer.header.fullscreen"
|
||||||
msgstr "Plein écran"
|
msgstr "Plein écran"
|
||||||
|
|
||||||
|
#: src/app/main/ui/viewer/header.cljs
|
||||||
|
msgid "viewer.header.interactions"
|
||||||
|
msgstr "Interactions"
|
||||||
|
|
||||||
#: src/app/main/ui/viewer/header.cljs
|
#: src/app/main/ui/viewer/header.cljs
|
||||||
msgid "viewer.header.share.copy-link"
|
msgid "viewer.header.share.copy-link"
|
||||||
msgstr "Copier le lien"
|
msgstr "Copier le lien"
|
||||||
|
@ -1226,10 +1229,6 @@ msgstr "Toute personne disposant du lien aura accès"
|
||||||
msgid "viewer.header.share.title"
|
msgid "viewer.header.share.title"
|
||||||
msgstr "Partager le prototype"
|
msgstr "Partager le prototype"
|
||||||
|
|
||||||
#: src/app/main/ui/viewer/header.cljs
|
|
||||||
msgid "viewer.header.interactions"
|
|
||||||
msgstr "Interactions"
|
|
||||||
|
|
||||||
#: src/app/main/ui/viewer/header.cljs
|
#: src/app/main/ui/viewer/header.cljs
|
||||||
msgid "viewer.header.show-interactions"
|
msgid "viewer.header.show-interactions"
|
||||||
msgstr "Afficher les interactions"
|
msgstr "Afficher les interactions"
|
||||||
|
|
|
@ -51,10 +51,6 @@ msgstr "Esqueceu a senha?"
|
||||||
msgid "auth.fullname"
|
msgid "auth.fullname"
|
||||||
msgstr "Nome completo"
|
msgstr "Nome completo"
|
||||||
|
|
||||||
#: src/app/main/ui/auth/recovery_request.cljs
|
|
||||||
msgid "auth.go-back-to-login"
|
|
||||||
msgstr "Voltar!"
|
|
||||||
|
|
||||||
#: src/app/main/ui/auth/register.cljs
|
#: src/app/main/ui/auth/register.cljs
|
||||||
msgid "auth.login-here"
|
msgid "auth.login-here"
|
||||||
msgstr "Entrar aqui"
|
msgstr "Entrar aqui"
|
||||||
|
@ -697,6 +693,9 @@ msgstr "Fontes"
|
||||||
msgid "labels.give-feedback"
|
msgid "labels.give-feedback"
|
||||||
msgstr "Enviar feedback"
|
msgstr "Enviar feedback"
|
||||||
|
|
||||||
|
msgid "labels.go-back"
|
||||||
|
msgstr "Voltar"
|
||||||
|
|
||||||
msgid "labels.icons"
|
msgid "labels.icons"
|
||||||
msgstr "Ícones"
|
msgstr "Ícones"
|
||||||
|
|
||||||
|
|
|
@ -52,10 +52,6 @@ msgstr "Ai uitat parola?"
|
||||||
msgid "auth.fullname"
|
msgid "auth.fullname"
|
||||||
msgstr "Numele complet"
|
msgstr "Numele complet"
|
||||||
|
|
||||||
#: src/app/main/ui/auth/recovery_request.cljs
|
|
||||||
msgid "auth.go-back-to-login"
|
|
||||||
msgstr "Întoarce-te!"
|
|
||||||
|
|
||||||
#: src/app/main/ui/auth/register.cljs
|
#: src/app/main/ui/auth/register.cljs
|
||||||
msgid "auth.login-here"
|
msgid "auth.login-here"
|
||||||
msgstr "Conectează-te"
|
msgstr "Conectează-te"
|
||||||
|
@ -907,6 +903,9 @@ msgstr "Fonturi"
|
||||||
msgid "labels.give-feedback"
|
msgid "labels.give-feedback"
|
||||||
msgstr "Lasă un feedback"
|
msgstr "Lasă un feedback"
|
||||||
|
|
||||||
|
msgid "labels.go-back"
|
||||||
|
msgstr "Întoarce-te"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs
|
#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs
|
||||||
msgid "labels.hide-resolved-comments"
|
msgid "labels.hide-resolved-comments"
|
||||||
msgstr "Ascunde comentariile rezolvate"
|
msgstr "Ascunde comentariile rezolvate"
|
||||||
|
@ -1441,6 +1440,10 @@ msgstr "Editează pagina"
|
||||||
msgid "viewer.header.fullscreen"
|
msgid "viewer.header.fullscreen"
|
||||||
msgstr "Ecran complet"
|
msgstr "Ecran complet"
|
||||||
|
|
||||||
|
#: src/app/main/ui/viewer/header.cljs
|
||||||
|
msgid "viewer.header.interactions"
|
||||||
|
msgstr "Interacţiunile"
|
||||||
|
|
||||||
#: src/app/main/ui/viewer/header.cljs
|
#: src/app/main/ui/viewer/header.cljs
|
||||||
msgid "viewer.header.share.copy-link"
|
msgid "viewer.header.share.copy-link"
|
||||||
msgstr "Copiază link"
|
msgstr "Copiază link"
|
||||||
|
@ -1469,10 +1472,6 @@ msgstr "Distribuie link"
|
||||||
msgid "viewer.header.show-interactions"
|
msgid "viewer.header.show-interactions"
|
||||||
msgstr "Afişează interacţiunile"
|
msgstr "Afişează interacţiunile"
|
||||||
|
|
||||||
#: src/app/main/ui/viewer/header.cljs
|
|
||||||
msgid "viewer.header.interactions"
|
|
||||||
msgstr "Interacţiunile"
|
|
||||||
|
|
||||||
#: src/app/main/ui/viewer/header.cljs
|
#: src/app/main/ui/viewer/header.cljs
|
||||||
msgid "viewer.header.show-interactions-on-click"
|
msgid "viewer.header.show-interactions-on-click"
|
||||||
msgstr "Afişează interacţiunile la click"
|
msgstr "Afişează interacţiunile la click"
|
||||||
|
|
|
@ -40,10 +40,6 @@ msgstr "Забыли пароль?"
|
||||||
msgid "auth.fullname"
|
msgid "auth.fullname"
|
||||||
msgstr "Полное имя"
|
msgstr "Полное имя"
|
||||||
|
|
||||||
#: src/app/main/ui/auth/recovery_request.cljs
|
|
||||||
msgid "auth.go-back-to-login"
|
|
||||||
msgstr "Назад!"
|
|
||||||
|
|
||||||
#: src/app/main/ui/auth/register.cljs
|
#: src/app/main/ui/auth/register.cljs
|
||||||
msgid "auth.login-here"
|
msgid "auth.login-here"
|
||||||
msgstr "Войти здесь"
|
msgstr "Войти здесь"
|
||||||
|
@ -359,6 +355,9 @@ msgstr "Email"
|
||||||
msgid "labels.give-feedback"
|
msgid "labels.give-feedback"
|
||||||
msgstr "Дать обратную связь"
|
msgstr "Дать обратную связь"
|
||||||
|
|
||||||
|
msgid "labels.go-back"
|
||||||
|
msgstr "Назад"
|
||||||
|
|
||||||
msgid "labels.icons"
|
msgid "labels.icons"
|
||||||
msgstr "Иконки"
|
msgstr "Иконки"
|
||||||
|
|
||||||
|
@ -563,6 +562,10 @@ msgstr "На странице не найдено ни одного кадра"
|
||||||
msgid "viewer.frame-not-found"
|
msgid "viewer.frame-not-found"
|
||||||
msgstr "Кадры не найдены."
|
msgstr "Кадры не найдены."
|
||||||
|
|
||||||
|
#: src/app/main/ui/viewer/header.cljs
|
||||||
|
msgid "viewer.header-interactions"
|
||||||
|
msgstr "взаимодействия"
|
||||||
|
|
||||||
#: src/app/main/ui/viewer/header.cljs
|
#: src/app/main/ui/viewer/header.cljs
|
||||||
msgid "viewer.header.dont-show-interactions"
|
msgid "viewer.header.dont-show-interactions"
|
||||||
msgstr "Не показывать взаимодействия"
|
msgstr "Не показывать взаимодействия"
|
||||||
|
@ -599,10 +602,6 @@ msgstr "Любой, у кого есть ссылка будет иметь до
|
||||||
msgid "viewer.header.share.title"
|
msgid "viewer.header.share.title"
|
||||||
msgstr "Поделиться ссылкой"
|
msgstr "Поделиться ссылкой"
|
||||||
|
|
||||||
#: src/app/main/ui/viewer/header.cljs
|
|
||||||
msgid "viewer.header-interactions"
|
|
||||||
msgstr "взаимодействия"
|
|
||||||
|
|
||||||
#: src/app/main/ui/viewer/header.cljs
|
#: src/app/main/ui/viewer/header.cljs
|
||||||
msgid "viewer.header.show-interactions"
|
msgid "viewer.header.show-interactions"
|
||||||
msgstr "Показывать взаимодействия"
|
msgstr "Показывать взаимодействия"
|
||||||
|
|
|
@ -51,10 +51,6 @@ msgstr "Parolanı mı unuttun?"
|
||||||
msgid "auth.fullname"
|
msgid "auth.fullname"
|
||||||
msgstr "Tam Adın"
|
msgstr "Tam Adın"
|
||||||
|
|
||||||
#: src/app/main/ui/auth/recovery_request.cljs
|
|
||||||
msgid "auth.go-back-to-login"
|
|
||||||
msgstr "Geri dön!"
|
|
||||||
|
|
||||||
#: src/app/main/ui/auth/register.cljs
|
#: src/app/main/ui/auth/register.cljs
|
||||||
msgid "auth.login-here"
|
msgid "auth.login-here"
|
||||||
msgstr "Buradan giriş yap"
|
msgstr "Buradan giriş yap"
|
||||||
|
@ -739,6 +735,9 @@ msgstr "Fontlar"
|
||||||
msgid "labels.give-feedback"
|
msgid "labels.give-feedback"
|
||||||
msgstr "Geri bildirimde bulun"
|
msgstr "Geri bildirimde bulun"
|
||||||
|
|
||||||
|
msgid "labels.go-back"
|
||||||
|
msgstr "Geri dön"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs
|
#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs
|
||||||
msgid "labels.hide-resolved-comments"
|
msgid "labels.hide-resolved-comments"
|
||||||
msgstr "Çözülmüş yorumları gizle"
|
msgstr "Çözülmüş yorumları gizle"
|
||||||
|
|
|
@ -42,10 +42,6 @@ msgstr "忘记密码?"
|
||||||
msgid "auth.fullname"
|
msgid "auth.fullname"
|
||||||
msgstr "全名"
|
msgstr "全名"
|
||||||
|
|
||||||
#: src/app/main/ui/auth/recovery_request.cljs
|
|
||||||
msgid "auth.go-back-to-login"
|
|
||||||
msgstr "返回!"
|
|
||||||
|
|
||||||
#: src/app/main/ui/auth/register.cljs
|
#: src/app/main/ui/auth/register.cljs
|
||||||
msgid "auth.login-here"
|
msgid "auth.login-here"
|
||||||
msgstr "在这里登录"
|
msgstr "在这里登录"
|
||||||
|
@ -751,6 +747,9 @@ msgstr "反馈已发出"
|
||||||
msgid "labels.give-feedback"
|
msgid "labels.give-feedback"
|
||||||
msgstr "提交反馈"
|
msgstr "提交反馈"
|
||||||
|
|
||||||
|
msgid "labels.go-back"
|
||||||
|
msgstr "返回"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs
|
#: src/app/main/ui/workspace/comments.cljs, src/app/main/ui/viewer/header.cljs
|
||||||
msgid "labels.hide-resolved-comments"
|
msgid "labels.hide-resolved-comments"
|
||||||
msgstr "隐藏已决定的评论"
|
msgstr "隐藏已决定的评论"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue