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 ### :sparkles: New features
- Set an artboard as the file thumbnail [Taiga #1526](https://tree.taiga.io/project/penpot/us/1526) - 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) - 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) - 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) - 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) - 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) - 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 - 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 ### :bug: Bugs fixed

View file

@ -90,7 +90,7 @@
(s/def ::flags ::us/set-of-keywords) (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 ::registration-enabled ::us/boolean)
(s/def ::smtp-enabled ::us/boolean) (s/def ::smtp-enabled ::us/boolean)
(s/def ::telemetry-enabled ::us/boolean) (s/def ::telemetry-enabled ::us/boolean)
@ -138,11 +138,15 @@
(s/def ::oidc-scopes ::us/set-of-str) (s/def ::oidc-scopes ::us/set-of-str)
(s/def ::oidc-roles ::us/set-of-str) (s/def ::oidc-roles ::us/set-of-str)
(s/def ::oidc-roles-attr ::us/keyword) (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 ::host ::us/string)
(s/def ::http-server-port ::us/integer) (s/def ::http-server-port ::us/integer)
(s/def ::http-server-host ::us/string) (s/def ::http-server-host ::us/string)
(s/def ::http-server-min-threads ::us/integer) (s/def ::http-server-max-body-size ::us/integer)
(s/def ::http-server-max-threads ::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-idle-max-age ::dt/duration)
(s/def ::http-session-updater-batch-max-age ::dt/duration) (s/def ::http-session-updater-batch-max-age ::dt/duration)
(s/def ::http-session-updater-batch-max-size ::us/integer) (s/def ::http-session-updater-batch-max-size ::us/integer)
@ -239,12 +243,16 @@
::oidc-user-uri ::oidc-user-uri
::oidc-scopes ::oidc-scopes
::oidc-roles-attr ::oidc-roles-attr
::oidc-email-attr
::oidc-name-attr
::oidc-roles ::oidc-roles
::host ::host
::http-server-host ::http-server-host
::http-server-port ::http-server-port
::http-server-max-threads ::http-server-max-body-size
::http-server-min-threads ::http-server-max-multipart-body-size
::http-server-io-threads
::http-server-worker-threads
::http-session-idle-max-age ::http-session-idle-max-age
::http-session-updater-batch-max-age ::http-session-updater-batch-max-age
::http-session-updater-batch-max-size ::http-session-updater-batch-max-size
@ -339,8 +347,8 @@
(when (ex/ex-info? e) (when (ex/ex-info? e)
(println ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;") (println ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;")
(println "Error on validating configuration:") (println "Error on validating configuration:")
(println (:explain (ex-data e)) (println (us/pretty-explain (ex-data e)))
(println ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"))) (println ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"))
(throw e)))) (throw e))))
(def version (def version

View file

@ -7,9 +7,7 @@
(ns app.http (ns app.http
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.spec :as us]
[app.http.doc :as doc] [app.http.doc :as doc]
[app.http.errors :as errors] [app.http.errors :as errors]
[app.http.middleware :as middleware] [app.http.middleware :as middleware]
@ -31,40 +29,46 @@
(s/def ::handler fn?) (s/def ::handler fn?)
(s/def ::router some?) (s/def ::router some?)
(s/def ::port ::us/integer) (s/def ::port integer?)
(s/def ::host ::us/string) (s/def ::host string?)
(s/def ::name ::us/string) (s/def ::name string?)
(s/def ::executors (s/map-of keyword? ::wrk/executor))
;; (s/def ::max-threads ::cf/http-server-max-threads) (s/def ::max-body-size integer?)
;; (s/def ::min-threads ::cf/http-server-min-threads) (s/def ::max-multipart-body-size integer?)
(s/def ::io-threads integer?)
(s/def ::worker-threads integer?)
(defmethod ig/prep-key ::server (defmethod ig/prep-key ::server
[_ cfg] [_ cfg]
(merge {:name "http" (merge {:name "http"
:port 6060 :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))) (d/without-nils cfg)))
(defmethod ig/pre-init-spec ::server [_] (defmethod ig/pre-init-spec ::server [_]
(s/keys :req-un [::port ::host ::name ::executors] (s/and
:opt-un [::router ::handler])) (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 (defmethod ig/init-key ::server
[_ {:keys [handler router port name host executors] :as cfg}] [_ {:keys [handler router port name host] :as cfg}]
(l/info :hint "starting http server" (l/info :hint "starting http server" :port port :host host :name name)
:port port :host host :name name)
(let [options {:http/port port (let [options {:http/port port
:http/host host :http/host host
:ring/async true :http/max-body-size (:max-body-size cfg)
:xnio/dispatch (:default executors)} :http/max-multipart-body-size (:max-multipart-body-size cfg)
handler (cond :xnio/io-threads (:io-threads cfg)
(fn? handler) handler :xnio/worker-threads (:worker-threads cfg)
(some? router) (wrap-router cfg router) :xnio/dispatch (:executor cfg)
:else (ex/raise :type :internal :ring/async true}
:code :invalid-argument handler (if (some? router)
:hint "Missing `handler` or `router` option.")) (wrap-router cfg router)
handler)
server (yt/server handler (d/without-nils options))] server (yt/server handler (d/without-nils options))]
(assoc cfg :server (yt/start! server)))) (assoc cfg :server (yt/start! server))))
@ -97,7 +101,7 @@
(handler request respond (handler request respond
(fn [cause] (fn [cause]
(l/error :hint "unexpected error processing request" (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) :query-string (yrq/query request)
:cause cause) :cause cause)
(respond (yrs/response 500 "internal server error"))))))) (respond (yrs/response 500 "internal server error")))))))

View file

@ -162,7 +162,8 @@
(let [context (dissoc report (let [context (dissoc report
:trace :cause :params :data :spec-problems :trace :cause :params :data :spec-problems
:spec-explain :spec-value :error :explain :hint) :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) :hint (:hint report)
:spec-explain (:spec-explain report) :spec-explain (:spec-explain report)
:spec-problems (:spec-problems report) :spec-problems (:spec-problems report)

View file

@ -21,27 +21,17 @@
(yrq/get-header request "x-real-ip") (yrq/get-header request "x-real-ip")
(yrq/remote-addr request))) (yrq/remote-addr request)))
(defn get-error-context (defn get-context
[request error] [request]
(let [data (ex-data error)] (merge
(merge {:path (:uri request)
{:path (:uri request) :method (:request-method request)
:method (:request-method request) :params (:params request)
:hint (ex-message error) :ip-addr (parse-client-ip request)
:params (:params request) :profile-id (:profile-id request)}
(let [headers (:headers request)]
:spec-problems (some->> data ::s/problems (take 10) seq vec) {:user-agent (get headers "user-agent")
:spec-value (some->> data ::s/value) :frontend-version (get headers "x-frontend-version" "unknown")})))
: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)}))))
(defmulti handle-exception (defmulti handle-exception
(fn [err & _rest] (fn [err & _rest]
@ -70,7 +60,7 @@
(let [edata (ex-data error) (let [edata (ex-data error)
explain (us/pretty-explain edata)] explain (us/pretty-explain edata)]
(l/error ::l/raw (ex-message error) (l/error ::l/raw (ex-message error)
::l/context (get-error-context request error) ::l/context (get-context request)
:cause error) :cause error)
(yrs/response :status 500 (yrs/response :status 500
:body {:type :server-error :body {:type :server-error
@ -96,7 +86,7 @@
(handle-exception (:handling edata) request) (handle-exception (:handling edata) request)
(do (do
(l/error ::l/raw (ex-message error) (l/error ::l/raw (ex-message error)
::l/context (get-error-context request error) ::l/context (get-context request)
:cause error) :cause error)
(yrs/response 500 {:type :server-error (yrs/response 500 {:type :server-error
:code :unexpected :code :unexpected
@ -107,7 +97,7 @@
[error request] [error request]
(let [state (.getSQLState ^java.sql.SQLException error)] (let [state (.getSQLState ^java.sql.SQLException error)]
(l/error ::l/raw (ex-message error) (l/error ::l/raw (ex-message error)
::l/context (get-error-context request error) ::l/context (get-context request)
:cause error) :cause error)
(cond (cond
(= state "57014") (= state "57014")

View file

@ -57,7 +57,8 @@
:grant_type "authorization_code" :grant_type "authorization_code"
:redirect_uri (build-redirect-uri cfg)} :redirect_uri (build-redirect-uri cfg)}
req {:method :post 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) :uri (:token-uri provider)
:body (u/map->query-string params)}] :body (u/map->query-string params)}]
(p/then (p/then
@ -69,42 +70,61 @@
:type (get data :token_type)}) :type (get data :token_type)})
(ex/raise :type :internal (ex/raise :type :internal
:code :unable-to-retrieve-token :code :unable-to-retrieve-token
::http-status status :http-status status
::http-body body)))))) :http-body body))))))
(defn- retrieve-user-info (defn- retrieve-user-info
[{:keys [provider http-client] :as cfg} tdata] [{:keys [provider http-client] :as cfg} tdata]
(p/then (letfn [(retrieve []
(http-client {:uri (:user-uri provider) (http-client {:uri (:user-uri provider)
:headers {"Authorization" (str (:type tdata) " " (:token tdata))} :headers {"Authorization" (str (:type tdata) " " (:token tdata))}
:timeout 6000 :timeout 6000
:method :get}) :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))}]
(when-not (s/valid? ::info info) (validate-response [{:keys [status body] :as res}]
(l/warn :hint "received incomplete profile info object (please set correct scopes)" (when-not (= 200 status)
:info (pr-str info)) (ex/raise :type :internal
(ex/raise :type :internal :code :unable-to-retrieve-user-info
:code :unable-to-auth :hint "unable to retrieve user info"
:hint "no user info")) :http-status status
info) :http-body body))
(ex/raise :type :internal res)
:code :unable-to-retrieve-user-info
::http-status status (get-email [info]
::http-body body))))) (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 ::backend ::us/not-empty-string)
(s/def ::email ::us/not-empty-string) (s/def ::email ::us/not-empty-string)
(s/def ::fullname ::us/not-empty-string) (s/def ::fullname ::us/not-empty-string)
(s/def ::props (s/map-of ::us/keyword any?)) (s/def ::props (s/map-of ::us/keyword any?))
(s/def ::info (s/def ::info
(s/keys :req-un [::backend (s/keys :req-un [::backend
::email ::email
@ -112,7 +132,7 @@
::props])) ::props]))
(defn retrieve-info (defn retrieve-info
[{:keys [tokens provider] :as cfg} request] [{:keys [tokens provider] :as cfg} {:keys [params] :as request}]
(letfn [(validate-oidc [info] (letfn [(validate-oidc [info]
;; If the provider is OIDC, we can proceed to check ;; If the provider is OIDC, we can proceed to check
;; roles if they are defined. ;; roles if they are defined.
@ -143,9 +163,15 @@
(map? (:props state)) (map? (:props state))
(update :props merge (:props state))))] (update :props merge (:props state))))]
(let [state (get-in request [:params :state]) (when-let [error (get params :error)]
state (tokens :verify {:token state :iss :oauth}) (ex/raise :type :internal
code (get-in request [:params :code])] :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/resolved code)
(p/then #(retrieve-access-token cfg %)) (p/then #(retrieve-access-token cfg %))
(p/then #(retrieve-user-info cfg %)) (p/then #(retrieve-user-info cfg %))
@ -224,15 +250,18 @@
(redirect-response uri)))) (redirect-response uri))))
(defn- auth-handler (defn- auth-handler
[{:keys [tokens] :as cfg} {:keys [params] :as request} respond _] [{:keys [tokens] :as cfg} {:keys [params] :as request} respond raise]
(let [props (extract-utm-props params) (try
state (tokens :generate (let [props (extract-utm-props params)
{:iss :oauth state (tokens :generate
:invitation-token (:invitation-token params) {:iss :oauth
:props props :invitation-token (:invitation-token params)
:exp (dt/in-future "15m")}) :props props
uri (build-auth-uri cfg state)] :exp (dt/in-future "15m")})
(respond (yrs/response 200 {:redirect-uri uri})))) uri (build-auth-uri cfg state)]
(respond (yrs/response 200 {:redirect-uri uri})))
(catch Throwable cause
(raise cause))))
(defn- callback-handler (defn- callback-handler
[cfg request respond _] [cfg request respond _]
@ -242,7 +271,7 @@
(generate-redirect cfg request info profile))) (generate-redirect cfg request info profile)))
(handle-error [cause] (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)))] (respond (generate-error-redirect cfg cause)))]
(-> (process-request) (-> (process-request)
@ -385,17 +414,16 @@
(assoc-in cfg [:providers "github"] opts)) (assoc-in cfg [:providers "github"] opts))
cfg))) cfg)))
(defn- initialize-gitlab-provider (defn- initialize-gitlab-provider
[cfg] [cfg]
(let [base (cf/get :gitlab-base-uri "https://gitlab.com") (let [base (cf/get :gitlab-base-uri "https://gitlab.com")
opts {:base-uri base opts {:base-uri base
:client-id (cf/get :gitlab-client-id) :client-id (cf/get :gitlab-client-id)
:client-secret (cf/get :gitlab-client-secret) :client-secret (cf/get :gitlab-client-secret)
:scopes #{"read_user"} :scopes #{"openid" "profile" "email"}
:auth-uri (str base "/oauth/authorize") :auth-uri (str base "/oauth/authorize")
:token-uri (str base "/oauth/token") :token-uri (str base "/oauth/token")
:user-uri (str base "/api/v4/user") :user-uri (str base "/oauth/userinfo")
:name "gitlab"}] :name "gitlab"}]
(if (and (string? (:client-id opts)) (if (and (string? (:client-id opts))
(string? (:client-secret opts))) (string? (:client-secret opts)))

View file

@ -254,7 +254,7 @@
"select * from audit_log "select * from audit_log
where archived_at is null where archived_at is null
order by created_at asc order by created_at asc
limit 1000 limit 256
for update skip locked;") for update skip locked;")
(defn archive-events (defn archive-events
@ -298,8 +298,9 @@
(if (= (:status resp) 204) (if (= (:status resp) 204)
true true
(do (do
(l/warn :hint "unable to archive events" (l/error :hint "unable to archive events"
:resp-status (:status resp)) :resp-status (:status resp)
:resp-body (:body resp))
false)))) false))))
(mark-as-archived [conn rows] (mark-as-archived [conn rows]

View file

@ -113,7 +113,8 @@
:host (cf/get :http-server-host) :host (cf/get :http-server-host)
:router (ig/ref :app.http/router) :router (ig/ref :app.http/router)
:metrics (ig/ref :app.metrics/metrics) :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 :app.http/router
{:assets (ig/ref :app.http.assets/handlers) {:assets (ig/ref :app.http.assets/handlers)

View file

@ -262,10 +262,3 @@
:gauge (make-gauge props) :gauge (make-gauge props)
:summary (make-summary props) :summary (make-summary props)
:histogram (make-histogram 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.queries.profile :as profile]
[app.rpc.rlimit :as rlimit] [app.rpc.rlimit :as rlimit]
[app.storage :as sto] [app.storage :as sto]
[app.util.async :as async]
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]
[buddy.hashers :as hashers] [buddy.hashers :as hashers]
@ -101,8 +100,14 @@
(sv/defmethod ::prepare-register-profile {:auth false} (sv/defmethod ::prepare-register-profile {:auth false}
[{:keys [pool tokens] :as cfg} params] [{:keys [pool tokens] :as cfg} params]
(when-not (contains? cf/flags :registration) (when-not (contains? cf/flags :registration)
(ex/raise :type :restriction (if-not (contains? params :invitation-token)
:code :registration-disabled)) (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-let [domains (cf/get :registration-domain-whitelist)]
(when-not (email-domain-in-whitelist? domains (:email params)) (when-not (email-domain-in-whitelist? domains (:email params))
@ -129,6 +134,7 @@
:backend "penpot" :backend "penpot"
:iss :prepared-register :iss :prepared-register
:exp (dt/in-future "48h")} :exp (dt/in-future "48h")}
token (tokens :generate params)] token (tokens :generate params)]
{:token token})) {:token token}))
@ -149,7 +155,6 @@
[{:keys [conn tokens session] :as cfg} {:keys [token] :as params}] [{:keys [conn tokens session] :as cfg} {:keys [token] :as params}]
(let [claims (tokens :verify {:token token :iss :prepared-register}) (let [claims (tokens :verify {:token token :iss :prepared-register})
params (merge params claims)] params (merge params claims)]
(check-profile-existence! conn params) (check-profile-existence! conn params)
(let [is-active (or (:is-active params) (let [is-active (or (:is-active params)
@ -158,10 +163,8 @@
(create-profile conn) (create-profile conn)
(create-profile-relations conn) (create-profile-relations conn)
(decode-profile-row)) (decode-profile-row))
invitation (when-let [token (:invitation-token params)] invitation (when-let [token (:invitation-token params)]
(tokens :verify {:token token :iss :team-invitation}))] (tokens :verify {:token token :iss :team-invitation}))]
(cond (cond
;; If invitation token comes in params, this is because the user comes from team-invitation process; ;; 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 ;; 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])) :opt-un [::scope ::invitation-token]))
(sv/defmethod ::login (sv/defmethod ::login
{:auth false {:auth false ::rlimit/permits (cf/get :rlimit-password)}
::async/dispatch :default
::rlimit/permits (cf/get :rlimit-password)}
[{:keys [pool session tokens] :as cfg} {:keys [email password] :as params}] [{: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] (letfn [(check-password [profile password]
(when (= (:password profile) "!") (when (= (:password profile) "!")
(ex/raise :type :validation (ex/raise :type :validation

View file

@ -259,7 +259,7 @@
;; A task responsible to permanently delete already marked as deleted ;; A task responsible to permanently delete already marked as deleted
;; storage files. The storage objects are practically never marked to ;; storage files. The storage objects are practically never marked to
;; be deleted directly by the api call. The touched-gc is responsible ;; 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) (declare sql:retrieve-deleted-objects-chunk)

View file

@ -7,6 +7,7 @@
(ns app.services-profile-test (ns app.services-profile-test
(:require (:require
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db] [app.db :as db]
[app.rpc.mutations.profile :as profile] [app.rpc.mutations.profile :as profile]
[app.test-helpers :as th] [app.test-helpers :as th]
@ -195,6 +196,56 @@
(t/is (nil? error)))) (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 (t/deftest prepare-register-with-registration-disabled
(th/with-mocks {#'app.config/flags nil} (th/with-mocks {#'app.config/flags nil}
(let [data {::th/type :prepare-register-profile (let [data {::th/type :prepare-register-profile

View file

@ -313,6 +313,14 @@
[v] [v]
(instance? clojure.lang.ExceptionInfo 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? (defn ex-of-type?
[e type] [e type]
(let [data (ex-data e)] (let [data (ex-data e)]

View file

@ -23,11 +23,12 @@
::cause])) ::cause]))
(defn error (defn error
[& {:keys [hint cause ::data] :as params}] [& {:keys [hint cause ::data type] :as params}]
(s/assert ::error-params params) (s/assert ::error-params params)
(let [payload (-> params (let [payload (-> params
(dissoc :cause ::data) (dissoc :cause ::data)
(merge data))] (merge data))
hint (or hint (pr-str type))]
(ex-info hint payload cause))) (ex-info hint payload cause)))
(defmacro raise (defmacro raise

View file

@ -12,7 +12,7 @@
(def default (def default
"A common flags that affects both: backend and frontend." "A common flags that affects both: backend and frontend."
[:enable-registration [:enable-registration
:enable-demo-users]) :enable-login])
(defn parse (defn parse
[& flags] [& flags]

View file

@ -8,8 +8,10 @@
(:require (:require
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.common.spec :as us]
[clojure.pprint :refer [pprint]] [clojure.pprint :refer [pprint]]
[cuerdas.core :as str] [cuerdas.core :as str]
[clojure.spec.alpha :as s]
[fipp.edn :as fpp] [fipp.edn :as fpp]
#?(:clj [io.aviso.exception :as ie]) #?(:clj [io.aviso.exception :as ie])
#?(:cljs [goog.log :as glog])) #?(:cljs [goog.log :as glog]))
@ -152,6 +154,18 @@
[logger level] [logger level]
(.isEnabled ^Logger logger ^Level 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 (defmacro log
[& {:keys [level cause ::logger ::async ::raw ::context] :or {async true} :as props}] [& {:keys [level cause ::logger ::async ::raw ::context] :or {async true} :as props}]
(if (:ns &env) ; CLJS (if (:ns &env) ; CLJS
@ -169,7 +183,9 @@
~(if async ~(if async
`(send-off logging-agent `(send-off logging-agent
(fn [_#] (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)) (->> (or ~raw (build-map-message ~props))
(write-log! ~logger-sym ~level-sym ~cause))))) (write-log! ~logger-sym ~level-sym ~cause)))))

View file

@ -140,22 +140,26 @@
;; --- SPEC: set of Keywords ;; --- SPEC: set of Keywords
(s/def ::set-of-keywords (letfn [(conform-fn [dest s]
(s/conformer (let [xform (keep (fn [s]
(fn [s] (cond
(let [xform (comp (string? s) (keyword s)
(map (fn [s] (keyword? s) s
(cond :else nil)))]
(string? s) (keyword s) (cond
(keyword? s) s (set? s) (into dest xform s)
:else nil))) (string? s) (into dest xform (str/words s))
(filter identity))] :else ::s/invalid)))]
(cond
(set? s) (into #{} xform s) (s/def ::set-of-keywords
(string? s) (into #{} xform (str/words s)) (s/conformer
:else ::s/invalid))) (fn [s] (conform-fn #{} s))
(fn [s] (fn [s] (str/join " " (map name 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 ;; --- 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"> <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>
<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>

Before

Width:  |  Height:  |  Size: 954 B

After

Width:  |  Height:  |  Size: 650 B

Before After
Before After

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; width: 412px;
.auth-buttons { .auth-buttons {
margin-top: $size-6; margin: $size-6 0 $size-4 0;
display: flex;
justify-content: center;
column-gap: 17px;
} }
form { 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 { .btn-google-auth {
background-color: #4285f4;
color: $color-white;
margin-bottom: $size-4; margin-bottom: $size-4;
text-decoration: none; text-decoration: none;
.logo {
width: 20px;
height: 20px;
margin-right: 1rem;
}
&:hover {
background-color: #2065d7;
color: $color-white;
}
} }
.btn-gitlab-auth { .btn-gitlab-auth {
background-color: #fc6d26;
color: $color-white;
margin-bottom: $size-4; margin-bottom: $size-4;
text-decoration: none; text-decoration: none;
@ -94,9 +118,16 @@
height: 20px; height: 20px;
margin-right: 1rem; margin-right: 1rem;
} }
&:hover {
background-color: #ee5f18;
color: $color-white;
}
} }
.btn-github-auth { .btn-github-auth {
background-color: #4c4c4c;
color: $color-white;
margin-bottom: $size-4; margin-bottom: $size-4;
text-decoration: none; text-decoration: none;
@ -105,6 +136,15 @@
height: 20px; height: 20px;
margin-right: 1rem; margin-right: 1rem;
} }
&:hover {
background-color: #2f2f2f;
color: $color-white;
}
}
.link-oidc {
text-align: center;
} }
.separator { .separator {
@ -112,6 +152,18 @@
justify-content: center; justify-content: center;
width: 100%; width: 100%;
text-transform: uppercase; 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 { .links {

View file

@ -86,6 +86,9 @@
(def browser (atom (parse-browser))) (def browser (atom (parse-browser)))
(def platform (atom (parse-platform))) (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 ;; maintain for backward compatibility
(let [login-with-ldap (obj/get global "penpotLoginWithLDAP" false) (let [login-with-ldap (obj/get global "penpotLoginWithLDAP" false)
registration (obj/get global "penpotRegistrationEnabled" true)] registration (obj/get global "penpotRegistrationEnabled" true)]
@ -135,5 +138,3 @@
(str (cond-> (u/join public-uri "assets/by-file-media-id/") (str (cond-> (u/join public-uri "assets/by-file-media-id/")
(true? thumbnail?) (u/join (str id "/thumbnail")) (true? thumbnail?) (u/join (str id "/thumbnail"))
(false? thumbnail?) (u/join (str id)))))) (false? thumbnail?) (u/join (str id))))))

View file

@ -6,6 +6,7 @@
(ns app.main.ui.auth (ns app.main.ui.auth
(:require (:require
[app.config :as cf]
[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]]
@ -15,6 +16,23 @@
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[rumext.alpha :as mf])) [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 (mf/defc auth
[{:keys [route] :as props}] [{:keys [route] :as props}]
(let [section (get-in route [:data :name]) (let [section (get-in route [:data :name])
@ -48,7 +66,5 @@
:auth-recovery :auth-recovery
[:& recovery-page {:params params}]) [:& recovery-page {:params params}])
[:div.terms-login [:& 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"]]]]))

View file

@ -97,7 +97,6 @@
(login-with-ldap event (with-meta params (login-with-ldap event (with-meta params
{:on-error on-error {:on-error on-error
:on-success on-succes})))))] :on-success on-succes})))))]
[:* [:*
(when-let [message @error] (when-let [message @error]
[:& msgs/inline-banner [:& msgs/inline-banner
@ -123,9 +122,10 @@
:label (tr "auth.password")}]] :label (tr "auth.password")}]]
[:div.buttons-stack [:div.buttons-stack
[:& fm/submit-button (when (contains? @cf/flags :login)
{:label (tr "auth.login-submit") [:& fm/submit-button
:data-test "login-submit"}] {:label (tr "auth.login-submit")
:data-test "login-submit"}])
(when (contains? @cf/flags :login-with-ldap) (when (contains? @cf/flags :login-with-ldap)
[:& fm/submit-button [:& fm/submit-button
@ -136,50 +136,69 @@
[{:keys [params] :as props}] [{:keys [params] :as props}]
[:div.auth-buttons [:div.auth-buttons
(when cf/google-client-id (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)} {:on-click #(login-with-oauth % :google params)}
[:span.logo i/brand-google]
(tr "auth.login-with-google-submit")]) (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 (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)} {:on-click #(login-with-oauth % :github params)}
[:img.logo [:span.logo i/brand-github]
{:src "/images/icons/brand-github.svg"}]
(tr "auth.login-with-github-submit")]) (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 (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)} {:on-click #(login-with-oauth % :oidc params)}
[:span.logo i/brand-openid]
(tr "auth.login-with-oidc-submit")])]) (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 (mf/defc login-page
[{:keys [params] :as props}] [{:keys [params] :as props}]
[:div.generic-form.login-form [:div.generic-form.login-form
[:div.form-container [:div.form-container
[:h1 {:data-test "login-title"} (tr "auth.login-title")] [:h1 {:data-test "login-title"} (tr "auth.login-title")]
[:div.subtitle (tr "auth.login-subtitle")]
[:& login-form {:params params}]
(when show-alt-login-buttons? (when show-alt-login-buttons?
[:* [:*
[:span.separator (tr "labels.or")] [:span.separator
[:span.line]
[:span.text (tr "labels.continue-with")]
[:span.line]]
[:div.buttons [: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.links
[:div.link-entry (when (contains? @cf/flags :login)
[:a {:on-click #(st/emit! (rt/nav :auth-recovery-request)) [:div.link-entry
:data-test "forgot-password"} [:a {:on-click #(st/emit! (rt/nav :auth-recovery-request))
(tr "auth.forgot-password")]] :data-test "forgot-password"}
(tr "auth.forgot-password")]])
(when (contains? @cf/flags :registration) (when (contains? @cf/flags :registration)
[:div.link-entry [:div.link-entry

View file

@ -121,15 +121,25 @@
(when (contains? @cf/flags :demo-warning) (when (contains? @cf/flags :demo-warning)
[:& 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}] [:& 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.links
[:div.link-entry [:div.link-entry
[:span (tr "auth.already-have-account") " "] [:span (tr "auth.already-have-account") " "]

View file

@ -170,6 +170,10 @@
(def uppercase (icon-xref :uppercase)) (def uppercase (icon-xref :uppercase))
(def user (icon-xref :user)) (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 (def loader-pencil
(mf/html (mf/html

View file

@ -56,10 +56,6 @@ msgstr "تسجيل الدخول هنا"
msgid "auth.login-submit" msgid "auth.login-submit"
msgstr "تسجيل الدخول" msgstr "تسجيل الدخول"
#: src/app/main/ui/auth/login.cljs
msgid "auth.login-subtitle"
msgstr "أدخل التفاصيل أدناه"
#: src/app/main/ui/auth/login.cljs #: src/app/main/ui/auth/login.cljs
msgid "auth.login-title" msgid "auth.login-title"
msgstr "سعيد برؤيتك مجددا!" msgstr "سعيد برؤيتك مجددا!"

View file

@ -59,10 +59,6 @@ msgstr "Inicieu la sessió aquí"
msgid "auth.login-submit" msgid "auth.login-submit"
msgstr "Entra" 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 #: src/app/main/ui/auth/login.cljs
msgid "auth.login-title" msgid "auth.login-title"
msgstr "Ens agrada tornar a veure-vos!" msgstr "Ens agrada tornar a veure-vos!"

View file

@ -59,10 +59,6 @@ msgstr "Log på her"
msgid "auth.login-submit" msgid "auth.login-submit"
msgstr "Log på" 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 #: src/app/main/ui/auth/login.cljs
msgid "auth.login-title" msgid "auth.login-title"
msgstr "Fedt at se dig igen!" msgstr "Fedt at se dig igen!"

View file

@ -59,10 +59,6 @@ msgstr "Hier einloggen"
msgid "auth.login-submit" msgid "auth.login-submit"
msgstr "Anmelden" 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 #: src/app/main/ui/auth/login.cljs
msgid "auth.login-title" msgid "auth.login-title"
msgstr "Schön, Sie wiederzusehen!" msgstr "Schön, Sie wiederzusehen!"

View file

@ -54,10 +54,6 @@ msgstr "Συνδεθείτε εδώ"
msgid "auth.login-submit" msgid "auth.login-submit"
msgstr "Συνδεθείτε" msgstr "Συνδεθείτε"
#: src/app/main/ui/auth/login.cljs
msgid "auth.login-subtitle"
msgstr "Εισαγάγετε τα στοιχεία σας παρακάτω"
#: src/app/main/ui/auth/login.cljs #: src/app/main/ui/auth/login.cljs
msgid "auth.login-title" msgid "auth.login-title"
msgstr "Χαίρομαι που σας ξαναδώ" msgstr "Χαίρομαι που σας ξαναδώ"

View file

@ -57,25 +57,21 @@ msgstr "Login here"
msgid "auth.login-submit" msgid "auth.login-submit"
msgstr "Login" 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 #: src/app/main/ui/auth/login.cljs
msgid "auth.login-title" msgid "auth.login-title"
msgstr "Great to see you again!" msgstr "Great to see you again!"
#: src/app/main/ui/auth/login.cljs #: src/app/main/ui/auth/login.cljs
msgid "auth.login-with-github-submit" msgid "auth.login-with-github-submit"
msgstr "Login with GitHub" msgstr "GitHub"
#: src/app/main/ui/auth/login.cljs #: src/app/main/ui/auth/login.cljs
msgid "auth.login-with-gitlab-submit" msgid "auth.login-with-gitlab-submit"
msgstr "Login with Gitlab" msgstr "Gitlab"
#: src/app/main/ui/auth/login.cljs #: src/app/main/ui/auth/login.cljs
msgid "auth.login-with-google-submit" msgid "auth.login-with-google-submit"
msgstr "Login with Google" msgstr "Google"
#: src/app/main/ui/auth/login.cljs #: src/app/main/ui/auth/login.cljs
msgid "auth.login-with-ldap-submit" msgid "auth.login-with-ldap-submit"
@ -83,7 +79,7 @@ msgstr "Login with LDAP"
#: src/app/main/ui/auth/login.cljs #: src/app/main/ui/auth/login.cljs
msgid "auth.login-with-oidc-submit" msgid "auth.login-with-oidc-submit"
msgstr "Login with OpenID (SSO)" msgstr "OpenID Connect"
#: src/app/main/ui/auth/recovery.cljs #: src/app/main/ui/auth/recovery.cljs
msgid "auth.new-password" msgid "auth.new-password"
@ -1231,6 +1227,9 @@ msgstr "Old password"
msgid "labels.only-yours" msgid "labels.only-yours"
msgstr "Only yours" msgstr "Only yours"
msgid "labels.continue-with"
msgstr "Continue with"
msgid "labels.or" msgid "labels.or"
msgstr "or" msgstr "or"

View file

@ -59,25 +59,21 @@ msgstr "Entra aquí"
msgid "auth.login-submit" msgid "auth.login-submit"
msgstr "Entrar" 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 #: src/app/main/ui/auth/login.cljs
msgid "auth.login-title" msgid "auth.login-title"
msgstr "¡Un placer verte de nuevo!" msgstr "¡Un placer verte de nuevo!"
#: src/app/main/ui/auth/login.cljs #: src/app/main/ui/auth/login.cljs
msgid "auth.login-with-github-submit" msgid "auth.login-with-github-submit"
msgstr "Entrar con Github" msgstr "Github"
#: src/app/main/ui/auth/login.cljs #: src/app/main/ui/auth/login.cljs
msgid "auth.login-with-gitlab-submit" msgid "auth.login-with-gitlab-submit"
msgstr "Entrar con Gitlab" msgstr "Gitlab"
#: src/app/main/ui/auth/login.cljs #: src/app/main/ui/auth/login.cljs
msgid "auth.login-with-google-submit" msgid "auth.login-with-google-submit"
msgstr "Entrar con Google" msgstr "Google"
#: src/app/main/ui/auth/login.cljs #: src/app/main/ui/auth/login.cljs
msgid "auth.login-with-ldap-submit" msgid "auth.login-with-ldap-submit"
@ -85,7 +81,7 @@ msgstr "Entrar con LDAP"
#: src/app/main/ui/auth/login.cljs #: src/app/main/ui/auth/login.cljs
msgid "auth.login-with-oidc-submit" msgid "auth.login-with-oidc-submit"
msgstr "Entrar con OpenID (SSO)" msgstr "OpenID Connect"
#: src/app/main/ui/auth/recovery.cljs #: src/app/main/ui/auth/recovery.cljs
msgid "auth.new-password" msgid "auth.new-password"
@ -1232,6 +1228,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.continue-with"
msgstr "Continúa con"
msgid "labels.or" msgid "labels.or"
msgstr "o" msgstr "o"

View file

@ -59,10 +59,6 @@ msgstr "Se connecter ici"
msgid "auth.login-submit" msgid "auth.login-submit"
msgstr "Se connecter" 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 #: src/app/main/ui/auth/login.cljs
msgid "auth.login-title" msgid "auth.login-title"
msgstr "Ravi de vous revoir!" msgstr "Ravi de vous revoir!"

View file

@ -56,10 +56,6 @@ msgstr "כניסה מכאן"
msgid "auth.login-submit" msgid "auth.login-submit"
msgstr "כניסה" msgstr "כניסה"
#: src/app/main/ui/auth/login.cljs
msgid "auth.login-subtitle"
msgstr "נא למלא את הפרטים שלך להלן"
#: src/app/main/ui/auth/login.cljs #: src/app/main/ui/auth/login.cljs
msgid "auth.login-title" msgid "auth.login-title"
msgstr "שמחים לראות אותך שוב!" msgstr "שמחים לראות אותך שוב!"

View file

@ -63,10 +63,6 @@ msgstr "Masuk disini"
msgid "auth.login-submit" msgid "auth.login-submit"
msgstr "Masuk" 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 #: src/app/main/ui/auth/login.cljs
msgid "auth.login-title" msgid "auth.login-title"
msgstr "Senang bertemu denganmu lagi!" msgstr "Senang bertemu denganmu lagi!"

View file

@ -59,10 +59,6 @@ msgstr "Entrar aqui"
msgid "auth.login-submit" msgid "auth.login-submit"
msgstr "Entrar" 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 #: src/app/main/ui/auth/login.cljs
msgid "auth.login-title" msgid "auth.login-title"
msgstr "Bom te ver de novo!" msgstr "Bom te ver de novo!"

View file

@ -60,10 +60,6 @@ msgstr "Conectează-te"
msgid "auth.login-submit" msgid "auth.login-submit"
msgstr "Intră în cont" 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 #: src/app/main/ui/auth/login.cljs
msgid "auth.login-title" msgid "auth.login-title"
msgstr "Mă bucur să te văd din nou!" msgstr "Mă bucur să te văd din nou!"

View file

@ -48,10 +48,6 @@ msgstr "Войти здесь"
msgid "auth.login-submit" msgid "auth.login-submit"
msgstr "Вход" msgstr "Вход"
#: src/app/main/ui/auth/login.cljs
msgid "auth.login-subtitle"
msgstr "Введите информацию о себе ниже"
#: src/app/main/ui/auth/login.cljs #: src/app/main/ui/auth/login.cljs
msgid "auth.login-title" msgid "auth.login-title"
msgstr "Рады видеть Вас снова!" msgstr "Рады видеть Вас снова!"

View file

@ -59,10 +59,6 @@ msgstr "Buradan oturum açın"
msgid "auth.login-submit" msgid "auth.login-submit"
msgstr "Oturum aç" 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 #: src/app/main/ui/auth/login.cljs
msgid "auth.login-title" msgid "auth.login-title"
msgstr "Seni tekrar görmek süper!" msgstr "Seni tekrar görmek süper!"

View file

@ -55,10 +55,6 @@ msgstr "在这里登录"
msgid "auth.login-submit" msgid "auth.login-submit"
msgstr "登录" msgstr "登录"
#: src/app/main/ui/auth/login.cljs
msgid "auth.login-subtitle"
msgstr "请在下面输入你的详细信息"
#: src/app/main/ui/auth/login.cljs #: src/app/main/ui/auth/login.cljs
msgid "auth.login-title" msgid "auth.login-title"
msgstr "很高兴又见到你!" msgstr "很高兴又见到你!"

View file

@ -55,10 +55,6 @@ msgstr "在此登入"
msgid "auth.login-submit" msgid "auth.login-submit"
msgstr "登入" msgstr "登入"
#: src/app/main/ui/auth/login.cljs
msgid "auth.login-subtitle"
msgstr "在下方輸入您的詳細資訊"
#: src/app/main/ui/auth/login.cljs #: src/app/main/ui/auth/login.cljs
msgid "auth.login-title" msgid "auth.login-title"
msgstr "很高興再次見到你!" msgstr "很高興再次見到你!"