mirror of
https://github.com/penpot/penpot.git
synced 2025-03-30 16:41:20 -05:00
Merge pull request #1656 from penpot/social-logins-redesign
Authentication page and OIDC flows improvements
This commit is contained in:
commit
1e580638d2
42 changed files with 418 additions and 264 deletions
|
@ -6,6 +6,7 @@
|
|||
### :sparkles: New features
|
||||
|
||||
- Set an artboard as the file thumbnail [Taiga #1526](https://tree.taiga.io/project/penpot/us/1526)
|
||||
- Social login redesign [Taiga #2974](https://tree.taiga.io/project/penpot/task/2974)
|
||||
- Add border radius to our artboars [Taiga #2056](https://tree.taiga.io/project/penpot/us/2056)
|
||||
- Allow send multiple team invitations at once [Taiga #2798](https://tree.taiga.io/project/penpot/us/2798)
|
||||
- Persist color palette and color picker across refresh [Taiga #1660](https://tree.taiga.io/project/penpot/issue/1660)
|
||||
|
@ -18,6 +19,9 @@
|
|||
- New focus mode in workspace [Taiga #2748](https://tree.taiga.io/project/penpot/us/2748)
|
||||
- Changed text shapes to be displayed as natives SVG text elements [Taiga #2759](https://tree.taiga.io/project/penpot/us/2759)
|
||||
- Texts now can have strokes, multiple fills and can be used as masks
|
||||
- Add the ability to specify the attr for retrieve the email on OIDC integration [#1460](https://github.com/penpot/penpot/issues/1460)
|
||||
- Allow registration with invitation token when registration is disabled
|
||||
- Add the ability to disable standard, password login [Taiga #2999](https://tree.taiga.io/project/penpot/us/2999)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
|
|
|
@ -90,7 +90,7 @@
|
|||
|
||||
(s/def ::flags ::us/set-of-keywords)
|
||||
|
||||
;; DEPRECATED PROPERTIES: should be removed in 1.10
|
||||
;; DEPRECATED PROPERTIES
|
||||
(s/def ::registration-enabled ::us/boolean)
|
||||
(s/def ::smtp-enabled ::us/boolean)
|
||||
(s/def ::telemetry-enabled ::us/boolean)
|
||||
|
@ -138,11 +138,15 @@
|
|||
(s/def ::oidc-scopes ::us/set-of-str)
|
||||
(s/def ::oidc-roles ::us/set-of-str)
|
||||
(s/def ::oidc-roles-attr ::us/keyword)
|
||||
(s/def ::oidc-email-attr ::us/keyword)
|
||||
(s/def ::oidc-name-attr ::us/keyword)
|
||||
(s/def ::host ::us/string)
|
||||
(s/def ::http-server-port ::us/integer)
|
||||
(s/def ::http-server-host ::us/string)
|
||||
(s/def ::http-server-min-threads ::us/integer)
|
||||
(s/def ::http-server-max-threads ::us/integer)
|
||||
(s/def ::http-server-max-body-size ::us/integer)
|
||||
(s/def ::http-server-max-multipart-body-size ::us/integer)
|
||||
(s/def ::http-server-io-threads ::us/integer)
|
||||
(s/def ::http-server-worker-threads ::us/integer)
|
||||
(s/def ::http-session-idle-max-age ::dt/duration)
|
||||
(s/def ::http-session-updater-batch-max-age ::dt/duration)
|
||||
(s/def ::http-session-updater-batch-max-size ::us/integer)
|
||||
|
@ -239,12 +243,16 @@
|
|||
::oidc-user-uri
|
||||
::oidc-scopes
|
||||
::oidc-roles-attr
|
||||
::oidc-email-attr
|
||||
::oidc-name-attr
|
||||
::oidc-roles
|
||||
::host
|
||||
::http-server-host
|
||||
::http-server-port
|
||||
::http-server-max-threads
|
||||
::http-server-min-threads
|
||||
::http-server-max-body-size
|
||||
::http-server-max-multipart-body-size
|
||||
::http-server-io-threads
|
||||
::http-server-worker-threads
|
||||
::http-session-idle-max-age
|
||||
::http-session-updater-batch-max-age
|
||||
::http-session-updater-batch-max-size
|
||||
|
@ -339,8 +347,8 @@
|
|||
(when (ex/ex-info? e)
|
||||
(println ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;")
|
||||
(println "Error on validating configuration:")
|
||||
(println (:explain (ex-data e))
|
||||
(println ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;")))
|
||||
(println (us/pretty-explain (ex-data e)))
|
||||
(println ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"))
|
||||
(throw e))))
|
||||
|
||||
(def version
|
||||
|
|
|
@ -7,9 +7,7 @@
|
|||
(ns app.http
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.logging :as l]
|
||||
[app.common.spec :as us]
|
||||
[app.http.doc :as doc]
|
||||
[app.http.errors :as errors]
|
||||
[app.http.middleware :as middleware]
|
||||
|
@ -31,40 +29,46 @@
|
|||
|
||||
(s/def ::handler fn?)
|
||||
(s/def ::router some?)
|
||||
(s/def ::port ::us/integer)
|
||||
(s/def ::host ::us/string)
|
||||
(s/def ::name ::us/string)
|
||||
(s/def ::executors (s/map-of keyword? ::wrk/executor))
|
||||
(s/def ::port integer?)
|
||||
(s/def ::host string?)
|
||||
(s/def ::name string?)
|
||||
|
||||
;; (s/def ::max-threads ::cf/http-server-max-threads)
|
||||
;; (s/def ::min-threads ::cf/http-server-min-threads)
|
||||
(s/def ::max-body-size integer?)
|
||||
(s/def ::max-multipart-body-size integer?)
|
||||
(s/def ::io-threads integer?)
|
||||
(s/def ::worker-threads integer?)
|
||||
|
||||
(defmethod ig/prep-key ::server
|
||||
[_ cfg]
|
||||
(merge {:name "http"
|
||||
:port 6060
|
||||
:host "0.0.0.0"}
|
||||
:host "0.0.0.0"
|
||||
:max-body-size (* 1024 1024 24) ; 24 MiB
|
||||
:max-multipart-body-size (* 1024 1024 120)} ; 120 MiB
|
||||
(d/without-nils cfg)))
|
||||
|
||||
(defmethod ig/pre-init-spec ::server [_]
|
||||
(s/keys :req-un [::port ::host ::name ::executors]
|
||||
:opt-un [::router ::handler]))
|
||||
(s/and
|
||||
(s/keys :req-un [::port ::host ::name ::max-body-size ::max-multipart-body-size]
|
||||
:opt-un [::router ::handler ::io-threads ::worker-threads ::wrk/executor])
|
||||
(fn [cfg]
|
||||
(or (contains? cfg :router)
|
||||
(contains? cfg :handler)))))
|
||||
|
||||
(defmethod ig/init-key ::server
|
||||
[_ {:keys [handler router port name host executors] :as cfg}]
|
||||
(l/info :hint "starting http server"
|
||||
:port port :host host :name name)
|
||||
|
||||
[_ {:keys [handler router port name host] :as cfg}]
|
||||
(l/info :hint "starting http server" :port port :host host :name name)
|
||||
(let [options {:http/port port
|
||||
:http/host host
|
||||
:ring/async true
|
||||
:xnio/dispatch (:default executors)}
|
||||
handler (cond
|
||||
(fn? handler) handler
|
||||
(some? router) (wrap-router cfg router)
|
||||
:else (ex/raise :type :internal
|
||||
:code :invalid-argument
|
||||
:hint "Missing `handler` or `router` option."))
|
||||
:http/max-body-size (:max-body-size cfg)
|
||||
:http/max-multipart-body-size (:max-multipart-body-size cfg)
|
||||
:xnio/io-threads (:io-threads cfg)
|
||||
:xnio/worker-threads (:worker-threads cfg)
|
||||
:xnio/dispatch (:executor cfg)
|
||||
:ring/async true}
|
||||
handler (if (some? router)
|
||||
(wrap-router cfg router)
|
||||
handler)
|
||||
server (yt/server handler (d/without-nils options))]
|
||||
(assoc cfg :server (yt/start! server))))
|
||||
|
||||
|
@ -97,7 +101,7 @@
|
|||
(handler request respond
|
||||
(fn [cause]
|
||||
(l/error :hint "unexpected error processing request"
|
||||
::l/context (errors/get-error-context request cause)
|
||||
::l/context (errors/get-context request)
|
||||
:query-string (yrq/query request)
|
||||
:cause cause)
|
||||
(respond (yrs/response 500 "internal server error")))))))
|
||||
|
|
|
@ -162,7 +162,8 @@
|
|||
(let [context (dissoc report
|
||||
:trace :cause :params :data :spec-problems
|
||||
:spec-explain :spec-value :error :explain :hint)
|
||||
params {:context (with-out-str (fpp/pprint context {:width 300}))
|
||||
params {:context (with-out-str
|
||||
(fpp/pprint context {:width 200}))
|
||||
:hint (:hint report)
|
||||
:spec-explain (:spec-explain report)
|
||||
:spec-problems (:spec-problems report)
|
||||
|
|
|
@ -21,27 +21,17 @@
|
|||
(yrq/get-header request "x-real-ip")
|
||||
(yrq/remote-addr request)))
|
||||
|
||||
(defn get-error-context
|
||||
[request error]
|
||||
(let [data (ex-data error)]
|
||||
(merge
|
||||
{:path (:uri request)
|
||||
:method (:request-method request)
|
||||
:hint (ex-message error)
|
||||
:params (:params request)
|
||||
|
||||
:spec-problems (some->> data ::s/problems (take 10) seq vec)
|
||||
:spec-value (some->> data ::s/value)
|
||||
:data (some-> data (dissoc ::s/problems ::s/value ::s/spec))
|
||||
:ip-addr (parse-client-ip request)
|
||||
:profile-id (:profile-id request)}
|
||||
|
||||
(let [headers (:headers request)]
|
||||
{:user-agent (get headers "user-agent")
|
||||
:frontend-version (get headers "x-frontend-version" "unknown")})
|
||||
|
||||
(when (and data (::s/problems data))
|
||||
{:spec-explain (us/pretty-explain data)}))))
|
||||
(defn get-context
|
||||
[request]
|
||||
(merge
|
||||
{:path (:uri request)
|
||||
:method (:request-method request)
|
||||
:params (:params request)
|
||||
:ip-addr (parse-client-ip request)
|
||||
:profile-id (:profile-id request)}
|
||||
(let [headers (:headers request)]
|
||||
{:user-agent (get headers "user-agent")
|
||||
:frontend-version (get headers "x-frontend-version" "unknown")})))
|
||||
|
||||
(defmulti handle-exception
|
||||
(fn [err & _rest]
|
||||
|
@ -70,7 +60,7 @@
|
|||
(let [edata (ex-data error)
|
||||
explain (us/pretty-explain edata)]
|
||||
(l/error ::l/raw (ex-message error)
|
||||
::l/context (get-error-context request error)
|
||||
::l/context (get-context request)
|
||||
:cause error)
|
||||
(yrs/response :status 500
|
||||
:body {:type :server-error
|
||||
|
@ -96,7 +86,7 @@
|
|||
(handle-exception (:handling edata) request)
|
||||
(do
|
||||
(l/error ::l/raw (ex-message error)
|
||||
::l/context (get-error-context request error)
|
||||
::l/context (get-context request)
|
||||
:cause error)
|
||||
(yrs/response 500 {:type :server-error
|
||||
:code :unexpected
|
||||
|
@ -107,7 +97,7 @@
|
|||
[error request]
|
||||
(let [state (.getSQLState ^java.sql.SQLException error)]
|
||||
(l/error ::l/raw (ex-message error)
|
||||
::l/context (get-error-context request error)
|
||||
::l/context (get-context request)
|
||||
:cause error)
|
||||
(cond
|
||||
(= state "57014")
|
||||
|
|
|
@ -57,7 +57,8 @@
|
|||
:grant_type "authorization_code"
|
||||
:redirect_uri (build-redirect-uri cfg)}
|
||||
req {:method :post
|
||||
:headers {"content-type" "application/x-www-form-urlencoded"}
|
||||
:headers {"content-type" "application/x-www-form-urlencoded"
|
||||
"accept" "application/json"}
|
||||
:uri (:token-uri provider)
|
||||
:body (u/map->query-string params)}]
|
||||
(p/then
|
||||
|
@ -69,42 +70,61 @@
|
|||
:type (get data :token_type)})
|
||||
(ex/raise :type :internal
|
||||
:code :unable-to-retrieve-token
|
||||
::http-status status
|
||||
::http-body body))))))
|
||||
:http-status status
|
||||
:http-body body))))))
|
||||
|
||||
(defn- retrieve-user-info
|
||||
[{:keys [provider http-client] :as cfg} tdata]
|
||||
(p/then
|
||||
(http-client {:uri (:user-uri provider)
|
||||
:headers {"Authorization" (str (:type tdata) " " (:token tdata))}
|
||||
:timeout 6000
|
||||
:method :get})
|
||||
(fn [{:keys [status body] :as res}]
|
||||
(if (= 200 status)
|
||||
(let [info (json/read body)
|
||||
info {:backend (:name provider)
|
||||
:email (get info :email)
|
||||
:fullname (get info :name)
|
||||
:props (->> (dissoc info :name :email)
|
||||
(qualify-props provider))}]
|
||||
(letfn [(retrieve []
|
||||
(http-client {:uri (:user-uri provider)
|
||||
:headers {"Authorization" (str (:type tdata) " " (:token tdata))}
|
||||
:timeout 6000
|
||||
:method :get}))
|
||||
|
||||
(when-not (s/valid? ::info info)
|
||||
(l/warn :hint "received incomplete profile info object (please set correct scopes)"
|
||||
:info (pr-str info))
|
||||
(ex/raise :type :internal
|
||||
:code :unable-to-auth
|
||||
:hint "no user info"))
|
||||
info)
|
||||
(ex/raise :type :internal
|
||||
:code :unable-to-retrieve-user-info
|
||||
::http-status status
|
||||
::http-body body)))))
|
||||
(validate-response [{:keys [status body] :as res}]
|
||||
(when-not (= 200 status)
|
||||
(ex/raise :type :internal
|
||||
:code :unable-to-retrieve-user-info
|
||||
:hint "unable to retrieve user info"
|
||||
:http-status status
|
||||
:http-body body))
|
||||
res)
|
||||
|
||||
(get-email [info]
|
||||
(let [attr-kw (cf/get :oidc-email-attr :email)]
|
||||
(get info attr-kw)))
|
||||
|
||||
(get-name [info]
|
||||
(let [attr-kw (cf/get :oidc-name-attr :name)]
|
||||
(get info attr-kw)))
|
||||
|
||||
(process-response [{:keys [body]}]
|
||||
(let [info (json/read body)]
|
||||
{:backend (:name provider)
|
||||
:email (get-email info)
|
||||
:fullname (get-name info)
|
||||
:props (->> (dissoc info :name :email)
|
||||
(qualify-props provider))}))
|
||||
|
||||
(validate-info [info]
|
||||
(when-not (s/valid? ::info info)
|
||||
(l/warn :hint "received incomplete profile info object (please set correct scopes)"
|
||||
:info (pr-str info))
|
||||
(ex/raise :type :internal
|
||||
:code :incomplete-user-info
|
||||
:hint "inconmplete user info"
|
||||
:info info))
|
||||
info)]
|
||||
|
||||
(-> (retrieve)
|
||||
(p/then' validate-response)
|
||||
(p/then' process-response)
|
||||
(p/then' validate-info))))
|
||||
|
||||
(s/def ::backend ::us/not-empty-string)
|
||||
(s/def ::email ::us/not-empty-string)
|
||||
(s/def ::fullname ::us/not-empty-string)
|
||||
(s/def ::props (s/map-of ::us/keyword any?))
|
||||
|
||||
(s/def ::info
|
||||
(s/keys :req-un [::backend
|
||||
::email
|
||||
|
@ -112,7 +132,7 @@
|
|||
::props]))
|
||||
|
||||
(defn retrieve-info
|
||||
[{:keys [tokens provider] :as cfg} request]
|
||||
[{:keys [tokens provider] :as cfg} {:keys [params] :as request}]
|
||||
(letfn [(validate-oidc [info]
|
||||
;; If the provider is OIDC, we can proceed to check
|
||||
;; roles if they are defined.
|
||||
|
@ -143,9 +163,15 @@
|
|||
(map? (:props state))
|
||||
(update :props merge (:props state))))]
|
||||
|
||||
(let [state (get-in request [:params :state])
|
||||
state (tokens :verify {:token state :iss :oauth})
|
||||
code (get-in request [:params :code])]
|
||||
(when-let [error (get params :error)]
|
||||
(ex/raise :type :internal
|
||||
:code :error-on-retrieving-code
|
||||
:error-id error
|
||||
:error-desc (get params :error_description)))
|
||||
|
||||
(let [state (get params :state)
|
||||
code (get params :code)
|
||||
state (tokens :verify {:token state :iss :oauth})]
|
||||
(-> (p/resolved code)
|
||||
(p/then #(retrieve-access-token cfg %))
|
||||
(p/then #(retrieve-user-info cfg %))
|
||||
|
@ -224,15 +250,18 @@
|
|||
(redirect-response uri))))
|
||||
|
||||
(defn- auth-handler
|
||||
[{:keys [tokens] :as cfg} {:keys [params] :as request} respond _]
|
||||
(let [props (extract-utm-props params)
|
||||
state (tokens :generate
|
||||
{:iss :oauth
|
||||
:invitation-token (:invitation-token params)
|
||||
:props props
|
||||
:exp (dt/in-future "15m")})
|
||||
uri (build-auth-uri cfg state)]
|
||||
(respond (yrs/response 200 {:redirect-uri uri}))))
|
||||
[{:keys [tokens] :as cfg} {:keys [params] :as request} respond raise]
|
||||
(try
|
||||
(let [props (extract-utm-props params)
|
||||
state (tokens :generate
|
||||
{:iss :oauth
|
||||
:invitation-token (:invitation-token params)
|
||||
:props props
|
||||
:exp (dt/in-future "15m")})
|
||||
uri (build-auth-uri cfg state)]
|
||||
(respond (yrs/response 200 {:redirect-uri uri})))
|
||||
(catch Throwable cause
|
||||
(raise cause))))
|
||||
|
||||
(defn- callback-handler
|
||||
[cfg request respond _]
|
||||
|
@ -242,7 +271,7 @@
|
|||
(generate-redirect cfg request info profile)))
|
||||
|
||||
(handle-error [cause]
|
||||
(l/warn :hint "error on oauth process" :cause cause)
|
||||
(l/error :hint "error on oauth process" :cause cause)
|
||||
(respond (generate-error-redirect cfg cause)))]
|
||||
|
||||
(-> (process-request)
|
||||
|
@ -385,17 +414,16 @@
|
|||
(assoc-in cfg [:providers "github"] opts))
|
||||
cfg)))
|
||||
|
||||
|
||||
(defn- initialize-gitlab-provider
|
||||
[cfg]
|
||||
(let [base (cf/get :gitlab-base-uri "https://gitlab.com")
|
||||
opts {:base-uri base
|
||||
:client-id (cf/get :gitlab-client-id)
|
||||
:client-secret (cf/get :gitlab-client-secret)
|
||||
:scopes #{"read_user"}
|
||||
:scopes #{"openid" "profile" "email"}
|
||||
:auth-uri (str base "/oauth/authorize")
|
||||
:token-uri (str base "/oauth/token")
|
||||
:user-uri (str base "/api/v4/user")
|
||||
:user-uri (str base "/oauth/userinfo")
|
||||
:name "gitlab"}]
|
||||
(if (and (string? (:client-id opts))
|
||||
(string? (:client-secret opts)))
|
||||
|
|
|
@ -254,7 +254,7 @@
|
|||
"select * from audit_log
|
||||
where archived_at is null
|
||||
order by created_at asc
|
||||
limit 1000
|
||||
limit 256
|
||||
for update skip locked;")
|
||||
|
||||
(defn archive-events
|
||||
|
@ -298,8 +298,9 @@
|
|||
(if (= (:status resp) 204)
|
||||
true
|
||||
(do
|
||||
(l/warn :hint "unable to archive events"
|
||||
:resp-status (:status resp))
|
||||
(l/error :hint "unable to archive events"
|
||||
:resp-status (:status resp)
|
||||
:resp-body (:body resp))
|
||||
false))))
|
||||
|
||||
(mark-as-archived [conn rows]
|
||||
|
|
|
@ -113,7 +113,8 @@
|
|||
:host (cf/get :http-server-host)
|
||||
:router (ig/ref :app.http/router)
|
||||
:metrics (ig/ref :app.metrics/metrics)
|
||||
:executors (ig/ref :app.worker/executors)}
|
||||
:executor (ig/ref [::default :app.worker/executor])
|
||||
:io-threads (cf/get :http-server-io-threads)}
|
||||
|
||||
:app.http/router
|
||||
{:assets (ig/ref :app.http.assets/handlers)
|
||||
|
|
|
@ -262,10 +262,3 @@
|
|||
:gauge (make-gauge props)
|
||||
:summary (make-summary props)
|
||||
:histogram (make-histogram props)))
|
||||
|
||||
;; (defn instrument-jetty!
|
||||
;; [^CollectorRegistry registry ^StatisticsHandler handler]
|
||||
;; (doto (JettyStatisticsCollector. handler)
|
||||
;; (.register registry))
|
||||
;; nil)
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
[app.rpc.queries.profile :as profile]
|
||||
[app.rpc.rlimit :as rlimit]
|
||||
[app.storage :as sto]
|
||||
[app.util.async :as async]
|
||||
[app.util.services :as sv]
|
||||
[app.util.time :as dt]
|
||||
[buddy.hashers :as hashers]
|
||||
|
@ -101,8 +100,14 @@
|
|||
(sv/defmethod ::prepare-register-profile {:auth false}
|
||||
[{:keys [pool tokens] :as cfg} params]
|
||||
(when-not (contains? cf/flags :registration)
|
||||
(ex/raise :type :restriction
|
||||
:code :registration-disabled))
|
||||
(if-not (contains? params :invitation-token)
|
||||
(ex/raise :type :restriction
|
||||
:code :registration-disabled)
|
||||
(let [invitation (tokens :verify {:token (:invitation-token params) :iss :team-invitation})]
|
||||
(when-not (= (:email params) (:member-email invitation))
|
||||
(ex/raise :type :restriction
|
||||
:code :email-does-not-match-invitation
|
||||
:hint "email should match the invitation")))))
|
||||
|
||||
(when-let [domains (cf/get :registration-domain-whitelist)]
|
||||
(when-not (email-domain-in-whitelist? domains (:email params))
|
||||
|
@ -129,6 +134,7 @@
|
|||
:backend "penpot"
|
||||
:iss :prepared-register
|
||||
:exp (dt/in-future "48h")}
|
||||
|
||||
token (tokens :generate params)]
|
||||
{:token token}))
|
||||
|
||||
|
@ -149,7 +155,6 @@
|
|||
[{:keys [conn tokens session] :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 [is-active (or (:is-active params)
|
||||
|
@ -158,10 +163,8 @@
|
|||
(create-profile conn)
|
||||
(create-profile-relations conn)
|
||||
(decode-profile-row))
|
||||
|
||||
invitation (when-let [token (:invitation-token params)]
|
||||
(tokens :verify {:token token :iss :team-invitation}))]
|
||||
|
||||
(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
|
||||
|
@ -280,10 +283,14 @@
|
|||
:opt-un [::scope ::invitation-token]))
|
||||
|
||||
(sv/defmethod ::login
|
||||
{:auth false
|
||||
::async/dispatch :default
|
||||
::rlimit/permits (cf/get :rlimit-password)}
|
||||
{:auth false ::rlimit/permits (cf/get :rlimit-password)}
|
||||
[{:keys [pool session tokens] :as cfg} {:keys [email password] :as params}]
|
||||
|
||||
(when-not (contains? cf/flags :login)
|
||||
(ex/raise :type :restriction
|
||||
:code :login-disabled
|
||||
:hint "login is disabled in this instance"))
|
||||
|
||||
(letfn [(check-password [profile password]
|
||||
(when (= (:password profile) "!")
|
||||
(ex/raise :type :validation
|
||||
|
|
|
@ -259,7 +259,7 @@
|
|||
;; A task responsible to permanently delete already marked as deleted
|
||||
;; storage files. The storage objects are practically never marked to
|
||||
;; be deleted directly by the api call. The touched-gc is responsible
|
||||
;; collect the usage of the object and mark it as deleted.
|
||||
;; of collecting the usage of the object and mark it as deleted.
|
||||
|
||||
(declare sql:retrieve-deleted-objects-chunk)
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
(ns app.services-profile-test
|
||||
(:require
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.rpc.mutations.profile :as profile]
|
||||
[app.test-helpers :as th]
|
||||
|
@ -195,6 +196,56 @@
|
|||
(t/is (nil? error))))
|
||||
))
|
||||
|
||||
(t/deftest prepare-and-register-with-invitation-and-disabled-registration-1
|
||||
(with-redefs [app.config/flags [:disable-registration]]
|
||||
(let [tokens-fn (:app.tokens/tokens th/*system*)
|
||||
itoken (tokens-fn :generate
|
||||
{:iss :team-invitation
|
||||
:exp (dt/in-future "48h")
|
||||
:role :editor
|
||||
:team-id uuid/zero
|
||||
:member-email "user@example.com"})
|
||||
data {::th/type :prepare-register-profile
|
||||
:invitation-token itoken
|
||||
:email "user@example.com"
|
||||
:password "foobar"}
|
||||
|
||||
{:keys [result error] :as out} (th/mutation! data)]
|
||||
(t/is (nil? error))
|
||||
(t/is (map? result))
|
||||
(t/is (string? (:token result)))
|
||||
|
||||
(let [rtoken (:token result)
|
||||
data {::th/type :register-profile
|
||||
:token rtoken
|
||||
:fullname "foobar"}
|
||||
|
||||
{:keys [result error] :as out} (th/mutation! data)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? error))
|
||||
(t/is (map? result))
|
||||
(t/is (string? (:invitation-token result)))))))
|
||||
|
||||
(t/deftest prepare-and-register-with-invitation-and-disabled-registration-2
|
||||
(with-redefs [app.config/flags [:disable-registration]]
|
||||
(let [tokens-fn (:app.tokens/tokens th/*system*)
|
||||
itoken (tokens-fn :generate
|
||||
{:iss :team-invitation
|
||||
:exp (dt/in-future "48h")
|
||||
:role :editor
|
||||
:team-id uuid/zero
|
||||
:member-email "user2@example.com"})
|
||||
|
||||
data {::th/type :prepare-register-profile
|
||||
:invitation-token itoken
|
||||
:email "user@example.com"
|
||||
:password "foobar"}
|
||||
{:keys [result error] :as out} (th/mutation! data)]
|
||||
(t/is (th/ex-info? error))
|
||||
(t/is (= :restriction (th/ex-type error)))
|
||||
(t/is (= :email-does-not-match-invitation (th/ex-code error))))))
|
||||
|
||||
|
||||
(t/deftest prepare-register-with-registration-disabled
|
||||
(th/with-mocks {#'app.config/flags nil}
|
||||
(let [data {::th/type :prepare-register-profile
|
||||
|
|
|
@ -313,6 +313,14 @@
|
|||
[v]
|
||||
(instance? clojure.lang.ExceptionInfo v))
|
||||
|
||||
(defn ex-type
|
||||
[e]
|
||||
(:type (ex-data e)))
|
||||
|
||||
(defn ex-code
|
||||
[e]
|
||||
(:code (ex-data e)))
|
||||
|
||||
(defn ex-of-type?
|
||||
[e type]
|
||||
(let [data (ex-data e)]
|
||||
|
|
|
@ -23,11 +23,12 @@
|
|||
::cause]))
|
||||
|
||||
(defn error
|
||||
[& {:keys [hint cause ::data] :as params}]
|
||||
[& {:keys [hint cause ::data type] :as params}]
|
||||
(s/assert ::error-params params)
|
||||
(let [payload (-> params
|
||||
(dissoc :cause ::data)
|
||||
(merge data))]
|
||||
(merge data))
|
||||
hint (or hint (pr-str type))]
|
||||
(ex-info hint payload cause)))
|
||||
|
||||
(defmacro raise
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
(def default
|
||||
"A common flags that affects both: backend and frontend."
|
||||
[:enable-registration
|
||||
:enable-demo-users])
|
||||
:enable-login])
|
||||
|
||||
(defn parse
|
||||
[& flags]
|
||||
|
|
|
@ -8,8 +8,10 @@
|
|||
(:require
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.common.spec :as us]
|
||||
[clojure.pprint :refer [pprint]]
|
||||
[cuerdas.core :as str]
|
||||
[clojure.spec.alpha :as s]
|
||||
[fipp.edn :as fpp]
|
||||
#?(:clj [io.aviso.exception :as ie])
|
||||
#?(:cljs [goog.log :as glog]))
|
||||
|
@ -152,6 +154,18 @@
|
|||
[logger level]
|
||||
(.isEnabled ^Logger logger ^Level level)))
|
||||
|
||||
#?(:clj
|
||||
(defn get-error-context
|
||||
[error]
|
||||
(when-let [data (ex-data error)]
|
||||
(merge
|
||||
{:hint (ex-message error)
|
||||
:spec-problems (some->> data ::s/problems (take 10) seq vec)
|
||||
:spec-value (some->> data ::s/value)
|
||||
:data (some-> data (dissoc ::s/problems ::s/value ::s/spec))}
|
||||
(when (and data (::s/problems data))
|
||||
{:spec-explain (us/pretty-explain data)})))))
|
||||
|
||||
(defmacro log
|
||||
[& {:keys [level cause ::logger ::async ::raw ::context] :or {async true} :as props}]
|
||||
(if (:ns &env) ; CLJS
|
||||
|
@ -169,7 +183,9 @@
|
|||
~(if async
|
||||
`(send-off logging-agent
|
||||
(fn [_#]
|
||||
(with-context (into {:id (uuid/next)} ~context)
|
||||
(with-context (merge {:id (uuid/next)}
|
||||
(get-error-context ~cause)
|
||||
~context)
|
||||
(->> (or ~raw (build-map-message ~props))
|
||||
(write-log! ~logger-sym ~level-sym ~cause)))))
|
||||
|
||||
|
|
|
@ -140,22 +140,26 @@
|
|||
|
||||
;; --- SPEC: set of Keywords
|
||||
|
||||
(s/def ::set-of-keywords
|
||||
(s/conformer
|
||||
(fn [s]
|
||||
(let [xform (comp
|
||||
(map (fn [s]
|
||||
(cond
|
||||
(string? s) (keyword s)
|
||||
(keyword? s) s
|
||||
:else nil)))
|
||||
(filter identity))]
|
||||
(cond
|
||||
(set? s) (into #{} xform s)
|
||||
(string? s) (into #{} xform (str/words s))
|
||||
:else ::s/invalid)))
|
||||
(fn [s]
|
||||
(str/join " " (map name s)))))
|
||||
(letfn [(conform-fn [dest s]
|
||||
(let [xform (keep (fn [s]
|
||||
(cond
|
||||
(string? s) (keyword s)
|
||||
(keyword? s) s
|
||||
:else nil)))]
|
||||
(cond
|
||||
(set? s) (into dest xform s)
|
||||
(string? s) (into dest xform (str/words s))
|
||||
:else ::s/invalid)))]
|
||||
|
||||
(s/def ::set-of-keywords
|
||||
(s/conformer
|
||||
(fn [s] (conform-fn #{} s))
|
||||
(fn [s] (str/join " " (map name s)))))
|
||||
|
||||
(s/def ::vec-of-keywords
|
||||
(s/conformer
|
||||
(fn [s] (conform-fn [] s))
|
||||
(fn [s] (str/join " " (map name s))))))
|
||||
|
||||
;; --- SPEC: email
|
||||
|
||||
|
|
|
@ -1,10 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 13.1072 13.10542" preserveAspectRatio="xMinYMin meet">
|
||||
<path d="M6.5534 13.1502l2.41333-7.42742H4.14l2.41334 7.42743z" fill="#e24329"/>
|
||||
<path d="M6.5534 13.15016L4.14 5.72273H.75783l5.79556 7.42743z" fill="#fc6d26"/>
|
||||
<path d="M.75783 5.72273L.02446 7.97991a.49964.49964 0 00.18147.5586l6.34746 4.6117L.75777 5.72278z" fill="#fca326"/>
|
||||
<path d="M.75783 5.72278H4.14L2.68654 1.24927c-.0748-.2302-.40045-.23015-.4752 0L.75783 5.72278z" fill="#e24329"/>
|
||||
<path d="M6.5534 13.15016l2.41333-7.42743h3.38223l-5.79562 7.42743z" fill="#fc6d26"/>
|
||||
<path d="M12.34896 5.72273l.73336 2.25718" fill="#fca326"/>
|
||||
<path d="M12.34896 5.72278H8.96673l1.45351-4.47351c.0748-.2302.40045-.23015.4752 0l1.45352 4.47351z" fill="#e24329"/>
|
||||
<path d="M12.34937 5.72273l.73337 2.25718a.49964.49964 0 01-.18147.5586l-6.34746 4.6117 5.79561-7.42742z" fill="#fca326"/>
|
||||
</svg>
|
||||
<svg viewBox="3658.551 302.026 20 17.949" width="20" height="17.949" xmlns="http://www.w3.org/2000/svg" style="-webkit-print-color-adjust:exact"><path d="m3668.55 319.974 3.685-11.043h-7.364l3.68 11.043ZM3659.71 308.932l-1.122 3.355a.733.733 0 0 0 .277.83l9.685 6.857-8.84-11.042ZM3659.71 308.931h5.16l-2.22-6.65c-.114-.34-.61-.34-.727 0l-2.213 6.65Z" style="fill:#fff"/><path d="m3677.396 308.932 1.118 3.355a.733.733 0 0 1-.276.83l-9.688 6.857 8.846-11.042ZM3677.396 308.931h-5.16l2.216-6.65c.114-.34.61-.34.727 0l2.217 6.65ZM3668.55 319.974l3.685-11.042h5.16l-8.845 11.042ZM3668.55 319.974l-8.84-11.042h5.16l3.68 11.042Z" style="fill:#fff"/></svg>
|
Before Width: | Height: | Size: 954 B After Width: | Height: | Size: 650 B |
1
frontend/resources/images/icons/brand-google.svg
Normal file
1
frontend/resources/images/icons/brand-google.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="5345 -1143 500 500"><path fill="#fff" fill-rule="evenodd" d="M5845-887c0-18-1-35-4-51h-240v96h137c-6 32-24 58-51 76v63h82c49-44 76-108 76-184z" clip-rule="evenodd"/><path fill="#fff" fill-rule="evenodd" d="M5601-643c68 0 126-22 168-60l-82-63a156 156 0 0 1-229-79h-85v64c42 82 128 138 228 138z" clip-rule="evenodd"/><path fill="#fff" fill-rule="evenodd" d="M5458-845a148 148 0 0 1 0-95v-65h-85a246 246 0 0 0 0 224z" clip-rule="evenodd"/><path fill="#fff" fill-rule="evenodd" d="M5601-1043c37 0 71 12 97 37l73-72a256 256 0 0 0-399 73l86 65c20-59 76-103 143-103z" clip-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 638 B |
1
frontend/resources/images/icons/brand-openid.svg
Normal file
1
frontend/resources/images/icons/brand-openid.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg viewBox="7437 302 20.011 18.182" width="20.011" height="18.182" xmlns="http://www.w3.org/2000/svg" style="-webkit-print-color-adjust:exact"><path d="M7455.039 309.1c-1.9-1.183-4.555-1.918-7.46-1.918-5.845 0-10.579 2.922-10.579 6.526 0 3.3 3.945 6.007 9.055 6.473v-1.9c-3.442-.43-6.024-2.313-6.024-4.573 0-2.564 3.37-4.662 7.549-4.662 2.08 0 3.962.52 5.325 1.363l-1.937 1.202h6.043v-3.73l-1.972 1.22Zm-8.984-5.146v16.227l3.03-1.9V302l-3.03 1.954Z" style="fill:#fff;fill-opacity:1"/></svg>
|
After Width: | Height: | Size: 492 B |
|
@ -72,20 +72,44 @@
|
|||
width: 412px;
|
||||
|
||||
.auth-buttons {
|
||||
margin-top: $size-6;
|
||||
margin: $size-6 0 $size-4 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
column-gap: 17px;
|
||||
}
|
||||
|
||||
form {
|
||||
margin: 2rem 0;
|
||||
margin: 2rem 0 0.5rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-large {
|
||||
flex-grow: 1;
|
||||
font-size: 14px;
|
||||
font-family: sourcesanspro;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.btn-google-auth {
|
||||
background-color: #4285f4;
|
||||
color: $color-white;
|
||||
margin-bottom: $size-4;
|
||||
text-decoration: none;
|
||||
.logo {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
&:hover {
|
||||
background-color: #2065d7;
|
||||
color: $color-white;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-gitlab-auth {
|
||||
background-color: #fc6d26;
|
||||
color: $color-white;
|
||||
margin-bottom: $size-4;
|
||||
text-decoration: none;
|
||||
|
||||
|
@ -94,9 +118,16 @@
|
|||
height: 20px;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #ee5f18;
|
||||
color: $color-white;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-github-auth {
|
||||
background-color: #4c4c4c;
|
||||
color: $color-white;
|
||||
margin-bottom: $size-4;
|
||||
text-decoration: none;
|
||||
|
||||
|
@ -105,6 +136,15 @@
|
|||
height: 20px;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #2f2f2f;
|
||||
color: $color-white;
|
||||
}
|
||||
}
|
||||
|
||||
.link-oidc {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.separator {
|
||||
|
@ -112,6 +152,18 @@
|
|||
justify-content: center;
|
||||
width: 100%;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
|
||||
.text {
|
||||
margin: 0 10px;
|
||||
color: $color-gray-40;
|
||||
}
|
||||
|
||||
.line {
|
||||
border: 1px solid $color-gray-10;
|
||||
flex-grow: 10;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.links {
|
||||
|
|
|
@ -86,6 +86,9 @@
|
|||
(def browser (atom (parse-browser)))
|
||||
(def platform (atom (parse-platform)))
|
||||
|
||||
(def terms-of-service-uri (obj/get global "penpotTermsOfServiceURI" nil))
|
||||
(def privacy-policy-uri (obj/get global "penpotPrivacyPolicyURI" nil))
|
||||
|
||||
;; maintain for backward compatibility
|
||||
(let [login-with-ldap (obj/get global "penpotLoginWithLDAP" false)
|
||||
registration (obj/get global "penpotRegistrationEnabled" true)]
|
||||
|
@ -135,5 +138,3 @@
|
|||
(str (cond-> (u/join public-uri "assets/by-file-media-id/")
|
||||
(true? thumbnail?) (u/join (str id "/thumbnail"))
|
||||
(false? thumbnail?) (u/join (str id))))))
|
||||
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
(ns app.main.ui.auth
|
||||
(:require
|
||||
[app.config :as cf]
|
||||
[app.main.ui.auth.login :refer [login-page]]
|
||||
[app.main.ui.auth.recovery :refer [recovery-page]]
|
||||
[app.main.ui.auth.recovery-request :refer [recovery-request-page]]
|
||||
|
@ -15,6 +16,23 @@
|
|||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc terms-login
|
||||
[]
|
||||
(let [show-all? (and cf/terms-of-service-uri cf/privacy-policy-uri)
|
||||
show-terms? (some? cf/terms-of-service-uri)
|
||||
show-privacy? (some? cf/privacy-policy-uri)]
|
||||
|
||||
(when show-all?
|
||||
[:div.terms-login
|
||||
(when show-terms?
|
||||
[:a {:href cf/terms-of-service-uri :target "_blank"} "Terms of service"])
|
||||
|
||||
(when show-all?
|
||||
[:span "and"])
|
||||
|
||||
(when show-privacy?
|
||||
[:a {:href cf/privacy-policy-uri :target "_blank"} "Privacy policy"])])))
|
||||
|
||||
(mf/defc auth
|
||||
[{:keys [route] :as props}]
|
||||
(let [section (get-in route [:data :name])
|
||||
|
@ -48,7 +66,5 @@
|
|||
:auth-recovery
|
||||
[:& recovery-page {:params params}])
|
||||
|
||||
[:div.terms-login
|
||||
[:a {:href "https://penpot.app/terms.html" :target "_blank"} "Terms of service"]
|
||||
[:span "and"]
|
||||
[:a {:href "https://penpot.app/privacy.html" :target "_blank"} "Privacy policy"]]]]))
|
||||
[:& terms-login {}]]]))
|
||||
|
||||
|
|
|
@ -97,7 +97,6 @@
|
|||
(login-with-ldap event (with-meta params
|
||||
{:on-error on-error
|
||||
:on-success on-succes})))))]
|
||||
|
||||
[:*
|
||||
(when-let [message @error]
|
||||
[:& msgs/inline-banner
|
||||
|
@ -123,9 +122,10 @@
|
|||
:label (tr "auth.password")}]]
|
||||
|
||||
[:div.buttons-stack
|
||||
[:& fm/submit-button
|
||||
{:label (tr "auth.login-submit")
|
||||
:data-test "login-submit"}]
|
||||
(when (contains? @cf/flags :login)
|
||||
[:& fm/submit-button
|
||||
{:label (tr "auth.login-submit")
|
||||
:data-test "login-submit"}])
|
||||
|
||||
(when (contains? @cf/flags :login-with-ldap)
|
||||
[:& fm/submit-button
|
||||
|
@ -136,50 +136,69 @@
|
|||
[{:keys [params] :as props}]
|
||||
[:div.auth-buttons
|
||||
(when cf/google-client-id
|
||||
[:a.btn-ocean.btn-large.btn-google-auth
|
||||
[:a.btn-primary.btn-large.btn-google-auth
|
||||
{:on-click #(login-with-oauth % :google params)}
|
||||
[:span.logo i/brand-google]
|
||||
(tr "auth.login-with-google-submit")])
|
||||
|
||||
(when cf/gitlab-client-id
|
||||
[:a.btn-ocean.btn-large.btn-gitlab-auth
|
||||
{:on-click #(login-with-oauth % :gitlab params)}
|
||||
[:img.logo
|
||||
{:src "/images/icons/brand-gitlab.svg"}]
|
||||
(tr "auth.login-with-gitlab-submit")])
|
||||
|
||||
(when cf/github-client-id
|
||||
[:a.btn-ocean.btn-large.btn-github-auth
|
||||
[:a.btn-primary.btn-large.btn-github-auth
|
||||
{:on-click #(login-with-oauth % :github params)}
|
||||
[:img.logo
|
||||
{:src "/images/icons/brand-github.svg"}]
|
||||
[:span.logo i/brand-github]
|
||||
(tr "auth.login-with-github-submit")])
|
||||
|
||||
(when cf/gitlab-client-id
|
||||
[:a.btn-primary.btn-large.btn-gitlab-auth
|
||||
{:on-click #(login-with-oauth % :gitlab params)}
|
||||
[:span.logo i/brand-gitlab]
|
||||
(tr "auth.login-with-gitlab-submit")])
|
||||
|
||||
(when cf/oidc-client-id
|
||||
[:a.btn-ocean.btn-large.btn-github-auth
|
||||
[:a.btn-primary.btn-large.btn-github-auth
|
||||
{:on-click #(login-with-oauth % :oidc params)}
|
||||
[:span.logo i/brand-openid]
|
||||
(tr "auth.login-with-oidc-submit")])])
|
||||
|
||||
(mf/defc login-button-oidc
|
||||
[{:keys [params] :as props}]
|
||||
(when cf/oidc-client-id
|
||||
[:div.link-entry.link-oidc
|
||||
[:a {:on-click #(login-with-oauth % :oidc params)}
|
||||
(tr "auth.login-with-oidc-submit")]]))
|
||||
|
||||
(mf/defc login-page
|
||||
[{:keys [params] :as props}]
|
||||
[:div.generic-form.login-form
|
||||
[:div.form-container
|
||||
[:h1 {:data-test "login-title"} (tr "auth.login-title")]
|
||||
[:div.subtitle (tr "auth.login-subtitle")]
|
||||
|
||||
[:& login-form {:params params}]
|
||||
|
||||
(when show-alt-login-buttons?
|
||||
[:*
|
||||
[:span.separator (tr "labels.or")]
|
||||
[:span.separator
|
||||
[:span.line]
|
||||
[:span.text (tr "labels.continue-with")]
|
||||
[:span.line]]
|
||||
|
||||
[:div.buttons
|
||||
[:& login-buttons {:params params}]]])
|
||||
[:& login-buttons {:params params}]]
|
||||
|
||||
(when (or (contains? @cf/flags :login)
|
||||
(contains? @cf/flags :login-with-ldap))
|
||||
[:span.separator
|
||||
[:span.line]
|
||||
[:span.text (tr "labels.or")]
|
||||
[:span.line]])])
|
||||
|
||||
(when (or (contains? @cf/flags :login)
|
||||
(contains? @cf/flags :login-with-ldap))
|
||||
[:& login-form {:params params}])
|
||||
|
||||
[:div.links
|
||||
[:div.link-entry
|
||||
[:a {:on-click #(st/emit! (rt/nav :auth-recovery-request))
|
||||
:data-test "forgot-password"}
|
||||
(tr "auth.forgot-password")]]
|
||||
(when (contains? @cf/flags :login)
|
||||
[:div.link-entry
|
||||
[:a {:on-click #(st/emit! (rt/nav :auth-recovery-request))
|
||||
:data-test "forgot-password"}
|
||||
(tr "auth.forgot-password")]])
|
||||
|
||||
(when (contains? @cf/flags :registration)
|
||||
[:div.link-entry
|
||||
|
|
|
@ -121,15 +121,25 @@
|
|||
(when (contains? @cf/flags :demo-warning)
|
||||
[:& demo-warning])
|
||||
|
||||
(when login/show-alt-login-buttons?
|
||||
[:*
|
||||
[:span.separator
|
||||
[:span.line]
|
||||
[:span.text (tr "labels.continue-with")]
|
||||
[:span.line]]
|
||||
|
||||
[:div.buttons
|
||||
[:& login/login-buttons {:params params}]]
|
||||
|
||||
(when (or (contains? @cf/flags :login)
|
||||
(contains? @cf/flags :login-with-ldap))
|
||||
[:span.separator
|
||||
[:span.line]
|
||||
[:span.text (tr "labels.or")]
|
||||
[:span.line]])])
|
||||
|
||||
[:& 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") " "]
|
||||
|
|
|
@ -170,6 +170,10 @@
|
|||
(def uppercase (icon-xref :uppercase))
|
||||
(def user (icon-xref :user))
|
||||
|
||||
(def brand-openid (icon-xref :brand-openid))
|
||||
(def brand-github (icon-xref :brand-github))
|
||||
(def brand-gitlab (icon-xref :brand-gitlab))
|
||||
(def brand-google (icon-xref :brand-google))
|
||||
|
||||
(def loader-pencil
|
||||
(mf/html
|
||||
|
|
|
@ -56,10 +56,6 @@ msgstr "تسجيل الدخول هنا"
|
|||
msgid "auth.login-submit"
|
||||
msgstr "تسجيل الدخول"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-subtitle"
|
||||
msgstr "أدخل التفاصيل أدناه"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-title"
|
||||
msgstr "سعيد برؤيتك مجددا!"
|
||||
|
|
|
@ -59,10 +59,6 @@ msgstr "Inicieu la sessió aquí"
|
|||
msgid "auth.login-submit"
|
||||
msgstr "Entra"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-subtitle"
|
||||
msgstr "Introduïu les vostres dades a continuació"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-title"
|
||||
msgstr "Ens agrada tornar a veure-vos!"
|
||||
|
|
|
@ -59,10 +59,6 @@ msgstr "Log på her"
|
|||
msgid "auth.login-submit"
|
||||
msgstr "Log på"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-subtitle"
|
||||
msgstr "Indtast dine oplysninger nedenunder"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-title"
|
||||
msgstr "Fedt at se dig igen!"
|
||||
|
|
|
@ -59,10 +59,6 @@ msgstr "Hier einloggen"
|
|||
msgid "auth.login-submit"
|
||||
msgstr "Anmelden"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-subtitle"
|
||||
msgstr "Geben Sie unten Ihre Daten ein"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-title"
|
||||
msgstr "Schön, Sie wiederzusehen!"
|
||||
|
|
|
@ -54,10 +54,6 @@ msgstr "Συνδεθείτε εδώ"
|
|||
msgid "auth.login-submit"
|
||||
msgstr "Συνδεθείτε"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-subtitle"
|
||||
msgstr "Εισαγάγετε τα στοιχεία σας παρακάτω"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-title"
|
||||
msgstr "Χαίρομαι που σας ξαναδώ"
|
||||
|
|
|
@ -57,25 +57,21 @@ msgstr "Login here"
|
|||
msgid "auth.login-submit"
|
||||
msgstr "Login"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-subtitle"
|
||||
msgstr "Enter your details below"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-title"
|
||||
msgstr "Great to see you again!"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-with-github-submit"
|
||||
msgstr "Login with GitHub"
|
||||
msgstr "GitHub"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-with-gitlab-submit"
|
||||
msgstr "Login with Gitlab"
|
||||
msgstr "Gitlab"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-with-google-submit"
|
||||
msgstr "Login with Google"
|
||||
msgstr "Google"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-with-ldap-submit"
|
||||
|
@ -83,7 +79,7 @@ msgstr "Login with LDAP"
|
|||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-with-oidc-submit"
|
||||
msgstr "Login with OpenID (SSO)"
|
||||
msgstr "OpenID Connect"
|
||||
|
||||
#: src/app/main/ui/auth/recovery.cljs
|
||||
msgid "auth.new-password"
|
||||
|
@ -1231,6 +1227,9 @@ msgstr "Old password"
|
|||
msgid "labels.only-yours"
|
||||
msgstr "Only yours"
|
||||
|
||||
msgid "labels.continue-with"
|
||||
msgstr "Continue with"
|
||||
|
||||
msgid "labels.or"
|
||||
msgstr "or"
|
||||
|
||||
|
|
|
@ -59,25 +59,21 @@ msgstr "Entra aquí"
|
|||
msgid "auth.login-submit"
|
||||
msgstr "Entrar"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-subtitle"
|
||||
msgstr "Introduce tus datos aquí"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-title"
|
||||
msgstr "¡Un placer verte de nuevo!"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-with-github-submit"
|
||||
msgstr "Entrar con Github"
|
||||
msgstr "Github"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-with-gitlab-submit"
|
||||
msgstr "Entrar con Gitlab"
|
||||
msgstr "Gitlab"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-with-google-submit"
|
||||
msgstr "Entrar con Google"
|
||||
msgstr "Google"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-with-ldap-submit"
|
||||
|
@ -85,7 +81,7 @@ msgstr "Entrar con LDAP"
|
|||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-with-oidc-submit"
|
||||
msgstr "Entrar con OpenID (SSO)"
|
||||
msgstr "OpenID Connect"
|
||||
|
||||
#: src/app/main/ui/auth/recovery.cljs
|
||||
msgid "auth.new-password"
|
||||
|
@ -1232,6 +1228,9 @@ msgstr "Contraseña anterior"
|
|||
msgid "labels.only-yours"
|
||||
msgstr "Sólo los tuyos"
|
||||
|
||||
msgid "labels.continue-with"
|
||||
msgstr "Continúa con"
|
||||
|
||||
msgid "labels.or"
|
||||
msgstr "o"
|
||||
|
||||
|
|
|
@ -59,10 +59,6 @@ msgstr "Se connecter ici"
|
|||
msgid "auth.login-submit"
|
||||
msgstr "Se connecter"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-subtitle"
|
||||
msgstr "Entrez vos informations ci‑dessous"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-title"
|
||||
msgstr "Ravi de vous revoir !"
|
||||
|
|
|
@ -56,10 +56,6 @@ msgstr "כניסה מכאן"
|
|||
msgid "auth.login-submit"
|
||||
msgstr "כניסה"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-subtitle"
|
||||
msgstr "נא למלא את הפרטים שלך להלן"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-title"
|
||||
msgstr "שמחים לראות אותך שוב!"
|
||||
|
|
|
@ -63,10 +63,6 @@ msgstr "Masuk disini"
|
|||
msgid "auth.login-submit"
|
||||
msgstr "Masuk"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-subtitle"
|
||||
msgstr "Masukkan detail anda di bawah ini"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-title"
|
||||
msgstr "Senang bertemu denganmu lagi!"
|
||||
|
|
|
@ -59,10 +59,6 @@ msgstr "Entrar aqui"
|
|||
msgid "auth.login-submit"
|
||||
msgstr "Entrar"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-subtitle"
|
||||
msgstr "Insira seus dados abaixo"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-title"
|
||||
msgstr "Bom te ver de novo!"
|
||||
|
|
|
@ -60,10 +60,6 @@ msgstr "Conectează-te"
|
|||
msgid "auth.login-submit"
|
||||
msgstr "Intră în cont"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-subtitle"
|
||||
msgstr "Introduceți detaliile dvs. mai jos"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-title"
|
||||
msgstr "Mă bucur să te văd din nou!"
|
||||
|
|
|
@ -48,10 +48,6 @@ msgstr "Войти здесь"
|
|||
msgid "auth.login-submit"
|
||||
msgstr "Вход"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-subtitle"
|
||||
msgstr "Введите информацию о себе ниже"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-title"
|
||||
msgstr "Рады видеть Вас снова!"
|
||||
|
|
|
@ -59,10 +59,6 @@ msgstr "Buradan oturum açın"
|
|||
msgid "auth.login-submit"
|
||||
msgstr "Oturum aç"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-subtitle"
|
||||
msgstr "Bilgilerini aşağıdaki alana gir"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-title"
|
||||
msgstr "Seni tekrar görmek süper!"
|
||||
|
|
|
@ -55,10 +55,6 @@ msgstr "在这里登录"
|
|||
msgid "auth.login-submit"
|
||||
msgstr "登录"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-subtitle"
|
||||
msgstr "请在下面输入你的详细信息"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-title"
|
||||
msgstr "很高兴又见到你!"
|
||||
|
|
|
@ -55,10 +55,6 @@ msgstr "在此登入"
|
|||
msgid "auth.login-submit"
|
||||
msgstr "登入"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-subtitle"
|
||||
msgstr "在下方輸入您的詳細資訊"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs
|
||||
msgid "auth.login-title"
|
||||
msgstr "很高興再次見到你!"
|
||||
|
|
Loading…
Add table
Reference in a new issue