0
Fork 0
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:
Andrey Antukh 2022-03-11 17:22:03 +01:00 committed by GitHub
commit 1e580638d2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 418 additions and 264 deletions

View file

@ -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

View file

@ -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

View file

@ -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")))))))

View file

@ -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)

View file

@ -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")

View file

@ -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)))

View file

@ -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]

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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)]

View file

@ -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

View file

@ -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]

View file

@ -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)))))

View file

@ -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

View file

@ -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

View 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

View 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

View file

@ -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 {

View file

@ -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))))))

View file

@ -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 {}]]]))

View file

@ -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

View file

@ -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") " "]

View file

@ -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

View file

@ -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 "سعيد برؤيتك مجددا!"

View file

@ -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!"

View file

@ -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!"

View file

@ -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!"

View file

@ -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 "Χαίρομαι που σας ξαναδώ"

View file

@ -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"

View file

@ -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"

View file

@ -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 cidessous"
#: src/app/main/ui/auth/login.cljs
msgid "auth.login-title"
msgstr "Ravi de vous revoir!"

View file

@ -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 "שמחים לראות אותך שוב!"

View file

@ -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!"

View file

@ -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!"

View file

@ -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!"

View file

@ -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 "Рады видеть Вас снова!"

View file

@ -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!"

View file

@ -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 "很高兴又见到你!"

View file

@ -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 "很高興再次見到你!"