mirror of
https://github.com/penpot/penpot.git
synced 2025-04-09 13:31:23 -05:00
✨ Add mvp access-token support
This commit is contained in:
parent
b90aef4e1d
commit
890583a13a
23 changed files with 907 additions and 100 deletions
CHANGES.md
backend
frontend
|
@ -8,6 +8,7 @@
|
|||
- Default naming of text layers [Taiga #2836](https://tree.taiga.io/project/penpot/us/2836)
|
||||
- Create typography style from a selected text layer[Taiga #3041](https://tree.taiga.io/project/penpot/us/3041)
|
||||
- Board as ruler origin [Taiga #4833](https://tree.taiga.io/project/penpot/us/4833)
|
||||
- Access tokens support [Taiga #4460](https://tree.taiga.io/project/penpot/us/4460)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
|
|
|
@ -26,12 +26,18 @@
|
|||
(when token
|
||||
(tokens/verify props {:token token :iss "access-token"})))
|
||||
|
||||
(defn- get-token-perms
|
||||
(def sql:get-token-data
|
||||
"SELECT perms, profile_id, expires_at
|
||||
FROM access_token
|
||||
WHERE id = ?
|
||||
AND (expires_at IS NULL
|
||||
OR (expires_at > now()));")
|
||||
|
||||
(defn- get-token-data
|
||||
[pool token-id]
|
||||
(when-not (db/read-only? pool)
|
||||
(when-let [token (db/get* pool :access-token {:id token-id} {:columns [:perms]})]
|
||||
(some-> (:perms token)
|
||||
(db/decode-pgarray #{})))))
|
||||
(some-> (db/exec-one! pool [sql:get-token-data token-id])
|
||||
(update :perms db/decode-pgarray #{}))))
|
||||
|
||||
(defn- wrap-soft-auth
|
||||
"Soft Authentication, will be executed synchronously on the undertow
|
||||
|
@ -56,10 +62,14 @@
|
|||
"Authorization middleware, will be executed synchronously on vthread."
|
||||
[handler {:keys [::db/pool]}]
|
||||
(fn [request]
|
||||
(let [perms (some->> (::id request) (get-token-perms pool))]
|
||||
(let [{:keys [perms profile-id expires-at]} (some->> (::id request) (get-token-data pool))]
|
||||
(handler (cond-> request
|
||||
(some? perms)
|
||||
(assoc ::perms perms))))))
|
||||
(assoc ::perms perms)
|
||||
(some? profile-id)
|
||||
(assoc ::profile-id profile-id)
|
||||
(some? expires-at)
|
||||
(assoc ::expires-at expires-at))))))
|
||||
|
||||
(def soft-auth
|
||||
{:name ::soft-auth
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.http :as-alias http]
|
||||
[app.http.access-token :as-alias actoken]
|
||||
[app.http.client :as http.client]
|
||||
[app.loggers.audit.tasks :as-alias tasks]
|
||||
[app.loggers.webhooks :as-alias webhooks]
|
||||
|
@ -152,7 +153,11 @@
|
|||
(dissoc :profile-id)
|
||||
(dissoc :type)))
|
||||
|
||||
(clean-props))]
|
||||
(clean-props))
|
||||
|
||||
token-id (::actoken/id request)
|
||||
context (d/without-nils
|
||||
{:access-token-id (some-> token-id str)})]
|
||||
|
||||
{::type (or (::type resultm)
|
||||
(::rpc/type cfg))
|
||||
|
@ -161,6 +166,7 @@
|
|||
::profile-id profile-id
|
||||
::ip-addr (some-> request parse-client-ip)
|
||||
::props props
|
||||
::context context
|
||||
|
||||
;; NOTE: for batch-key lookup we need the params as-is
|
||||
;; because the rpc api does not need to know the
|
||||
|
@ -188,6 +194,7 @@
|
|||
:type (::type event)
|
||||
:profile-id (::profile-id event)
|
||||
:ip-addr (::ip-addr event)
|
||||
:context (::context event)
|
||||
:props (::props event)}]
|
||||
|
||||
(when (contains? cf/flags :audit-log)
|
||||
|
@ -201,6 +208,7 @@
|
|||
(db/insert! conn-or-pool :audit-log
|
||||
(-> params
|
||||
(update :props db/tjson)
|
||||
(update :context db/tjson)
|
||||
(update :ip-addr db/inet)
|
||||
(assoc :created-at now)
|
||||
(assoc :tracked-at now)
|
||||
|
|
|
@ -315,7 +315,8 @@
|
|||
{:name "0101-mod-server-error-report-table"
|
||||
:fn (mg/resource "app/migrations/sql/0101-mod-server-error-report-table.sql")}
|
||||
|
||||
])
|
||||
{:name "0102-mod-access-token-table"
|
||||
:fn (mg/resource "app/migrations/sql/0102-mod-access-token-table.sql")}])
|
||||
|
||||
(defn apply-migrations!
|
||||
[pool name migrations]
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE access_token
|
||||
ADD COLUMN expires_at timestamptz NULL;
|
|
@ -112,22 +112,6 @@
|
|||
:hint "authentication required for this endpoint")
|
||||
(f cfg params)))))
|
||||
|
||||
(defn- wrap-access-token
|
||||
"Wraps service method with access token validation."
|
||||
[_ f {:keys [::sv/name] :as mdata}]
|
||||
(if (contains? cf/flags :access-tokens)
|
||||
(fn [cfg params]
|
||||
(let [request (::http/request params)]
|
||||
(if (contains? request ::actoken/id)
|
||||
(let [perms (::actoken/perms request #{})]
|
||||
(if (contains? perms name)
|
||||
(f cfg params)
|
||||
(ex/raise :type :authorization
|
||||
:code :operation-not-allowed
|
||||
:allowed perms)))
|
||||
(f cfg params))))
|
||||
f))
|
||||
|
||||
(defn- wrap-audit
|
||||
[_ f mdata]
|
||||
(if (or (contains? cf/flags :webhooks)
|
||||
|
@ -157,8 +141,7 @@
|
|||
(rlimit/wrap cfg $ mdata)
|
||||
(wrap-audit cfg $ mdata)
|
||||
(wrap-spec-conform cfg $ mdata)
|
||||
(wrap-authentication cfg $ mdata)
|
||||
(wrap-access-token cfg $ mdata)))
|
||||
(wrap-authentication cfg $ mdata)))
|
||||
|
||||
(defn- wrap
|
||||
[cfg f mdata]
|
||||
|
|
|
@ -19,18 +19,19 @@
|
|||
[clojure.spec.alpha :as s]))
|
||||
|
||||
(defn- decode-row
|
||||
[{:keys [perms] :as row}]
|
||||
(cond-> row
|
||||
(db/pgarray? perms "text")
|
||||
(assoc :perms (db/decode-pgarray perms #{}))))
|
||||
[row]
|
||||
(dissoc row :perms))
|
||||
|
||||
(defn- create-access-token
|
||||
[{:keys [::conn ::main/props]} profile-id name perms]
|
||||
(defn create-access-token
|
||||
[{:keys [::db/conn ::main/props]} profile-id name expiration]
|
||||
(let [created-at (dt/now)
|
||||
token-id (uuid/next)
|
||||
token (tokens/generate props {:iss "access-token"
|
||||
:tid token-id
|
||||
:iat created-at})]
|
||||
:iat created-at})
|
||||
|
||||
expires-at (some-> expiration dt/in-future)]
|
||||
|
||||
(db/insert! conn :access-token
|
||||
{:id token-id
|
||||
:name name
|
||||
|
@ -38,33 +39,36 @@
|
|||
:profile-id profile-id
|
||||
:created-at created-at
|
||||
:updated-at created-at
|
||||
:perms (db/create-array conn "text" perms)})))
|
||||
:expires-at expires-at
|
||||
:perms (db/create-array conn "text" [])})))
|
||||
|
||||
|
||||
(defn repl-create-access-token
|
||||
[{:keys [::db/pool] :as system} profile-id name perms]
|
||||
[{:keys [::db/pool] :as system} profile-id name expiration]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [props (:app.setup/props system)]
|
||||
(create-access-token {::conn conn ::main/props props}
|
||||
(create-access-token {::db/conn conn ::main/props props}
|
||||
profile-id
|
||||
name
|
||||
perms))))
|
||||
expiration))))
|
||||
|
||||
(s/def ::name ::us/not-empty-string)
|
||||
(s/def ::perms ::us/set-of-strings)
|
||||
(s/def ::expiration ::dt/duration)
|
||||
|
||||
(s/def ::create-access-token
|
||||
(s/keys :req [::rpc/profile-id]
|
||||
:req-un [::name ::perms]))
|
||||
:req-un [::name]
|
||||
:opt-un [::expiration]))
|
||||
|
||||
(sv/defmethod ::create-access-token
|
||||
{::doc/added "1.18"}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id name perms]}]
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id name expiration]}]
|
||||
(db/with-atomic [conn pool]
|
||||
(let [cfg (assoc cfg ::conn conn)]
|
||||
(let [cfg (assoc cfg ::db/conn conn)]
|
||||
(quotes/check-quote! conn
|
||||
{::quotes/id ::quotes/access-tokens-per-profile
|
||||
::quotes/profile-id profile-id})
|
||||
(-> (create-access-token cfg profile-id name perms)
|
||||
(-> (create-access-token cfg profile-id name expiration)
|
||||
(decode-row)))))
|
||||
|
||||
(s/def ::delete-access-token
|
||||
|
@ -83,5 +87,8 @@
|
|||
(sv/defmethod ::get-access-tokens
|
||||
{::doc/added "1.18"}
|
||||
[{:keys [::db/pool]} {:keys [::rpc/profile-id]}]
|
||||
(->> (db/query pool :access-token {:profile-id profile-id})
|
||||
(->> (db/query pool :access-token
|
||||
{:profile-id profile-id}
|
||||
{:order-by [[:expires-at :asc] [:created-at :asc]]
|
||||
:columns [:id :name :perms :created-at :updated-at :expires-at]})
|
||||
(mapv decode-row)))
|
||||
|
|
|
@ -491,6 +491,7 @@
|
|||
([key default]
|
||||
(get data key (get cf/config key default)))))
|
||||
|
||||
|
||||
(defn reset-mock!
|
||||
[m]
|
||||
(swap! m (fn [m]
|
||||
|
|
|
@ -19,12 +19,14 @@
|
|||
(t/use-fixtures :each th/database-reset)
|
||||
|
||||
(t/deftest access-tokens-crud
|
||||
(let [prof (th/create-profile* 1 {:is-active true})
|
||||
team-id (:default-team-id prof)
|
||||
proj-id (:default-project-id prof)
|
||||
atoken (atom nil)]
|
||||
(let [prof (th/create-profile* 1 {:is-active true})
|
||||
team-id (:default-team-id prof)
|
||||
proj-id (:default-project-id prof)
|
||||
atoken-no-expiration (atom nil)
|
||||
atoken-future-expiration (atom nil)
|
||||
atoken-past-expiration (atom nil)]
|
||||
|
||||
(t/testing "create access token"
|
||||
(t/testing "create access token without expiration date"
|
||||
(let [params {::th/type :create-access-token
|
||||
::rpc/profile-id (:id prof)
|
||||
:name "token 1"
|
||||
|
@ -34,32 +36,65 @@
|
|||
(t/is (nil? (:error out)))
|
||||
|
||||
(let [result (:result out)]
|
||||
(reset! atoken result)
|
||||
(reset! atoken-no-expiration result)
|
||||
(t/is (contains? result :id))
|
||||
(t/is (contains? result :created-at))
|
||||
(t/is (contains? result :updated-at))
|
||||
(t/is (contains? result :token))
|
||||
(t/is (contains? result :perms)))))
|
||||
(t/is (contains? result :token)))))
|
||||
|
||||
(t/testing "get access token"
|
||||
(t/testing "create access token with expiration date in the future"
|
||||
(let [params {::th/type :create-access-token
|
||||
::rpc/profile-id (:id prof)
|
||||
:name "token 1"
|
||||
:perms ["get-profile"]
|
||||
:expiration "130h"}
|
||||
out (th/command! params)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
|
||||
(let [result (:result out)]
|
||||
(reset! atoken-past-expiration result)
|
||||
(t/is (contains? result :id))
|
||||
(t/is (contains? result :created-at))
|
||||
(t/is (contains? result :updated-at))
|
||||
(t/is (contains? result :expires-at))
|
||||
(t/is (contains? result :token)))))
|
||||
|
||||
(t/testing "create access token with expiration date in the past"
|
||||
(let [params {::th/type :create-access-token
|
||||
::rpc/profile-id (:id prof)
|
||||
:name "token 1"
|
||||
:perms ["get-profile"]
|
||||
:expiration "-130h"}
|
||||
out (th/command! params)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
|
||||
(let [result (:result out)]
|
||||
(reset! atoken-future-expiration result)
|
||||
(t/is (contains? result :id))
|
||||
(t/is (contains? result :created-at))
|
||||
(t/is (contains? result :updated-at))
|
||||
(t/is (contains? result :expires-at))
|
||||
(t/is (contains? result :token)))))
|
||||
|
||||
(t/testing "get access tokens"
|
||||
(let [params {::th/type :get-access-tokens
|
||||
::rpc/profile-id (:id prof)}
|
||||
out (th/command! params)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
(let [[result :as results] (:result out)]
|
||||
(t/is (= 1 (count results)))
|
||||
(t/is (= 3 (count results)))
|
||||
(t/is (contains? result :id))
|
||||
(t/is (contains? result :created-at))
|
||||
(t/is (contains? result :updated-at))
|
||||
(t/is (contains? result :token))
|
||||
(t/is (contains? result :perms))
|
||||
(t/is (= @atoken result)))))
|
||||
(t/is (not (contains? result :token))))))
|
||||
|
||||
(t/testing "delete access token"
|
||||
(let [params {::th/type :delete-access-token
|
||||
::rpc/profile-id (:id prof)
|
||||
:id (:id @atoken)}
|
||||
:id (:id @atoken-no-expiration)}
|
||||
out (th/command! params)]
|
||||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
|
@ -72,5 +107,4 @@
|
|||
;; (th/print-result! out)
|
||||
(t/is (nil? (:error out)))
|
||||
(let [results (:result out)]
|
||||
(t/is (= 0 (count results))))))
|
||||
))
|
||||
(t/is (= 2 (count results))))))))
|
||||
|
|
4
frontend/resources/images/icons/icon-key.svg
Normal file
4
frontend/resources/images/icons/icon-key.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
|
||||
<svg width="500" height="500" viewBox="0 0 132.292 132.292" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m 124.4936,4.1479733 c 2.35562,2.3556156 2.35562,6.1748267 0,8.5304757 l -7.79862,7.798611 13.83031,13.83063 c 2.35561,2.355624 2.35561,6.17481 0,8.530434 l -21.11215,21.111814 c -2.35563,2.355624 -6.17473,2.355624 -8.53035,0 L 87.052489,50.119308 70.486062,66.685653 c 1.854567,2.508174 3.40751,5.234628 4.621289,8.123967 2.002817,4.767224 3.042881,9.883616 3.060161,15.054661 0.01736,5.170963 -0.988386,10.293969 -2.959206,15.074669 -1.97082,4.7807 -4.867849,9.12402 -8.524233,12.78024 -3.656384,3.65704 -7.999943,6.55341 -12.780562,8.52456 -4.780618,1.97115 -9.903873,2.97657 -15.074752,2.95921 -5.17088,-0.0174 -10.287272,-1.05751 -15.054578,-3.06008 -4.767307,-2.00258 -9.091352,-4.92871 -12.723181,-8.60973 l -0.04523,-0.0455 C 3.8638091,110.09256 -0.08784955,100.18884 0.00148225,89.908103 0.09081323,79.628273 4.2142341,69.794432 11.483678,62.525071 18.753039,55.255627 28.586797,51.132252 38.866875,51.042872 c 8.280816,-0.07193 16.317554,2.478324 22.995239,7.205861 L 115.96324,4.1479733 c 2.35563,-2.3556231 6.17473,-2.3556231 8.53036,0 z M 58.230947,70.879639 c -0.05614,-0.05036 -0.111622,-0.101947 -0.166275,-0.154781 -5.119368,-4.944413 -11.975896,-7.680376 -19.092955,-7.618529 -7.116977,0.06185 -13.92497,2.916541 -18.957605,7.949176 -5.032717,5.032634 -7.88733,11.840628 -7.949176,18.957604 -0.06177,7.10656 2.665849,13.952671 7.596205,19.069891 2.509661,2.53753 5.49516,4.55498 8.785675,5.93743 3.300437,1.38659 6.842553,2.10675 10.422375,2.11832 3.57982,0.0124 7.126733,-0.68378 10.436348,-2.04804 3.309696,-1.36509 6.316776,-3.37014 8.848099,-5.90188 2.531325,-2.53091 4.536954,-5.53807 5.90138,-8.84785 1.364427,-3.309781 2.060695,-6.85686 2.048706,-10.436185 -0.01199,-3.58015 -0.731987,-7.122185 -2.118572,-10.422622 -1.348965,-3.210892 -3.302835,-6.131319 -5.754205,-8.602534 z m 37.351893,-29.290765 9.56554,9.565371 12.58097,-12.58138 -9.56554,-9.565371 z"/>
|
||||
</svg>
|
After (image error) Size: 2 KiB |
|
@ -156,3 +156,148 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dashboard-access-tokens {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.access-tokens-hero-container {
|
||||
max-width: 1000px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.access-tokens-hero {
|
||||
font-size: $fs14;
|
||||
padding: $size-6;
|
||||
background-color: $color-white;
|
||||
margin-top: $size-6;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.desc {
|
||||
width: 80%;
|
||||
color: $color-gray-40;
|
||||
h2 {
|
||||
margin-bottom: $size-4;
|
||||
color: $color-black;
|
||||
}
|
||||
p {
|
||||
font-size: $fs16;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.access-tokens-empty {
|
||||
text-align: center;
|
||||
max-width: 1000px;
|
||||
width: 100%;
|
||||
padding: $size-6;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 1px dashed $color-gray-20;
|
||||
color: $color-gray-40;
|
||||
margin-top: 12px;
|
||||
min-height: 136px;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
background-color: $color-white;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 43% 12px;
|
||||
height: 63px;
|
||||
&:not(:first-child) {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-field {
|
||||
&.name {
|
||||
color: $color-gray-60;
|
||||
}
|
||||
|
||||
&.expiration-date {
|
||||
color: $color-gray-40;
|
||||
font-size: $fs14;
|
||||
.content {
|
||||
padding: 2px 5px;
|
||||
&.expired {
|
||||
background-color: $color-warning-lighter;
|
||||
border-radius: $br4;
|
||||
color: $color-gray-40;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.access-token-created {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
&.actions {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.access-tokens-modal {
|
||||
.action-buttons {
|
||||
gap: 10px;
|
||||
|
||||
.cancel-button {
|
||||
border: 1px solid $color-gray-30;
|
||||
background: $color-canvas;
|
||||
border-radius: $br3;
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
margin-right: 8px;
|
||||
|
||||
&:hover {
|
||||
background: $color-gray-20;
|
||||
}
|
||||
}
|
||||
}
|
||||
.access-token-created {
|
||||
position: relative;
|
||||
word-break: break-all;
|
||||
|
||||
.custom-input input {
|
||||
background-color: $color-success-lighter;
|
||||
border: 0;
|
||||
padding: 0 0 0 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.help-icon {
|
||||
border: none;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
cursor: pointer;
|
||||
background-color: $color-success-lighter;
|
||||
|
||||
svg {
|
||||
fill: $color-gray-30;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
svg {
|
||||
fill: $color-gray-60;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.token-created-info {
|
||||
font-size: $fs12;
|
||||
color: $color-gray-40;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -337,6 +337,10 @@ textarea {
|
|||
border: 1px solid $color-gray-20;
|
||||
height: 40px;
|
||||
|
||||
&.focus {
|
||||
border-color: $color-gray-60;
|
||||
}
|
||||
|
||||
&.invalid {
|
||||
border-color: $color-danger;
|
||||
label {
|
||||
|
|
|
@ -525,4 +525,56 @@
|
|||
(->> (rp/cmd! :create-demo-profile {})
|
||||
(rx/map login)))))
|
||||
|
||||
;; --- EVENT: fetch-team-webhooks
|
||||
|
||||
(defn access-tokens-fetched
|
||||
[access-tokens]
|
||||
(ptk/reify ::access-tokens-fetched
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc state :access-tokens access-tokens))))
|
||||
|
||||
(defn fetch-access-tokens
|
||||
[]
|
||||
(ptk/reify ::fetch-access-tokens
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rp/command! :get-access-tokens)
|
||||
(rx/map access-tokens-fetched)))))
|
||||
|
||||
;; --- EVENT: create-access-token
|
||||
|
||||
(defn access-token-created
|
||||
[access-token]
|
||||
(ptk/reify ::access-token-created
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc state :access-token-created access-token))))
|
||||
|
||||
(defn create-access-token
|
||||
[{:keys [] :as params}]
|
||||
(ptk/reify ::create-access-token
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(let [{:keys [on-success on-error]
|
||||
:or {on-success identity
|
||||
on-error rx/throw}} (meta params)]
|
||||
(->> (rp/command! :create-access-token params)
|
||||
(rx/map access-token-created)
|
||||
(rx/tap on-success)
|
||||
(rx/catch on-error))))))
|
||||
|
||||
;; --- EVENT: delete-access-token
|
||||
|
||||
(defn delete-access-token
|
||||
[{:keys [id] :as params}]
|
||||
(us/assert! ::us/uuid id)
|
||||
(ptk/reify ::delete-access-token
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(let [{:keys [on-success on-error]
|
||||
:or {on-success identity
|
||||
on-error rx/throw}} (meta params)]
|
||||
(->> (rp/command! :delete-access-token params)
|
||||
(rx/tap on-success)
|
||||
(rx/catch on-error))))))
|
||||
|
|
|
@ -55,7 +55,8 @@
|
|||
(:settings-profile
|
||||
:settings-password
|
||||
:settings-options
|
||||
:settings-feedback)
|
||||
:settings-feedback
|
||||
:settings-access-tokens)
|
||||
[:& settings/settings {:route route}]
|
||||
|
||||
:debug-icons-preview
|
||||
|
|
|
@ -191,25 +191,38 @@
|
|||
[:span.hint hint])]]))
|
||||
|
||||
(mf/defc select
|
||||
[{:keys [options label form default data-test] :as props
|
||||
[{:keys [options disabled label form default data-test] :as props
|
||||
:or {default ""}}]
|
||||
(let [input-name (get props :name)
|
||||
form (or form (mf/use-ctx form-ctx))
|
||||
value (or (get-in @form [:data input-name]) default)
|
||||
cvalue (d/seek #(= value (:value %)) options)
|
||||
on-change (fn [event]
|
||||
(let [target (dom/get-target event)
|
||||
value (dom/get-value target)]
|
||||
(fm/on-input-change form input-name value)))]
|
||||
form (or form (mf/use-ctx form-ctx))
|
||||
value (or (get-in @form [:data input-name]) default)
|
||||
cvalue (d/seek #(= value (:value %)) options)
|
||||
focus? (mf/use-state false)
|
||||
on-change
|
||||
(fn [event]
|
||||
(let [target (dom/get-target event)
|
||||
value (dom/get-value target)]
|
||||
(fm/on-input-change form input-name value)))
|
||||
|
||||
on-focus
|
||||
(fn [_]
|
||||
(reset! focus? true))
|
||||
|
||||
on-blur
|
||||
(fn [_]
|
||||
(reset! focus? false))]
|
||||
|
||||
[:div.custom-select
|
||||
[:select {:value value
|
||||
:on-change on-change
|
||||
:on-focus on-focus
|
||||
:on-blur on-blur
|
||||
:disabled disabled
|
||||
:data-test data-test}
|
||||
(for [item options]
|
||||
[:option {:key (:value item) :value (:value item)} (:label item)])]
|
||||
|
||||
[:div.input-container
|
||||
[:div.input-container {:class (dom/classnames :disabled disabled :focus @focus?)}
|
||||
[:div.main-content
|
||||
[:label label]
|
||||
[:span.value (:label cvalue "")]]
|
||||
|
|
|
@ -150,6 +150,7 @@
|
|||
(def justify-content-row-center (icon-xref :justify-content-row-center))
|
||||
(def justify-content-row-end (icon-xref :justify-content-row-end))
|
||||
(def justify-content-row-start (icon-xref :justify-content-row-start))
|
||||
(def icon-key (icon-xref :icon-key))
|
||||
(def layers (icon-xref :layers))
|
||||
(def layout-columns (icon-xref :layout-columns))
|
||||
(def layout-rows (icon-xref :layout-rows))
|
||||
|
|
|
@ -33,19 +33,20 @@
|
|||
|
||||
(def routes
|
||||
[["/auth"
|
||||
["/login" :auth-login]
|
||||
["/register" :auth-register]
|
||||
["/login" :auth-login]
|
||||
["/register" :auth-register]
|
||||
["/register/validate" :auth-register-validate]
|
||||
["/register/success" :auth-register-success]
|
||||
["/recovery/request" :auth-recovery-request]
|
||||
["/recovery" :auth-recovery]
|
||||
["/verify-token" :auth-verify-token]]
|
||||
["/register/success" :auth-register-success]
|
||||
["/recovery/request" :auth-recovery-request]
|
||||
["/recovery" :auth-recovery]
|
||||
["/verify-token" :auth-verify-token]]
|
||||
|
||||
["/settings"
|
||||
["/profile" :settings-profile]
|
||||
["/password" :settings-password]
|
||||
["/feedback" :settings-feedback]
|
||||
["/options" :settings-options]]
|
||||
["/profile" :settings-profile]
|
||||
["/password" :settings-password]
|
||||
["/feedback" :settings-feedback]
|
||||
["/options" :settings-options]
|
||||
["/access-tokens" :settings-access-tokens]]
|
||||
|
||||
["/view/:file-id"
|
||||
{:name :viewer
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
(:require
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.settings.access-tokens :refer [access-tokens-page]]
|
||||
[app.main.ui.settings.change-email]
|
||||
[app.main.ui.settings.delete-account]
|
||||
[app.main.ui.settings.feedback :refer [feedback-page]]
|
||||
|
@ -55,5 +56,8 @@
|
|||
[:& password-page {:locale locale}]
|
||||
|
||||
:settings-options
|
||||
[:& options-page {:locale locale}])]]]))
|
||||
[:& options-page {:locale locale}]
|
||||
|
||||
:settings-access-tokens
|
||||
[:& access-tokens-page])]]]))
|
||||
|
||||
|
|
278
frontend/src/app/main/ui/settings/access_tokens.cljs
Normal file
278
frontend/src/app/main/ui/settings/access_tokens.cljs
Normal file
|
@ -0,0 +1,278 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.settings.access-tokens
|
||||
(:require
|
||||
[app.common.spec :as us]
|
||||
[app.main.data.messages :as dm]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.context-menu-a11y.context-menu-a11y :refer [context-menu-a11y]]
|
||||
[app.main.ui.components.forms :as fm]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.time :as dt]
|
||||
[app.util.webapi :as wapi]
|
||||
[cljs.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def tokens-ref
|
||||
(l/derived :access-tokens st/state))
|
||||
|
||||
(def token-created-ref
|
||||
(l/derived :access-token-created st/state))
|
||||
|
||||
(s/def ::name ::us/not-empty-string)
|
||||
(s/def ::expiration-date ::us/not-empty-string)
|
||||
(s/def ::access-token-form
|
||||
(s/keys :req-un [::name ::expiration-date]))
|
||||
|
||||
(defn- name-validator
|
||||
[errors data]
|
||||
(let [name (:name data)]
|
||||
(cond-> errors
|
||||
(str/blank? name)
|
||||
(assoc :name {:message (tr "dashboard.access-tokens.errors-required-name")}))))
|
||||
|
||||
|
||||
(def initial-data
|
||||
{:name "" :expiration-date "never"})
|
||||
|
||||
(mf/defc access-token-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :access-token}
|
||||
[]
|
||||
(let [form (fm/use-form
|
||||
:initial initial-data
|
||||
:spec ::access-token-form
|
||||
:validators [name-validator])
|
||||
created (mf/deref token-created-ref)
|
||||
created? (mf/use-state false)
|
||||
locale (mf/deref i18n/locale)
|
||||
|
||||
on-success
|
||||
(mf/use-fn
|
||||
(mf/deps created)
|
||||
(fn [_]
|
||||
(let [message (tr "dashboard.access-tokens.create.success")]
|
||||
(st/emit! (du/fetch-access-tokens)
|
||||
(dm/success message)
|
||||
(reset! created? true)))))
|
||||
|
||||
on-close
|
||||
(mf/use-fn
|
||||
(mf/deps created)
|
||||
(fn [_]
|
||||
(reset! created? false)
|
||||
(st/emit! (modal/hide))))
|
||||
|
||||
on-error
|
||||
(mf/use-fn
|
||||
(fn [_]
|
||||
(st/emit! (dm/error (tr "errors.generic"))
|
||||
(modal/hide))))
|
||||
|
||||
on-submit
|
||||
(mf/use-fn
|
||||
(fn [form]
|
||||
(let [cdata (:clean-data @form)
|
||||
mdata {:on-success (partial on-success form)
|
||||
:on-error (partial on-error form)}
|
||||
expiration (:expiration-date cdata)
|
||||
params (cond-> {:name (:name cdata)
|
||||
:perms (:perms cdata)}
|
||||
(not= "never" expiration) (assoc :expiration expiration))]
|
||||
(st/emit! (du/create-access-token
|
||||
(with-meta params mdata))))))
|
||||
|
||||
copy-token
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(wapi/write-to-clipboard (:token created))
|
||||
(st/emit! (dm/show {:type :info
|
||||
:content (tr "dashboard.access-tokens.copied-success")
|
||||
:timeout 1000})))]
|
||||
|
||||
[:div.modal-overlay
|
||||
[:div.modal-container.access-tokens-modal
|
||||
[:& fm/form {:form form :on-submit on-submit}
|
||||
|
||||
[:div.modal-header
|
||||
[:div.modal-header-title
|
||||
[:h2 (tr "modals.create-access-token.title")]]
|
||||
|
||||
[:div.modal-close-button
|
||||
{:on-click on-close} i/close]]
|
||||
|
||||
[:div.modal-content.generic-form
|
||||
[:div.fields-container
|
||||
[:div.fields-row
|
||||
[:& fm/input {:type "text"
|
||||
:auto-focus? true
|
||||
:form form
|
||||
:name :name
|
||||
:disabled @created?
|
||||
:label (tr "modals.create-access-token.name.label")
|
||||
:placeholder (tr "modals.create-access-token.name.placeholder")}]]
|
||||
|
||||
[:div.fields-row
|
||||
[:& fm/select {:options [{:label (tr "dashboard.access-tokens.expiration-never") :value "never" :key "never"}
|
||||
{:label (tr "dashboard.access-tokens.expiration-30-days") :value "720h" :key "720h"}
|
||||
{:label (tr "dashboard.access-tokens.expiration-60-days") :value "1440h" :key "1440h"}
|
||||
{:label (tr "dashboard.access-tokens.expiration-90-days") :value "2160h" :key "2160h"}
|
||||
{:label (tr "dashboard.access-tokens.expiration-180-days") :value "4320h" :key "4320h"}]
|
||||
:label (tr "modals.create-access-token.expiration-date.label")
|
||||
:default "never"
|
||||
:disabled @created?
|
||||
:name :expiration-date}]
|
||||
(when @created?
|
||||
[:span.token-created-info
|
||||
(if (:expires-at created)
|
||||
(tr "dashboard.access-tokens.token-will-expire" (dt/format-date-locale (:expires-at created) {:locale locale}))
|
||||
(tr "dashboard.access-tokens.token-will-not-expire"))])]
|
||||
|
||||
[:div.fields-row.access-token-created
|
||||
(when @created?
|
||||
[:div.custom-input.with-icon
|
||||
[:input {:type "text"
|
||||
:value (:token created "")
|
||||
:placeholder (tr "modals.create-access-token.token")
|
||||
:read-only true}]
|
||||
[:button.help-icon {:title (tr "modals.create-access-token.copy-token")
|
||||
:on-click copy-token}
|
||||
|
||||
i/copy]])]]]
|
||||
|
||||
[:div.modal-footer
|
||||
[:div.action-buttons
|
||||
(if @created?
|
||||
[:input.cancel-button
|
||||
{:type "button"
|
||||
:value (tr "labels.close")
|
||||
:on-click #(modal/hide!)}]
|
||||
[:*
|
||||
[:input.cancel-button
|
||||
{:type "button"
|
||||
:value (tr "labels.cancel")
|
||||
:on-click #(modal/hide!)}]
|
||||
[:& fm/submit-button
|
||||
{:label (tr "modals.create-access-token.submit-label")}]])]]]]]))
|
||||
|
||||
(mf/defc access-tokens-hero
|
||||
[]
|
||||
(let [on-click (mf/use-fn #(st/emit! (modal/show :access-token {})))]
|
||||
[:div.access-tokens-hero-container
|
||||
[:div.access-tokens-hero
|
||||
[:div.desc
|
||||
[:h2 (tr "dashboard.access-tokens.personal")]
|
||||
[:p (tr "dashboard.access-tokens.personal.description")]]
|
||||
|
||||
[:button.btn-primary
|
||||
{:on-click on-click}
|
||||
[:span (tr "dashboard.access-tokens.create")]]]]))
|
||||
|
||||
(mf/defc access-token-actions
|
||||
[{:keys [on-delete] :as props}]
|
||||
(let [local (mf/use-state {:menu-open false})
|
||||
show? (:menu-open @local)
|
||||
menu-ref (mf/use-ref)
|
||||
options [{:option-name (tr "labels.delete")
|
||||
:id "access-token-delete"
|
||||
:option-handler on-delete}]
|
||||
|
||||
on-menu-close
|
||||
(mf/use-fn
|
||||
#(swap! local assoc :menu-open false))
|
||||
|
||||
on-menu-click
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(swap! local assoc :menu-open true)))]
|
||||
|
||||
[:div.icon
|
||||
{:tab-index "0"
|
||||
:ref menu-ref
|
||||
:on-click on-menu-click
|
||||
:on-key-down (fn [event]
|
||||
(when (kbd/enter? event)
|
||||
(dom/stop-propagation event)
|
||||
(on-menu-click event)))}
|
||||
i/actions
|
||||
[:& context-menu-a11y
|
||||
{:on-close on-menu-close
|
||||
:show show?
|
||||
:fixed? true
|
||||
:min-width? true
|
||||
:top "auto"
|
||||
:left "auto"
|
||||
:options options}]]))
|
||||
|
||||
(mf/defc access-token-item
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [token] :as props}]
|
||||
(let [locale (mf/deref i18n/locale)
|
||||
expires-at (:expires-at token)
|
||||
expires-txt (some-> expires-at (dt/format-date-locale {:locale locale}))
|
||||
expired? (and (some? expires-at) (> (dt/now) expires-at))
|
||||
|
||||
delete-fn
|
||||
(mf/use-fn
|
||||
(mf/deps token)
|
||||
(fn []
|
||||
(let [params {:id (:id token)}
|
||||
mdata {:on-success #(st/emit! (du/fetch-access-tokens))}]
|
||||
(st/emit! (du/delete-access-token (with-meta params mdata))))))
|
||||
|
||||
on-delete
|
||||
(mf/use-fn
|
||||
(mf/deps delete-fn)
|
||||
(fn []
|
||||
(st/emit! (modal/show
|
||||
{:type :confirm
|
||||
:title (tr "modals.delete-acces-token.title")
|
||||
:message (tr "modals.delete-acces-token.message")
|
||||
:accept-label (tr "modals.delete-acces-token.accept")
|
||||
:on-accept delete-fn}))))]
|
||||
|
||||
[:div.table-row
|
||||
[:div.table-field.name
|
||||
(str (:name token))]
|
||||
[:div.table-field.expiration-date
|
||||
[:span.content {:class (when expired? "expired")}
|
||||
(cond
|
||||
(nil? expires-at) (tr "dashboard.access-tokens.no-expiration")
|
||||
expired? (tr "dashboard.access-tokens.expired-on" expires-txt)
|
||||
:else (tr "dashboard.access-tokens.expires-on" expires-txt))]]
|
||||
[:div.table-field.actions
|
||||
[:& access-token-actions
|
||||
{:on-delete on-delete :key (:id token)}]]]))
|
||||
|
||||
(mf/defc access-tokens-page
|
||||
[]
|
||||
(mf/with-effect []
|
||||
(dom/set-html-title (tr "title.settings.access-tokens"))
|
||||
(st/emit! (du/fetch-access-tokens)))
|
||||
|
||||
(let [tokens (mf/deref tokens-ref)]
|
||||
[:div.dashboard-access-tokens
|
||||
[:div
|
||||
[:& access-tokens-hero]
|
||||
(if (empty? tokens)
|
||||
[:div.access-tokens-empty
|
||||
[:div (tr "dashboard.access-tokens.empty.no-access-tokens")]
|
||||
[:div (tr "dashboard.access-tokens.empty.add-one")]]
|
||||
[:div.dashboard-table
|
||||
[:div.table-rows
|
||||
(for [token tokens]
|
||||
[:& access-token-item {:token token :key (:id token)}])]])]]))
|
||||
|
||||
|
|
@ -21,44 +21,50 @@
|
|||
|
||||
(mf/defc sidebar-content
|
||||
[{:keys [profile section] :as props}]
|
||||
(let [profile? (= section :settings-profile)
|
||||
password? (= section :settings-password)
|
||||
options? (= section :settings-options)
|
||||
feedback? (= section :settings-feedback)
|
||||
(let [profile? (= section :settings-profile)
|
||||
password? (= section :settings-password)
|
||||
options? (= section :settings-options)
|
||||
feedback? (= section :settings-feedback)
|
||||
access-tokens? (= section :settings-access-tokens)
|
||||
|
||||
go-dashboard
|
||||
(mf/use-callback
|
||||
(mf/deps profile)
|
||||
#(st/emit! (rt/nav :dashboard-projects {:team-id (du/get-current-team-id profile)})))
|
||||
(mf/deps profile)
|
||||
#(st/emit! (rt/nav :dashboard-projects {:team-id (du/get-current-team-id profile)})))
|
||||
|
||||
go-settings-profile
|
||||
(mf/use-callback
|
||||
(mf/deps profile)
|
||||
#(st/emit! (rt/nav :settings-profile)))
|
||||
(mf/deps profile)
|
||||
#(st/emit! (rt/nav :settings-profile)))
|
||||
|
||||
go-settings-feedback
|
||||
(mf/use-callback
|
||||
(mf/deps profile)
|
||||
#(st/emit! (rt/nav :settings-feedback)))
|
||||
(mf/deps profile)
|
||||
#(st/emit! (rt/nav :settings-feedback)))
|
||||
|
||||
go-settings-password
|
||||
(mf/use-callback
|
||||
(mf/deps profile)
|
||||
#(st/emit! (rt/nav :settings-password)))
|
||||
(mf/deps profile)
|
||||
#(st/emit! (rt/nav :settings-password)))
|
||||
|
||||
go-settings-options
|
||||
(mf/use-callback
|
||||
(mf/deps profile)
|
||||
#(st/emit! (rt/nav :settings-options)))
|
||||
(mf/deps profile)
|
||||
#(st/emit! (rt/nav :settings-options)))
|
||||
|
||||
go-settings-access-tokens
|
||||
(mf/use-callback
|
||||
(mf/deps profile)
|
||||
#(st/emit! (rt/nav :settings-access-tokens)))
|
||||
|
||||
show-release-notes
|
||||
(mf/use-callback
|
||||
(fn [event]
|
||||
(let [version (:main @cf/version)]
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version}))
|
||||
(if (and (kbd/alt? event) (kbd/mod? event))
|
||||
(st/emit! (modal/show {:type :onboarding}))
|
||||
(st/emit! (modal/show {:type :release-notes :version version}))))))]
|
||||
(fn [event]
|
||||
(let [version (:main @cf/version)]
|
||||
(st/emit! (ptk/event ::ev/event {::ev/name "show-release-notes" :version version}))
|
||||
(if (and (kbd/alt? event) (kbd/mod? event))
|
||||
(st/emit! (modal/show {:type :onboarding}))
|
||||
(st/emit! (modal/show {:type :release-notes :version version}))))))]
|
||||
|
||||
[:div.sidebar-content
|
||||
[:div.sidebar-content-section
|
||||
|
@ -85,6 +91,13 @@
|
|||
i/tree
|
||||
[:span.element-title (tr "labels.settings")]]
|
||||
|
||||
(when (contains? @cf/flags :access-tokens)
|
||||
[:li {:class (when access-tokens? "current")
|
||||
:on-click go-settings-access-tokens
|
||||
:data-test "settings-access-tokens"}
|
||||
i/icon-key
|
||||
[:span.element-title (tr "labels.access-tokens")]])
|
||||
|
||||
[:hr]
|
||||
|
||||
[:li {:on-click show-release-notes :data-test "release-notes"}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
(ns app.util.time
|
||||
(:require
|
||||
["date-fns/format" :default dateFnsFormat]
|
||||
["date-fns/formatDistanceToNowStrict" :default dateFnsFormatDistanceToNowStrict]
|
||||
["date-fns/locale/ar-SA" :default dateFnsLocalesAr]
|
||||
["date-fns/locale/ca" :default dateFnsLocalesCa]
|
||||
|
@ -232,3 +233,13 @@
|
|||
:addSuffix true
|
||||
:locale (obj/get locales locale)}
|
||||
(dateFnsFormatDistanceToNowStrict v))))))
|
||||
|
||||
(defn format-date-locale
|
||||
([v] (format-date-locale v nil))
|
||||
([v {:keys [locale] :or {locale "en"}}]
|
||||
(when v
|
||||
(let [v (if (datetime? v) (format v :date) v)
|
||||
locale (obj/get locales locale)
|
||||
f (.date (.-formatLong locale) v)]
|
||||
(->> #js {:locale locale}
|
||||
(dateFnsFormat v f))))))
|
||||
|
|
|
@ -271,6 +271,114 @@ msgstr "Add as Shared Library"
|
|||
msgid "dashboard.change-email"
|
||||
msgstr "Change email"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.personal"
|
||||
msgstr "Personal access tokens"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.personal.description"
|
||||
msgstr "Personal access tokens function like an alternative to our login/password authentication system and can be used to allow an application to access the internal Penpot API"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.create"
|
||||
msgstr "Generate new token"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.empty.no-access-tokens"
|
||||
msgstr "You have no tokens so far."
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.empty.add-one"
|
||||
msgstr "Press the button \"Generate new token\" to generate one."
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "modals.create-access-token.title"
|
||||
msgstr "Generate access token"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "modals.create-access-token.name.label"
|
||||
msgstr "Name"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "modals.create-access-token.name.placeholder"
|
||||
msgstr "The name can help to know what's the token for"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "modals.create-access-token.expiration-date.label"
|
||||
msgstr "Expiration date"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "modals.create-access-token.submit-label"
|
||||
msgstr "Create token"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "modals.delete-acces-token.title"
|
||||
msgstr "Delete token"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "modals.delete-acces-token.message"
|
||||
msgstr "Are you sure you want to delete this token?"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "modals.delete-acces-token.accept"
|
||||
msgstr "Delete token"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.create.success"
|
||||
msgstr "Access token created successfully."
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.expires-on"
|
||||
msgstr "Expires on %s"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.expired-on"
|
||||
msgstr "Expired on %s"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.no-expiration"
|
||||
msgstr "No expiration date"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "modals.create-access-token.copy-token"
|
||||
msgstr "Copy token"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.copied-success"
|
||||
msgstr "Copied token"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.expiration-never"
|
||||
msgstr "Never"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.expiration-30-days"
|
||||
msgstr "30 days"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.expiration-60-days"
|
||||
msgstr "60 days"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.expiration-90-days"
|
||||
msgstr "90 days"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.expiration-180-days"
|
||||
msgstr "180 days"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.errors-required-name"
|
||||
msgstr "The name is required"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.token-will-expire"
|
||||
msgstr "The token will expire on %s"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.token-will-not-expire"
|
||||
msgstr "The token has no expiration date"
|
||||
|
||||
#: src/app/main/data/dashboard.cljs, src/app/main/data/dashboard.cljs
|
||||
msgid "dashboard.copy-suffix"
|
||||
msgstr "(copy)"
|
||||
|
@ -1429,6 +1537,10 @@ msgstr "Owner"
|
|||
msgid "labels.password"
|
||||
msgstr "Password"
|
||||
|
||||
#: src/app/main/ui/settings/sidebar.cljs
|
||||
msgid "labels.access-tokens"
|
||||
msgstr "Access tokens"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs
|
||||
msgid "labels.pending-invitation"
|
||||
msgstr "Pending"
|
||||
|
@ -2699,6 +2811,10 @@ msgstr "Password - Penpot"
|
|||
msgid "title.settings.profile"
|
||||
msgstr "Profile - Penpot"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "title.settings.access-tokens"
|
||||
msgstr "Profile - Access tokens"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs
|
||||
msgid "title.team-invitations"
|
||||
msgstr "Invitations - %s - Penpot"
|
||||
|
|
|
@ -277,6 +277,115 @@ msgstr "Añadir como Biblioteca Compartida"
|
|||
msgid "dashboard.change-email"
|
||||
msgstr "Cambiar correo"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.personal"
|
||||
msgstr "Access tokens personales"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.personal.description"
|
||||
msgstr "Los access tokens personales funcionan como una alternativa a nuestro sistema de autenticación "
|
||||
"usuario/password y se pueden usar para permitir a otras aplicaciones acceso a la API interna de Penpot"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.create"
|
||||
msgstr "Generar nuevo token"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.empty.no-access-tokens"
|
||||
msgstr "Todavía no tienes ningún token."
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.empty.add-one"
|
||||
msgstr "Pulsa el botón \"Generar nuevo token\" para generar uno."
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "modals.create-access-token.title"
|
||||
msgstr "Generar access token"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "modals.create-access-token.name.label"
|
||||
msgstr "Nombre"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "modals.create-access-token.name.placeholder"
|
||||
msgstr "El nombre te pude ayudar a saber para qué se utiliza el token"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "modals.create-access-token.expiration-date.label"
|
||||
msgstr "Fecha de expiración"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "modals.create-access-token.submit-label"
|
||||
msgstr "Crear token"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "modals.delete-acces-token.title"
|
||||
msgstr "Borrar token"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "modals.delete-acces-token.message"
|
||||
msgstr "¿Seguro que deseas borrar este token?"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "modals.delete-acces-token.accept"
|
||||
msgstr "Borrar token"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.create.success"
|
||||
msgstr "Access token creado con éxito."
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.expires-on"
|
||||
msgstr "Expira el %s"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.expired-on"
|
||||
msgstr "Expiró el %s"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.no-expiration"
|
||||
msgstr "Sin fecha de expiración"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "modals.create-access-token.copy-token"
|
||||
msgstr "Copiar token"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.copied-success"
|
||||
msgstr "Token copiado"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.expiration-never"
|
||||
msgstr "Nunca"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.expiration-30-days"
|
||||
msgstr "30 días"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.expiration-60-days"
|
||||
msgstr "60 días"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.expiration-90-days"
|
||||
msgstr "90 días"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.expiration-180-days"
|
||||
msgstr "180 días"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.errors-required-name"
|
||||
msgstr "El nombre es obligatorio"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.token-will-expire"
|
||||
msgstr "El token expirará el %s"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "dashboard.access-tokens.token-will-not-expire"
|
||||
msgstr "El token no tiene fecha de expiración"
|
||||
|
||||
#: src/app/main/data/dashboard.cljs, src/app/main/data/dashboard.cljs
|
||||
msgid "dashboard.copy-suffix"
|
||||
msgstr "(copia)"
|
||||
|
@ -1479,6 +1588,10 @@ msgstr "Propiedad"
|
|||
msgid "labels.password"
|
||||
msgstr "Contraseña"
|
||||
|
||||
#: src/app/main/ui/settings/sidebar.cljs
|
||||
msgid "labels.access-tokens"
|
||||
msgstr "Access tokens"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs
|
||||
msgid "labels.pending-invitation"
|
||||
msgstr "Pendiente"
|
||||
|
@ -2775,6 +2888,10 @@ msgstr "Contraseña - Penpot"
|
|||
msgid "title.settings.profile"
|
||||
msgstr "Perfil - Penpot"
|
||||
|
||||
#: src/app/main/ui/settings/access-tokens.cljs
|
||||
msgid "title.settings.access-tokens"
|
||||
msgstr "Perfil - Access tokens"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs
|
||||
msgid "title.team-invitations"
|
||||
msgstr "Invitaciones - %s - Penpot"
|
||||
|
|
Loading…
Add table
Reference in a new issue