Merge remote-tracking branch 'origin/staging' into develop

This commit is contained in:
Andrey Antukh 2026-08-03 18:36:27 +02:00
commit 6e843faba3
40 changed files with 506 additions and 457 deletions

View File

@ -5,7 +5,7 @@
## Stable namespace map
- `app.common.data` and `app.common.data.macros`: generic data helpers and performance macros that do not depend on Penpot domain entities.
- `app.common.types.*`: shared shape/file/page/component/token data types, schemas, predicates, and entity-local operations. `app.common.types.nitrate-permissions` contains shared fail-closed Nitrate organization/team permission rules.
- `app.common.types.*`: shared shape/file/page/component/token data types, schemas, predicates, and entity-local operations. `app.common.types.organization` contains organization schemas, `apply-organization`, and fail-closed organization/team permission rules (`allowed?`, `can-send-invitations?`).
- `app.common.files.*`: file-level operations, shape tree helpers, change application, migrations, validation, and undo/redo-related logic.
- `app.common.logic.*`: higher-level workflows/algorithms over files, shapes, components, variants, libraries, tokens, etc.
- `app.common.geom.*`: geometry helpers and transformations.

View File

@ -1,6 +1,6 @@
#!/usr/bin/env bash
export PENPOT_NITRATE_SHARED_KEY=super-secret-nitrate-api-key
export PENPOT_ADMIN_CONSOLE_SHARED_KEY=super-secret-nitrate-api-key
export PENPOT_EXPORTER_SHARED_KEY=super-secret-exporter-api-key
export PENPOT_NEXUS_SHARED_KEY=super-secret-nexus-api-key
export PENPOT_SECRET_KEY=super-secret-devenv-key
@ -71,7 +71,7 @@ export PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE=314572800
export PENPOT_USER_FEEDBACK_DESTINATION="support@example.com"
export PENPOT_NITRATE_BACKEND_URI=http://localhost:3000/admin-console
export PENPOT_ADMIN_CONSOLE_URI=http://localhost:3000/admin-console
export JAVA_OPTS="\
-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \

View File

@ -119,7 +119,7 @@
[:allowed-origins {:optional true} [::sm/set :string]]
[:exporter-shared-key {:optional true} :string]
[:nitrate-shared-key {:optional true} :string]
[:admin-console-shared-key {:optional true} :string]
[:nexus-shared-key {:optional true} :string]
[:management-api-key {:optional true} :string]
@ -264,7 +264,7 @@
[:netty-io-threads {:optional true} ::sm/int]
[:nitrate-backend-uri {:optional true} ::sm/uri]
[:admin-console-uri {:optional true} ::sm/uri]
;; DEPRECATED
[:assets-storage-backend {:optional true} :keyword]

View File

@ -469,7 +469,7 @@
::setup/shared-keys
{::setup/props (ig/ref ::setup/props)
:nexus (cf/get :nexus-shared-key)
:nitrate (cf/get :nitrate-shared-key)
:admin-console (cf/get :admin-console-shared-key)
:exporter (cf/get :exporter-shared-key)}
::setup/clock

View File

@ -49,7 +49,7 @@
"Joins relative path segments to the Nitrate backend URI.
Segments must not start with `/`"
[& segments]
(apply join-base-uri (cf/get :nitrate-backend-uri) segments))
(apply join-base-uri (cf/get :admin-console-uri) segments))
(defn- generate-public-uri
"Joins relative path segments to the public backend URI.
@ -143,7 +143,7 @@
(defn- request-to-nitrate
[cfg method uri schema {:keys [::rpc/profile-id request-params throw-on-error?] :as params}]
(let [shared-key (-> cfg ::setup/shared-keys :nitrate)
(let [shared-key (-> cfg ::setup/shared-keys :admin-console)
full-http-call (-> (request-builder cfg method uri shared-key profile-id request-params)
(with-retries 3)
(with-validate uri schema :throw-on-error? throw-on-error?))]
@ -155,7 +155,7 @@
(defn call
[cfg method params]
(when (contains? cf/flags :nitrate)
(when (contains? cf/flags :admin-console)
(let [client (get cfg ::client)
method (get client method)]
(method params))))
@ -487,7 +487,7 @@
(defmethod ig/init-key ::client
[_ cfg]
(when (contains? cf/flags :nitrate)
(when (contains? cf/flags :admin-console)
{:get-team-organization (partial get-team-organization-api cfg)
:set-team-organization (partial set-team-organization-api cfg)
:get-organization-membership (partial get-organization-membership-api cfg)
@ -549,7 +549,7 @@
callers are unaffected. Returns false when the :nitrate flag is off."
[cfg profile-id team-id]
(boolean
(when (and (contains? cf/flags :nitrate)
(when (and (contains? cf/flags :admin-console)
(nitrate-client? cfg)
(some? team-id)
(some? profile-id))

View File

@ -279,7 +279,7 @@
Raises :nitrate-sso-required error if user is not authorized in the organization."
[_ f mdata]
(if (and (contains? cf/flags :nitrate)
(if (and (contains? cf/flags :admin-console)
(::auth mdata true) ;; only for endpoints that needs auth
(::nitrate/sso mdata true))
(fn [cfg params]
@ -430,7 +430,7 @@
[cfg]
(let [cfg (assoc cfg ::module "management" ::type "command" ::metrics-id :rpc-management-timing)
mods (cond->> (list 'app.rpc.management.exporter)
(contains? cf/flags :nitrate)
(contains? cf/flags :admin-console)
(cons 'app.rpc.management.nitrate))]
(->> (apply sv/scan-ns mods)

View File

@ -14,7 +14,7 @@
[app.common.json :as json]
[app.common.schema :as sm]
[app.common.time :as ct]
[app.common.types.nitrate-permissions :as nitrate-perms]
[app.common.types.organization :as cto]
[app.config :as cf]
[app.db :as db]
[app.nitrate :as nitrate]
@ -423,16 +423,16 @@
(assert-not-default-team cfg team-id)
(assert-membership cfg profile-id organization-id)
;; Check moveTeams permission on the source organization
(when (contains? cf/flags :nitrate)
(when (contains? cf/flags :admin-console)
(let [organization-perms (nitrate/call cfg :get-organization-permissions
{:organization-id organization-id})]
(if (nil? organization-perms)
(ex/raise :type :validation
:code :not-allowed
:hint "Unable to verify organization permissions")
(when-not (nitrate-perms/allowed? :move-team
{:organization-perms organization-perms
:profile-id profile-id})
(when-not (cto/allowed? :move-team
{:organization-perms organization-perms
:profile-id profile-id})
(ex/raise :type :validation
:code :not-allowed
:hint "You are not allowed to move teams that are part of this organization. If you need more information, contact the owner.")))))
@ -462,7 +462,7 @@
Returns {:allows-anybody bool :external-emails [...]}"
[{:keys [::db/conn] :as cfg} team-id organization-id]
(let [organization-perms (nitrate/call cfg :get-organization-permissions {:organization-id organization-id})
allows-anybody (nitrate-perms/allowed? :add-anybody-to-team {:organization-perms organization-perms})]
allows-anybody (cto/allowed? :add-anybody-to-team {:organization-perms organization-perms})]
(if allows-anybody
{:allows-anybody true :external-emails []}
(let [emails (map :email (noh/get-team-invitation-emails conn team-id))]
@ -493,7 +493,7 @@
(assert-not-default-team cfg team-id)
(assert-membership cfg profile-id organization-id)
(when (contains? cf/flags :nitrate)
(when (contains? cf/flags :admin-console)
(let [organization-member-ids-before (into #{} (nitrate/call cfg :get-organization-members {:organization-id organization-id}))
team-with-organization (nitrate/call cfg :get-team-organization {:team-id team-id})
source-organization-id (get-in team-with-organization [:organization :id])
@ -517,18 +517,18 @@
(ex/raise :type :validation
:code :not-allowed
:hint "Unable to verify organization permissions"))
(when-not (nitrate-perms/allowed? :move-team
{:organization-perms source-organization-perms
:profile-id profile-id
:target-organization-same-owner? target-organization-same-owner?})
(when-not (cto/allowed? :move-team
{:organization-perms source-organization-perms
:profile-id profile-id
:target-organization-same-owner? target-organization-same-owner?})
(ex/raise :type :validation
:code :not-allowed
:hint "You are not allowed to move teams that are part of this organization. If you need more information, contact the owner.")))
;; Always check target create-teams permission (new/add and move flows).
(when-not (nitrate-perms/allowed? :create-team
{:organization-perms target-organization-perms
:profile-id profile-id})
(when-not (cto/allowed? :create-team
{:organization-perms target-organization-perms
:profile-id profile-id})
(ex/raise :type :validation
:code :not-allowed
:hint "You are not allowed to add teams in this organization"))
@ -540,7 +540,7 @@
(remove #{profile-id})
(remove organization-member-ids-before))]
(doseq [member-id new-member-ids]
(teams/initialize-user-in-nitrate-organization cfg member-id organization-id)))
(teams/initialize-user-in-organization cfg member-id organization-id)))
;; Api call to nitrate
(let [team (nitrate/call cfg :set-team-organization {:team-id team-id
@ -574,7 +574,7 @@
::sm/result [:map-of :string :boolean]
::db/transaction true}
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id organization-id emails]}]
(or (when (contains? cf/flags :nitrate)
(or (when (contains? cf/flags :admin-console)
(assert-membership cfg profile-id organization-id)
(let [emails-array (db/create-array conn "text" emails)
profiles (db/exec! conn [sql:get-profiles-by-emails emails-array])
@ -598,7 +598,7 @@
::sm/params schema:all-organization-members-in-team-params
::sm/result ::sm/boolean}
[cfg {:keys [::rpc/profile-id team-id organization-id]}]
(if (contains? cf/flags :nitrate)
(if (contains? cf/flags :admin-console)
(let [perms (teams/get-permissions cfg profile-id team-id)]
(when-not (or (:is-admin perms) (:is-owner perms))
(ex/raise :type :validation
@ -622,7 +622,7 @@
::sm/params schema:all-team-members-in-organizations-params
::sm/result [:map-of ::sm/uuid ::sm/boolean]}
[cfg {:keys [::rpc/profile-id team-id organization-ids]}]
(if (contains? cf/flags :nitrate)
(if (contains? cf/flags :admin-console)
(let [perms (teams/get-permissions cfg profile-id team-id)]
(when-not (or (:is-admin perms) (:is-owner perms))
(ex/raise :type :validation
@ -659,7 +659,7 @@
::sm/result schema:check-team-external-invitations-result
::db/transaction true}
[cfg {:keys [::rpc/profile-id team-id organization-id]}]
(if (contains? cf/flags :nitrate)
(if (contains? cf/flags :admin-console)
(let [perms (teams/get-permissions cfg profile-id team-id)]
(when-not (or (:is-admin perms) (:is-owner perms))
(ex/raise :type :validation
@ -692,7 +692,7 @@
::sm/params schema:check-nitrate-sso
::nitrate/sso false}
[cfg {:keys [::rpc/profile-id team-id organization-id url] :as params}]
(if (contains? cf/flags :nitrate)
(if (contains? cf/flags :admin-console)
(if (and team-id
(not (teams/has-read-permissions? cfg profile-id team-id)))
;; Let the destination RPC enforce its own permissions. Starting SSO before

View File

@ -45,8 +45,12 @@
[:email-comments [::sm/one-of #{:all :partial :none}]]
[:email-invites [::sm/one-of #{:all :none}]]])
(def system-managed-props
"Props keys managed by the system (not user-writable via RPC)."
#{:subscription})
(def schema:props
[:map {:title "ProfileProps"}
[:map {:title "ProfileProps" :closed true}
[:plugins {:optional true} schema:plugin-registry]
[:renderer {:optional true} [::sm/one-of #{:svg :wasm}]]
[:mcp-enabled {:optional true} ::sm/boolean]
@ -96,7 +100,7 @@
(defn- with-nitrate-licence
[profile cfg]
(if (contains? cf/flags :nitrate)
(if (contains? cf/flags :admin-console)
(nitrate/add-nitrate-licence-to-profile cfg profile)
profile))
@ -454,7 +458,7 @@
(assoc props k v))
props))
(:props profile)
props)]
(apply dissoc props system-managed-props))]
(db/update! conn :profile
{:props (db/tjson props)}
@ -503,7 +507,7 @@
;; imported "Your Penpot" teams according to whether they still have files.
;; Let Nitrate clean up the data associated with the deleted Penpot user:
;; owned organizations, remaining memberships, and subscription cancellation.
(when (contains? cf/flags :nitrate)
(when (contains? cf/flags :admin-console)
(nitrate/call cfg :cleanup-deleted-penpot-user
{:profile-id profile-id}))
@ -562,7 +566,7 @@
{::doc/added "2.18"
::sm/result schema:get-owned-organizations-summary-result}
[cfg {:keys [::rpc/profile-id]}]
(if (contains? cf/flags :nitrate)
(if (contains? cf/flags :admin-console)
(or (nitrate/call cfg :get-owned-organizations-summary {:profile-id profile-id}) [])
[]))

View File

@ -12,7 +12,7 @@
[app.common.features :as cfeat]
[app.common.schema :as sm]
[app.common.time :as ct]
[app.common.types.nitrate-permissions :as nitrate-perms]
[app.common.types.organization :as cto]
[app.common.types.team :as types.team]
[app.common.uuid :as uuid]
[app.config :as cf]
@ -197,9 +197,9 @@
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(dm/with-open [conn (db/open pool)]
(cond->> (get-teams conn profile-id)
(contains? cf/flags :nitrate)
(contains? cf/flags :admin-console)
(map #(nitrate/add-organization-info-to-team cfg % params))
(contains? cf/flags :nitrate)
(contains? cf/flags :admin-console)
(remove #(get-in % [:organization :expired-license])))))
(def ^:private sql:get-owned-teams
@ -244,7 +244,7 @@
::sm/params schema:get-team}
[cfg {:keys [::rpc/profile-id id file-id] :as params}]
(let [team (get-team cfg :profile-id profile-id :team-id id :file-id file-id)]
(if (contains? cf/flags :nitrate)
(if (contains? cf/flags :admin-console)
(nitrate/add-organization-info-to-team cfg team params)
team)))
@ -537,16 +537,16 @@
;; When creating inside an organization, verify the user has permission to do so.
;; Fail closed: if organization permissions cannot be fetched, deny the operation.
(when (and organization-id (contains? cf/flags :nitrate))
(when (and organization-id (contains? cf/flags :admin-console))
(let [organization-perms (nitrate/call cfg :get-organization-permissions
{:organization-id organization-id})]
(if (nil? organization-perms)
(ex/raise :type :validation
:code :not-allowed
:hint "Unable to verify organization permissions")
(when-not (nitrate-perms/allowed? :create-team
{:organization-perms organization-perms
:profile-id profile-id})
(when-not (cto/allowed? :create-team
{:organization-perms organization-perms
:profile-id profile-id})
(ex/raise :type :validation
:code :not-allowed
:hint "You are not allowed to create teams in this organization")))))
@ -579,15 +579,15 @@
team (create-team cfg params)]
(select-keys team [:id])))
(defn initialize-user-in-nitrate-organization
(defn initialize-user-in-organization
"If needed, create a default team for the user on the organization,
and notify Nitrate that a user has been added to an organization."
and initialize the user in the organization."
([cfg profile-id organization-id]
(initialize-user-in-nitrate-organization cfg profile-id organization-id nil))
(initialize-user-in-organization cfg profile-id organization-id nil))
([cfg profile-id organization-id email]
(assert (db/connection-map? cfg)
"expected cfg with valid connection")
(when (contains? cf/flags :nitrate)
(when (contains? cf/flags :admin-console)
(db/tx-run!
cfg
(fn [{:keys [::db/conn] :as tx-cfg}]
@ -621,13 +621,13 @@
([{:keys [::db/conn] :as cfg} {:keys [:profile-id :team-id] :as params} options]
(assert (db/connection-map? cfg)
"expected cfg with valid connection")
(when (contains? cf/flags :nitrate)
(when (contains? cf/flags :admin-console)
(let [membership (nitrate/call cfg :get-organization-membership-by-team {:profile-id profile-id :team-id team-id})]
;; Only when the team belong to an organization and the user is not a member
(when (and
(some? (:organization-id membership)) ;; the team do belong to an organization
(not (:is-member membership))) ;; the user is not a member of the organization yet
(initialize-user-in-nitrate-organization cfg profile-id (:organization-id membership)))))
(initialize-user-in-organization cfg profile-id (:organization-id membership)))))
(db/insert! conn :team-profile-rel (assoc params :id (uuid/next)) options)))
(defn create-team
@ -643,7 +643,7 @@
project (create-team-default-project conn params)]
(create-team-role cfg params)
;; Set team organization in Nitrate if organization-id is provided
(when (and (contains? cf/flags :nitrate) (:organization-id params))
(when (and (contains? cf/flags :admin-console) (:organization-id params))
(nitrate/set-team-organization cfg team params))
(assoc team :default-project-id (:id project))))
@ -803,19 +803,19 @@
[{:keys [::db/conn] :as cfg} {:keys [profile-id team-id] :as params}]
(let [team (get-team conn :profile-id profile-id :team-id team-id)
team (if (contains? cf/flags :nitrate)
team (if (contains? cf/flags :admin-console)
(nitrate/add-organization-info-to-team cfg team params)
team)
perms (get team :permissions)
organization (:organization team)
in-organization? (and (contains? cf/flags :nitrate) organization)
in-organization? (and (contains? cf/flags :admin-console) organization)
can-delete?
(if in-organization?
(nitrate-perms/allowed? :delete-team
{:organization-perms {:owner-id (dm/get-in team [:organization :owner-id])
:permissions (dm/get-in team [:organization :permissions])}
:profile-id profile-id
:team-perms perms})
(cto/allowed? :delete-team
{:organization-perms {:owner-id (dm/get-in team [:organization :owner-id])
:permissions (dm/get-in team [:organization :permissions])}
:profile-id profile-id
:team-perms perms})
(boolean (:is-owner perms)))]
(when-not can-delete?
@ -836,7 +836,7 @@
{::db/return-keys true})]
;; Api call to nitrate
(when (contains? cf/flags :nitrate)
(when (contains? cf/flags :admin-console)
(nitrate/call cfg :delete-team {:profile-id profile-id :team-id team-id}))
(wrk/submit! {::db/conn conn

View File

@ -14,7 +14,7 @@
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.time :as ct]
[app.common.types.nitrate-permissions :as nitrate-perms]
[app.common.types.organization :as cto]
[app.common.types.team :as types.team]
[app.common.uuid :as uuid]
[app.config :as cf]
@ -147,7 +147,7 @@
;; When nitrate is active and the team belongs to an organization, check that
;; the email is already an organization member unless the organization explicitly allows adding anybody.
(when (and (contains? cf/flags :nitrate)
(when (and (contains? cf/flags :admin-console)
(:organization team))
(assert-email-can-be-invited member organization-member-ids))
@ -166,8 +166,8 @@
(if organization
;; Insert the invited member to the organization
(when (contains? cf/flags :nitrate)
(teams/initialize-user-in-nitrate-organization cfg (:id member) (:id organization) email))
(when (contains? cf/flags :admin-console)
(teams/initialize-user-in-organization cfg (:id member) (:id organization) email))
;; Insert the invited member to the team
(teams/add-profile-to-team! cfg params {::db/on-conflict-do-nothing? true}))
@ -253,7 +253,7 @@
(when (allow-invitation-emails? member)
(if organization
(when (contains? cf/flags :nitrate)
(when (contains? cf/flags :admin-console)
(eml/send! {::eml/conn conn
::eml/factory eml/invite-to-organization
:public-uri (cf/get :public-uri)
@ -346,12 +346,12 @@
- invitations (vector of {:email :role} maps)"
[{:keys [::db/conn] :as cfg} {:keys [profile team role emails invitations] :as params}]
(let [;; Enrich team with organization info once for all invitations when nitrate is active
team (if (contains? cf/flags :nitrate)
team (if (contains? cf/flags :admin-console)
(nitrate/add-organization-info-to-team cfg team {})
team)
organization (:organization team)
organization-id (:id organization)
restricted? (and organization-id (not (nitrate-perms/allowed? :add-anybody-to-team {:organization-perms organization})))
restricted? (and organization-id (not (cto/allowed? :add-anybody-to-team {:organization-perms organization})))
all-organization-member-ids
(when organization-id
(into #{} (nitrate/call cfg :get-organization-members {:organization-id organization-id})))

View File

@ -87,7 +87,7 @@
(defn- with-nitrate-licence
[profile cfg]
(if (contains? cf/flags :nitrate)
(if (contains? cf/flags :admin-console)
(nitrate/add-nitrate-licence-to-profile cfg profile)
profile))
@ -136,8 +136,8 @@
accepted-team-id (if organization-id
;; Insert the invited member to the organization
(when (contains? cf/flags :nitrate)
(teams/initialize-user-in-nitrate-organization cfg id-member organization-id member-email))
(when (contains? cf/flags :admin-console)
(teams/initialize-user-in-organization cfg id-member organization-id member-email))
;; Insert the invited member to the team
(do (teams/add-profile-to-team! cfg params {::db/on-conflict-do-nothing? true})
team-id))]
@ -206,7 +206,7 @@
{:columns [:id :email :default-team-id]})
registration-disabled? (not (contains? cf/flags :registration))
organization-invitation? (and (contains? cf/flags :nitrate) organization-id)]
organization-invitation? (and (contains? cf/flags :admin-console) organization-id)]
(if profile
(do
@ -229,7 +229,7 @@
;; would call nitrate needlessly and could mask the clean
;; :canceled-invitation/:invalid-token response with a generic error.
(let [membership
(when (contains? cf/flags :nitrate)
(when (contains? cf/flags :admin-console)
(cond
organization-id
(nitrate/call cfg :get-organization-membership {:profile-id profile-id

View File

@ -14,7 +14,7 @@
[app.common.exceptions :as ex]
[app.common.schema :as sm]
[app.common.time :as ct]
[app.common.types.organization :refer [schema:team-with-organization schema:organization-with-avatar schema:nitrate-sso]]
[app.common.types.organization :as cto]
[app.common.types.profile :refer [schema:profile, schema:basic-profile]]
[app.common.types.team :refer [schema:team]]
[app.common.uuid :as uuid]
@ -152,7 +152,7 @@
(sv/defmethod ::notify-team-change
"Notify to Penpot a team change from nitrate"
{::doc/added "2.14"
::sm/params schema:team-with-organization
::sm/params cto/schema:team-with-organization
::rpc/auth false}
[cfg team]
(notifications/notify-team-change cfg (select-keys team [:id :is-your-penpot :organization]) nil)
@ -493,7 +493,7 @@ RETURNING id, deleted_at;")
{::doc/added "2.15"
::sm/params [:map
[:email ::sm/email]
[:organization schema:organization-with-avatar]]
[:organization cto/schema:organization-with-avatar]]
::nitrate/sso false}
[cfg params]
(db/tx-run! cfg ti/create-organization-invitation params)
@ -688,7 +688,7 @@ RETURNING id, deleted_at;")
[:user-name [:maybe ::sm/text]]
[:renewal-date :string]
[:estimated-amount :double]
[:organizations [:vector schema:organization-with-avatar]]])
[:organizations [:vector cto/schema:organization-with-avatar]]])
(sv/defmethod ::send-renewal-email
"Send an Enterprise subscription renewal notice email to a user."
@ -941,7 +941,7 @@ RETURNING id, deleted_at;")
Nitrate calls this while configuring SSO to verify client credentials and OIDC
discovery before saving the settings."
{::doc/added "2.20"
::sm/params schema:nitrate-sso
::sm/params cto/schema:nitrate-sso
::sm/result schema:check-organization-sso-result
::rpc/auth false}
[cfg params]
@ -997,7 +997,7 @@ RETURNING id, deleted_at;")
created users skip email verification and onboarding. Emails that already
belong to an existing profile are skipped. Intended for the Nitrate admin
bulk-creation screen; access is gated by the shared key and, in Nitrate, an
email allow-list. Requires the `nitrate-bulk-create-profiles` flag, disabled
email allow-list. Requires the `admin-console-bulk-create-profiles` flag, disabled
by default so it is only available on test environments."
{::doc/added "2.19"
::sm/params schema:bulk-create-profiles-params
@ -1005,9 +1005,9 @@ RETURNING id, deleted_at;")
::rpc/auth false}
[cfg {:keys [password emails]}]
(when-not (contains? cf/flags :nitrate-bulk-create-profiles)
(when-not (contains? cf/flags :admin-console-bulk-create-profiles)
(ex/raise :type :restriction
:code :nitrate-bulk-create-profiles-not-allowed
:code :bulk-create-profiles-not-allowed
:hint "Bulk profile creation is disabled by config."))
(let [derived (aauth/derive-password password)]

View File

@ -31,7 +31,7 @@
(defn- organization-sso-active?
"Return whether SSO is enabled for the organization."
[cfg organization-id]
(when (contains? cf/flags :nitrate)
(when (contains? cf/flags :admin-console)
(true? (:active (nitrate/call cfg :get-organization-sso {:organization-id organization-id})))))
(def ^:private xf:map-email (map :email))

View File

@ -115,7 +115,7 @@
(assoc keys id key)))))
{}
[:exporter
:nitrate
:admin-console
:nexus])))
(sm/register! ::props [:map-of :keyword ::sm/any])

View File

@ -25,7 +25,7 @@
[mockery.core :refer [with-mocks]]))
(t/use-fixtures :once (t/compose-fixtures
(partial th/init-config [:enable-nitrate])
(partial th/init-config [:enable-admin-console])
th/init-system))
(t/use-fixtures :each th/database-reset)

View File

@ -101,7 +101,7 @@
:team-id (:id team)
:url "https://penpot.example.com/#/workspace"}
{::http/request {}})]
(binding [cf/flags (conj cf/flags :nitrate)]
(binding [cf/flags (conj cf/flags :admin-console)]
(with-redefs [nitrate/call
(active-sso-call-mock
(:id team)
@ -124,7 +124,7 @@
:team-id (:id team)
:url "https://penpot.example.com/#/workspace"}
{::http/request {}})]
(binding [cf/flags (conj cf/flags :nitrate)]
(binding [cf/flags (conj cf/flags :admin-console)]
(with-redefs [nitrate/call
(active-sso-call-mock
(:id team)
@ -150,7 +150,7 @@
:team-id (:id team)
:url "https://penpot.example.com/#/workspace"}
{::http/request {}})]
(binding [cf/flags (conj cf/flags :nitrate)]
(binding [cf/flags (conj cf/flags :admin-console)]
(with-redefs [nitrate/organization-owner-of-team?
(fn [_cfg profile-id team-id]
(and (= (:id organization-owner) profile-id)
@ -851,7 +851,7 @@
organization-id-1 (uuid/random)
organization-id-2 (uuid/random)
calls (atom [])]
(with-redefs [cf/flags (conj cf/flags :nitrate)
(with-redefs [cf/flags (conj cf/flags :admin-console)
nitrate/call (fn [_cfg method params]
(swap! calls conj [method params])
(case method
@ -882,7 +882,7 @@
organization-id-1 (uuid/random)
organization-id-2 (uuid/random)
calls (atom [])]
(with-redefs [cf/flags (conj cf/flags :nitrate)
(with-redefs [cf/flags (conj cf/flags :admin-console)
nitrate/call (fn [_cfg method params]
(swap! calls conj [method params])
(case method
@ -990,7 +990,7 @@
:role "editor"
:valid-until (ct/in-future "48h")})
(with-redefs [cf/flags (conj cf/flags :nitrate)
(with-redefs [cf/flags (conj cf/flags :admin-console)
nitrate/call (add-team-to-organization-nitrate-mock
{:organization-id organization-id
:organization-summary organization-summary
@ -999,7 +999,7 @@
:team-id (:id team)
:sso-active? true
:set-team-params set-team-params})
teams/initialize-user-in-nitrate-organization (fn [& _] nil)
teams/initialize-user-in-organization (fn [& _] nil)
eml/send! (fn [params] (swap! sent conj params))]
(let [out (th/command! {::th/type :add-team-to-organization
::rpc/profile-id (:id owner)
@ -1066,7 +1066,7 @@
:role "editor"
:valid-until (ct/in-future "48h")})
(with-redefs [cf/flags (conj cf/flags :nitrate)
(with-redefs [cf/flags (conj cf/flags :admin-console)
nitrate/call (add-team-to-organization-nitrate-mock
{:organization-id organization-id
:organization-summary organization-summary
@ -1074,7 +1074,7 @@
:owner-id (:id owner)
:team-id (:id team)
:sso-active? false})
teams/initialize-user-in-nitrate-organization (fn [& _] nil)
teams/initialize-user-in-organization (fn [& _] nil)
eml/send! (fn [params] (swap! sent conj params))]
(let [out (th/command! {::th/type :add-team-to-organization
::rpc/profile-id (:id owner)
@ -1088,7 +1088,7 @@
nitrate-id "nitrate-instance-1"
public-key "-----BEGIN PUBLIC KEY-----\nMIIB\n-----END PUBLIC KEY-----"
now (ct/now)]
(with-redefs [cf/flags (conj cf/flags :nitrate)
(with-redefs [cf/flags (conj cf/flags :admin-console)
ct/now (constantly now)
nitrate/call (fn [_cfg method _params]
(t/is (= :get-identity method))
@ -1117,7 +1117,7 @@
(t/deftest get-nitrate-activation-code-request-identity-unavailable
(let [profile (th/create-profile* 1 {:is-active true})]
(with-redefs [cf/flags (conj cf/flags :nitrate)
(with-redefs [cf/flags (conj cf/flags :admin-console)
nitrate/call (fn [_cfg _method _params] nil)]
(let [out (th/command! {::th/type :get-nitrate-activation-code-request
::rpc/profile-id (:id profile)})]

View File

@ -28,7 +28,7 @@
(defn- with-organization-owner-access
[{:keys [organization-owner-id organization-id team-id]} f]
(with-redefs [cf/flags (conj cf/flags :nitrate)
(with-redefs [cf/flags (conj cf/flags :admin-console)
nitrate/organization-owner-of-team?
(fn [_cfg profile-id candidate-team-id]
(and (= organization-owner-id profile-id)

View File

@ -91,7 +91,7 @@
(t/is (not (contains? result :password))))))
(t/testing "update profile"
(with-redefs [app.config/flags #{:nitrate}]
(with-redefs [app.config/flags #{:admin-console}]
(with-redefs [nitrate/add-nitrate-licence-to-profile
(fn [_ profile]
(assoc profile :subscription {:plan :pro}))]
@ -1160,3 +1160,45 @@
(t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :validation))
(t/is (th/ex-of-code? error :email-as-password))))
(t/deftest update-profile-props-rejects-subscription
;; N1-16: Mass Assignment — :subscription must not be writable via RPC
;; The closed schema rejects :subscription at validation time
(let [profile (th/create-profile* 1)
data {::th/type :update-profile-props
::rpc/profile-id (:id profile)
:props {:subscription {:type "unlimited" :status "active"}}}
out (th/command! data)]
;; The call must fail with validation error
(t/is (th/ex-info? (:error out)))
(t/is (th/ex-of-type? (:error out) :validation))
(t/is (th/ex-of-code? (:error out) :params-validation))
;; And :subscription must NOT be persisted
(let [saved (th/db-get :profile {:id (:id profile)})
props (profile/decode-row saved)]
(t/is (nil? (get-in props [:props :subscription]))
":subscription must not be writable via update-profile-props"))))
(t/deftest update-profile-props-accepts-valid-keys
;; Verify that valid props keys still work after closing the schema
(let [profile (th/create-profile* 1)
data {::th/type :update-profile-props
::rpc/profile-id (:id profile)
:props {:onboarding-viewed true
:newsletter-updates false
:renderer :wasm}}
out (th/command! data)]
;; The call should succeed
(t/is (nil? (:error out)))
;; And all valid keys should be persisted
(let [saved (th/db-get :profile {:id (:id profile)})
props (profile/decode-row saved)]
(t/is (true? (get-in props [:props :onboarding-viewed])))
(t/is (false? (get-in props [:props :newsletter-updates])))
(t/is (= :wasm (get-in props [:props :renderer]))))))

View File

@ -137,7 +137,7 @@
:team-id (:id team)
:role :editor
:emails [email]}))]
(with-redefs [cf/flags (conj cf/flags :nitrate :email-verification)
(with-redefs [cf/flags (conj cf/flags :admin-console :email-verification)
nitrate/call nitrate-call]
(t/is (th/success? (invite! organization-team (:email invitee))))
(t/is (th/success? (invite! organization-team (:email invitee))))
@ -482,7 +482,7 @@
:role "editor"
:valid-until (ct/in-future "48h")})
(with-redefs [cf/flags (conj cf/flags :nitrate)
(with-redefs [cf/flags (conj cf/flags :admin-console)
nitrate/call
(fn [_cfg method _params]
(case method
@ -490,7 +490,7 @@
:is-member false}
:get-organization-members [(:id inviter) (uuid/random) (uuid/random)]
nil))
teams/initialize-user-in-nitrate-organization
teams/initialize-user-in-organization
(fn [& _] default-team-id)]
(let [out (verify! direct-token)]
(t/is (th/success? out))
@ -527,7 +527,7 @@
:role "editor"
:valid-until (ct/in-future "48h")})
(with-redefs [cf/flags (conj cf/flags :nitrate)
(with-redefs [cf/flags (conj cf/flags :admin-console)
nitrate/call
(fn [_cfg method _params]
(case method
@ -570,7 +570,7 @@
:role "editor"
:valid-until (ct/in-future "48h")})
(with-redefs [cf/flags (conj cf/flags :nitrate)
(with-redefs [cf/flags (conj cf/flags :admin-console)
nitrate/call
(fn [_cfg method _params]
(case method

View File

@ -164,13 +164,13 @@
;; redis for caching data
:redis-cache
;; Activates the nitrate module
:nitrate
;; Activates the admin-console module
:admin-console
;; disabled by default. When enabled, allows the nitrate
;; disabled by default. When enabled, allows the admin-console
;; `bulk-create-profiles` method to create batches of already
;; active profiles. Only intended for test environments.
:nitrate-bulk-create-profiles
:admin-console-bulk-create-profiles
:mcp
:background-blur

View File

@ -1,103 +0,0 @@
;; 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.common.types.nitrate-permissions)
(def ^:private defaults
{:create-teams "any"
:delete-teams "onlyOwners"
:move-teams "always"
:send-invitations "ownersAndAdmins"
:new-team-members "anyone"})
(defn- can-create-team?
[{:keys [is-organization-owner? permission-value]}]
(or is-organization-owner?
(= permission-value "any")))
(defn- can-delete-team?
[{:keys [is-organization-owner? permission-value team-perms]}]
(cond
;; Organization owners can always delete teams inside their organizations.
is-organization-owner?
true
(= permission-value "onlyOwners")
(boolean (:is-owner team-perms))
:else false))
(defn- can-move-team?
[{:keys [permission-value target-organization-same-owner?]}]
(cond
(= permission-value "never")
false
(= permission-value "always")
true
(= permission-value "myOrganizations")
(true? target-organization-same-owner?)
:else false))
(defn- can-invite-to-team?
[{:keys [permission-value team-perms]}]
(cond
(= permission-value "ownersAndAdmins")
(or (boolean (:is-owner team-perms))
(boolean (:is-admin team-perms)))
(= permission-value "owners")
(boolean (:is-owner team-perms))
:else false))
(defn- can-add-anybody-to-team?
[{:keys [permission-value]}]
(= permission-value "anyone"))
(def ^:private action-rules
{:create-team {:permission-key :create-teams
:check-fn can-create-team?}
:delete-team {:permission-key :delete-teams
:check-fn can-delete-team?}
:move-team {:permission-key :move-teams
:check-fn can-move-team?}
:send-invitations {:permission-key :send-invitations
:check-fn can-invite-to-team?}
:add-anybody-to-team {:permission-key :new-team-members
:check-fn can-add-anybody-to-team?}})
(defn- normalize-organization-permissions
[organization-perms]
(merge defaults (or (:permissions organization-perms) {})))
(defn- owner?
[organization-perms profile-id]
(= profile-id (:owner-id organization-perms)))
(defn allowed?
"Returns true only for explicitly allowed actions (fail-closed)."
[action {:keys [organization-perms profile-id team-perms target-organization-same-owner?]}]
(let [{:keys [permission-key check-fn] :as rule}
(get action-rules action)
permissions (normalize-organization-permissions organization-perms)
is-organization-owner? (owner? organization-perms profile-id)
permission-value (get permissions permission-key)]
(cond
(nil? rule) false
:else (boolean (check-fn {:is-organization-owner? is-organization-owner?
:permission-value permission-value
:team-perms team-perms
:target-organization-same-owner? target-organization-same-owner?})))))
(defn can-send-invitations?
[{:keys [nitrate-enabled? organization profile-id team-permissions]}]
(let [in-organization? (and nitrate-enabled? organization)]
(if in-organization?
(allowed? :send-invitations
{:organization-perms {:owner-id (:owner-id organization)
:permissions (:permissions organization)}
:profile-id profile-id
:team-perms team-permissions})
(or (boolean (:is-owner team-permissions))
(boolean (:is-admin team-permissions))))))

View File

@ -6,6 +6,7 @@
(ns app.common.types.organization
(:require
[app.common.flags :as flags]
[app.common.schema :as sm]))
(def schema:organization
@ -72,3 +73,104 @@
[:client-id {:optional true} [:maybe :string]]
[:client-secret {:optional true} [:maybe :string]]
[:issuer {:optional true} [:maybe :string]]])
;; --- Organization permission rules ---
(def ^:private defaults
{:create-teams "any"
:delete-teams "onlyOwners"
:move-teams "always"
:send-invitations "ownersAndAdmins"
:new-team-members "anyone"})
(defn- can-create-team?
[{:keys [is-organization-owner? permission-value]}]
(or is-organization-owner?
(= permission-value "any")))
(defn- can-delete-team?
[{:keys [is-organization-owner? permission-value team-perms]}]
(cond
;; Organization owners can always delete teams inside their organizations.
is-organization-owner?
true
(= permission-value "onlyOwners")
(boolean (:is-owner team-perms))
:else false))
(defn- can-move-team?
[{:keys [permission-value target-organization-same-owner?]}]
(cond
(= permission-value "never")
false
(= permission-value "always")
true
(= permission-value "myOrganizations")
(true? target-organization-same-owner?)
:else false))
(defn- can-invite-to-team?
[{:keys [permission-value team-perms]}]
(cond
(= permission-value "ownersAndAdmins")
(or (boolean (:is-owner team-perms))
(boolean (:is-admin team-perms)))
(= permission-value "owners")
(boolean (:is-owner team-perms))
:else false))
(defn- can-add-anybody-to-team?
[{:keys [permission-value]}]
(= permission-value "anyone"))
(def ^:private action-rules
{:create-team {:permission-key :create-teams
:check-fn can-create-team?}
:delete-team {:permission-key :delete-teams
:check-fn can-delete-team?}
:move-team {:permission-key :move-teams
:check-fn can-move-team?}
:send-invitations {:permission-key :send-invitations
:check-fn can-invite-to-team?}
:add-anybody-to-team {:permission-key :new-team-members
:check-fn can-add-anybody-to-team?}})
(defn- normalize-organization-permissions
[organization-perms]
(merge defaults (or (:permissions organization-perms) {})))
(defn- owner?
[organization-perms profile-id]
(= profile-id (:owner-id organization-perms)))
(defn allowed?
"Returns true only for explicitly allowed actions (fail-closed)."
[action {:keys [organization-perms profile-id team-perms target-organization-same-owner?]}]
(let [{:keys [permission-key check-fn] :as rule}
(get action-rules action)
permissions (normalize-organization-permissions organization-perms)
is-organization-owner? (owner? organization-perms profile-id)
permission-value (get permissions permission-key)]
(cond
(nil? rule) false
:else (boolean (check-fn {:is-organization-owner? is-organization-owner?
:permission-value permission-value
:team-perms team-perms
:target-organization-same-owner? target-organization-same-owner?})))))
(defn can-send-invitations?
"Returns true when the user can send invitations to a team.
Falls back to team-level permissions (owner/admin) when the
admin-console flag is off or the team has no organization."
[{:keys [organization profile-id team-permissions]}]
(if (and (contains? flags/*current* :admin-console) organization)
(allowed? :send-invitations
{:organization-perms {:owner-id (:owner-id organization)
:permissions (:permissions organization)}
:profile-id profile-id
:team-perms team-permissions})
(or (boolean (:is-owner team-permissions))
(boolean (:is-admin team-permissions)))))

View File

@ -77,8 +77,8 @@
[common-tests.types.container-test]
[common-tests.types.fill-test]
[common-tests.types.modifiers-test]
[common-tests.types.nitrate-permissions-test]
[common-tests.types.objects-map-test]
[common-tests.types.organization-test]
[common-tests.types.path-data-test]
[common-tests.types.shape-decode-encode-test]
[common-tests.types.shape-interactions-test]
@ -154,8 +154,8 @@
'common-tests.types.container-test
'common-tests.types.fill-test
'common-tests.types.modifiers-test
'common-tests.types.nitrate-permissions-test
'common-tests.types.objects-map-test
'common-tests.types.organization-test
'common-tests.types.path-data-test
'common-tests.types.shape-decode-encode-test
'common-tests.types.shape-interactions-test

View File

@ -1,178 +0,0 @@
;; 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 common-tests.types.nitrate-permissions-test
(:require
[app.common.types.nitrate-permissions :as nitrate-perms]
[clojure.test :as t]))
(def organization-perms
{:owner-id :owner
:permissions {:create-teams "any"
:delete-teams "onlyOwners"
:send-invitations "ownersAndAdmins"}})
(t/deftest unknown-action-is-denied
(t/is (false? (nitrate-perms/allowed? :unknown
{:organization-perms organization-perms
:profile-id :member
:team-perms {:is-admin true}}))))
(t/deftest organization-owner-is-allowed-for-create-and-delete
(t/is (true? (nitrate-perms/allowed? :create-team
{:organization-perms organization-perms
:profile-id :owner
:team-perms {:is-admin false}})))
(t/is (true? (nitrate-perms/allowed? :delete-team
{:organization-perms organization-perms
:profile-id :owner
:team-perms {:is-admin false}}))))
(t/deftest create-team-permission-rules
(t/is (true? (nitrate-perms/allowed? :create-team
{:organization-perms organization-perms
:profile-id :member
:team-perms {:is-admin false}})))
(t/is (false? (nitrate-perms/allowed? :create-team
{:organization-perms (assoc organization-perms :permissions {:create-teams "none"
:delete-teams "onlyOwners"})
:profile-id :member
:team-perms {:is-admin false}}))))
(t/deftest delete-team-onlyowners-allows-only-team-owners
(t/is (true? (nitrate-perms/allowed? :delete-team
{:organization-perms organization-perms
:profile-id :member
:team-perms {:is-owner true :is-admin true}})))
(t/is (false? (nitrate-perms/allowed? :delete-team
{:organization-perms organization-perms
:profile-id :member
:team-perms {:is-admin true}})))
(t/is (false? (nitrate-perms/allowed? :delete-team
{:organization-perms (assoc organization-perms :permissions {:create-teams "any"
:delete-teams "invalid-value"})
:profile-id :member
:team-perms {:is-admin true}}))))
(t/deftest delete-team-onlyme-still-allows-organization-owner
(let [only-me-organization (assoc organization-perms :permissions {:create-teams "any"
:delete-teams "onlyMe"})]
(t/is (true? (nitrate-perms/allowed? :delete-team
{:organization-perms only-me-organization
:profile-id :owner
:team-perms {:is-owner false :is-admin false}})))
(t/is (false? (nitrate-perms/allowed? :delete-team
{:organization-perms only-me-organization
:profile-id :member
:team-perms {:is-owner true :is-admin true}})))))
(t/deftest move-team-always-allows-any-organization-owner-or-all-users
(let [always-organization (assoc organization-perms :permissions {:create-teams "any"
:delete-teams "onlyOwners"
:move-teams "always"})]
;; Organization owner should always be allowed
(t/is (true? (nitrate-perms/allowed? :move-team
{:organization-perms always-organization
:profile-id :owner
:team-perms {}})))
;; Regular member should be allowed when move-teams is "always"
(t/is (true? (nitrate-perms/allowed? :move-team
{:organization-perms always-organization
:profile-id :member
:team-perms {}})))))
(t/deftest move-team-myorganizations-allows-only-within-same-owner
(let [my-organizations (assoc organization-perms :permissions {:create-teams "any"
:delete-teams "onlyOwners"
:move-teams "myOrganizations"})]
;; Organization owner must also stay within same-owner organizations
(t/is (false? (nitrate-perms/allowed? :move-team
{:organization-perms my-organizations
:profile-id :owner
:team-perms {}
:target-organization-same-owner? false})))
(t/is (true? (nitrate-perms/allowed? :move-team
{:organization-perms my-organizations
:profile-id :owner
:team-perms {}
:target-organization-same-owner? true})))
;; Regular member should be allowed only if target has same owner
(t/is (true? (nitrate-perms/allowed? :move-team
{:organization-perms my-organizations
:profile-id :member
:team-perms {}
:target-organization-same-owner? true})))
(t/is (false? (nitrate-perms/allowed? :move-team
{:organization-perms my-organizations
:profile-id :member
:team-perms {}
:target-organization-same-owner? false})))))
(t/deftest move-team-never-denies-all
(let [never-organization (assoc organization-perms :permissions {:create-teams "any"
:delete-teams "onlyOwners"
:move-teams "never"})]
;; Even organization owner should be denied
(t/is (false? (nitrate-perms/allowed? :move-team
{:organization-perms never-organization
:profile-id :owner
:team-perms {}})))
;; Regular member should be denied
(t/is (false? (nitrate-perms/allowed? :move-team
{:organization-perms never-organization
:profile-id :member
:team-perms {}})))))
(t/deftest move-team-defaults-to-always
(let [default-organization (assoc organization-perms :permissions {:create-teams "any"
:delete-teams "onlyOwners"})]
;; Should default to "always" when not specified
(t/is (true? (nitrate-perms/allowed? :move-team
{:organization-perms default-organization
:profile-id :member
:team-perms {}})))))
(t/deftest send-invitations-defaults-to-owners-and-admins
(let [default-organization (assoc organization-perms :permissions {:create-teams "any"
:delete-teams "onlyOwners"})]
(t/is (true? (nitrate-perms/allowed? :send-invitations
{:organization-perms default-organization
:profile-id :owner
:team-perms {:is-owner true :is-admin false}})))
(t/is (true? (nitrate-perms/allowed? :send-invitations
{:organization-perms default-organization
:profile-id :member
:team-perms {:is-owner false :is-admin true}})))
(t/is (false? (nitrate-perms/allowed? :send-invitations
{:organization-perms default-organization
:profile-id :member
:team-perms {:is-owner false :is-admin false}})))))
(t/deftest send-invitations-owners-allows-only-team-owners
(let [only-owners-organization (assoc organization-perms :permissions {:create-teams "any"
:delete-teams "onlyOwners"
:send-invitations "owners"})]
(t/is (true? (nitrate-perms/allowed? :send-invitations
{:organization-perms only-owners-organization
:profile-id :member
:team-perms {:is-owner true :is-admin true}})))
(t/is (false? (nitrate-perms/allowed? :send-invitations
{:organization-perms only-owners-organization
:profile-id :owner
:team-perms {:is-owner false :is-admin false}})))
(t/is (false? (nitrate-perms/allowed? :send-invitations
{:organization-perms only-owners-organization
:profile-id :member
:team-perms {:is-owner false :is-admin true}})))))
(t/deftest send-invitations-invalid-value-is-denied
(let [invalid-organization (assoc organization-perms :permissions {:create-teams "any"
:delete-teams "onlyOwners"
:send-invitations "invalid-value"})]
(t/is (false? (nitrate-perms/allowed? :send-invitations
{:organization-perms invalid-organization
:profile-id :member
:team-perms {:is-owner true :is-admin true}})))))

View File

@ -0,0 +1,178 @@
;; 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 common-tests.types.organization-test
(:require
[app.common.types.organization :as cto]
[clojure.test :as t]))
(def organization-perms
{:owner-id :owner
:permissions {:create-teams "any"
:delete-teams "onlyOwners"
:send-invitations "ownersAndAdmins"}})
(t/deftest unknown-action-is-denied
(t/is (false? (cto/allowed? :unknown
{:organization-perms organization-perms
:profile-id :member
:team-perms {:is-admin true}}))))
(t/deftest organization-owner-is-allowed-for-create-and-delete
(t/is (true? (cto/allowed? :create-team
{:organization-perms organization-perms
:profile-id :owner
:team-perms {:is-admin false}})))
(t/is (true? (cto/allowed? :delete-team
{:organization-perms organization-perms
:profile-id :owner
:team-perms {:is-admin false}}))))
(t/deftest create-team-permission-rules
(t/is (true? (cto/allowed? :create-team
{:organization-perms organization-perms
:profile-id :member
:team-perms {:is-admin false}})))
(t/is (false? (cto/allowed? :create-team
{:organization-perms (assoc organization-perms :permissions {:create-teams "none"
:delete-teams "onlyOwners"})
:profile-id :member
:team-perms {:is-admin false}}))))
(t/deftest delete-team-onlyowners-allows-only-team-owners
(t/is (true? (cto/allowed? :delete-team
{:organization-perms organization-perms
:profile-id :member
:team-perms {:is-owner true :is-admin true}})))
(t/is (false? (cto/allowed? :delete-team
{:organization-perms organization-perms
:profile-id :member
:team-perms {:is-admin true}})))
(t/is (false? (cto/allowed? :delete-team
{:organization-perms (assoc organization-perms :permissions {:create-teams "any"
:delete-teams "invalid-value"})
:profile-id :member
:team-perms {:is-admin true}}))))
(t/deftest delete-team-onlyme-still-allows-organization-owner
(let [only-me-organization (assoc organization-perms :permissions {:create-teams "any"
:delete-teams "onlyMe"})]
(t/is (true? (cto/allowed? :delete-team
{:organization-perms only-me-organization
:profile-id :owner
:team-perms {:is-owner false :is-admin false}})))
(t/is (false? (cto/allowed? :delete-team
{:organization-perms only-me-organization
:profile-id :member
:team-perms {:is-owner true :is-admin true}})))))
(t/deftest move-team-always-allows-any-organization-owner-or-all-users
(let [always-organization (assoc organization-perms :permissions {:create-teams "any"
:delete-teams "onlyOwners"
:move-teams "always"})]
;; Organization owner should always be allowed
(t/is (true? (cto/allowed? :move-team
{:organization-perms always-organization
:profile-id :owner
:team-perms {}})))
;; Regular member should be allowed when move-teams is "always"
(t/is (true? (cto/allowed? :move-team
{:organization-perms always-organization
:profile-id :member
:team-perms {}})))))
(t/deftest move-team-myorganizations-allows-only-within-same-owner
(let [my-organizations (assoc organization-perms :permissions {:create-teams "any"
:delete-teams "onlyOwners"
:move-teams "myOrganizations"})]
;; Organization owner must also stay within same-owner organizations
(t/is (false? (cto/allowed? :move-team
{:organization-perms my-organizations
:profile-id :owner
:team-perms {}
:target-organization-same-owner? false})))
(t/is (true? (cto/allowed? :move-team
{:organization-perms my-organizations
:profile-id :owner
:team-perms {}
:target-organization-same-owner? true})))
;; Regular member should be allowed only if target has same owner
(t/is (true? (cto/allowed? :move-team
{:organization-perms my-organizations
:profile-id :member
:team-perms {}
:target-organization-same-owner? true})))
(t/is (false? (cto/allowed? :move-team
{:organization-perms my-organizations
:profile-id :member
:team-perms {}
:target-organization-same-owner? false})))))
(t/deftest move-team-never-denies-all
(let [never-organization (assoc organization-perms :permissions {:create-teams "any"
:delete-teams "onlyOwners"
:move-teams "never"})]
;; Even organization owner should be denied
(t/is (false? (cto/allowed? :move-team
{:organization-perms never-organization
:profile-id :owner
:team-perms {}})))
;; Regular member should be denied
(t/is (false? (cto/allowed? :move-team
{:organization-perms never-organization
:profile-id :member
:team-perms {}})))))
(t/deftest move-team-defaults-to-always
(let [default-organization (assoc organization-perms :permissions {:create-teams "any"
:delete-teams "onlyOwners"})]
;; Should default to "always" when not specified
(t/is (true? (cto/allowed? :move-team
{:organization-perms default-organization
:profile-id :member
:team-perms {}})))))
(t/deftest send-invitations-defaults-to-owners-and-admins
(let [default-organization (assoc organization-perms :permissions {:create-teams "any"
:delete-teams "onlyOwners"})]
(t/is (true? (cto/allowed? :send-invitations
{:organization-perms default-organization
:profile-id :owner
:team-perms {:is-owner true :is-admin false}})))
(t/is (true? (cto/allowed? :send-invitations
{:organization-perms default-organization
:profile-id :member
:team-perms {:is-owner false :is-admin true}})))
(t/is (false? (cto/allowed? :send-invitations
{:organization-perms default-organization
:profile-id :member
:team-perms {:is-owner false :is-admin false}})))))
(t/deftest send-invitations-owners-allows-only-team-owners
(let [only-owners-organization (assoc organization-perms :permissions {:create-teams "any"
:delete-teams "onlyOwners"
:send-invitations "owners"})]
(t/is (true? (cto/allowed? :send-invitations
{:organization-perms only-owners-organization
:profile-id :member
:team-perms {:is-owner true :is-admin true}})))
(t/is (false? (cto/allowed? :send-invitations
{:organization-perms only-owners-organization
:profile-id :owner
:team-perms {:is-owner false :is-admin false}})))
(t/is (false? (cto/allowed? :send-invitations
{:organization-perms only-owners-organization
:profile-id :member
:team-perms {:is-owner false :is-admin true}})))))
(t/deftest send-invitations-invalid-value-is-denied
(let [invalid-organization (assoc organization-perms :permissions {:create-teams "any"
:delete-teams "onlyOwners"
:send-invitations "invalid-value"})]
(t/is (false? (cto/allowed? :send-invitations
{:organization-perms invalid-organization
:profile-id :member
:team-perms {:is-owner true :is-admin true}})))))

View File

@ -53,13 +53,13 @@ update_oidc_name /var/www/app/js/config.js
export PENPOT_BACKEND_URI=${PENPOT_BACKEND_URI:-http://penpot-backend:6060}
export PENPOT_EXPORTER_URI=${PENPOT_EXPORTER_URI:-http://penpot-exporter:6061}
export PENPOT_NITRATE_URI=${PENPOT_NITRATE_URI:-http://penpot-nitrate:3000}
export PENPOT_ADMIN_CONSOLE_URI=${PENPOT_ADMIN_CONSOLE_URI:-http://penpot-nitrate:3000}
export PENPOT_HTTP_SERVER_MAX_BODY_SIZE=${PENPOT_HTTP_SERVER_MAX_BODY_SIZE:-367001600} # Default to 350MiB
export PENPOT_IPV6_LISTEN_DIRECTIVE=${PENPOT_IPV6_LISTEN_DIRECTIVE:-"listen [::]:8080 default_server reuseport backlog=16384;"}
if is_truthy "${PENPOT_DISABLE_IPV6_LISTEN:-}"; then
export PENPOT_IPV6_LISTEN_DIRECTIVE=""
fi
envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_NITRATE_URI,\$PENPOT_HTTP_SERVER_MAX_BODY_SIZE,\$PENPOT_IPV6_LISTEN_DIRECTIVE" \
envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_ADMIN_CONSOLE_URI,\$PENPOT_HTTP_SERVER_MAX_BODY_SIZE,\$PENPOT_IPV6_LISTEN_DIRECTIVE" \
< /tmp/nginx.conf.template > /etc/nginx/nginx.conf
if [[ $PENPOT_FLAGS == *"enable-mcp"* ]]; then

View File

@ -156,7 +156,7 @@ http {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $http_cf_connecting_ip;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass $PENPOT_NITRATE_URI$request_uri;
proxy_pass $PENPOT_ADMIN_CONSOLE_URI$request_uri;
}
include /etc/nginx/overrides/server.d/*.conf;

View File

@ -8,7 +8,7 @@
"author": "Andrey Antukh",
"license": "MPL-2.0",
"dependencies": {
"draft-js": "penpot/draft-js.git#ba3b26ed63a01227a3560e440531b69d79c03f35",
"draft-js": "penpot/draft-js.git#c58ebd9429a6359d72a88cff87e078aaf6fe285d",
"immutable": "^5.1.9"
},
"peerDependencies": {

View File

@ -268,8 +268,8 @@ importers:
packages/draft-js:
dependencies:
draft-js:
specifier: penpot/draft-js.git#ba3b26ed63a01227a3560e440531b69d79c03f35
version: https://codeload.github.com/penpot/draft-js/tar.gz/ba3b26ed63a01227a3560e440531b69d79c03f35(encoding@0.1.13)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
specifier: penpot/draft-js.git#c58ebd9429a6359d72a88cff87e078aaf6fe285d
version: https://codeload.github.com/penpot/draft-js/tar.gz/c58ebd9429a6359d72a88cff87e078aaf6fe285d(encoding@0.1.13)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
immutable:
specifier: ^5.1.9
version: 5.1.9
@ -2895,8 +2895,8 @@ packages:
resolution: {integrity: sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==}
engines: {node: '>=20.19.0'}
draft-js@https://codeload.github.com/penpot/draft-js/tar.gz/ba3b26ed63a01227a3560e440531b69d79c03f35:
resolution: {gitHosted: true, integrity: sha512-IN2r8sw36jcH32WPP9eBFXNglirhkFIsqgoKzwwSDxiCbeipumeKXjYB3vw82CxYRwKc+oRTMcZ5jNnCWnmnBw==, tarball: https://codeload.github.com/penpot/draft-js/tar.gz/ba3b26ed63a01227a3560e440531b69d79c03f35}
draft-js@https://codeload.github.com/penpot/draft-js/tar.gz/c58ebd9429a6359d72a88cff87e078aaf6fe285d:
resolution: {gitHosted: true, integrity: sha512-Ji/UoFEm11scM7Zbp1O+7f498Ly5zkGRCJNvxR8BRL0xhqEs1ubgckZdmopjNxi+xjepHopa5hIv9pJTRaQOgg==, tarball: https://codeload.github.com/penpot/draft-js/tar.gz/c58ebd9429a6359d72a88cff87e078aaf6fe285d}
version: 0.11.7
peerDependencies:
react: '>=0.14.0'
@ -8249,7 +8249,7 @@ snapshots:
domelementtype: 3.0.0
domhandler: 6.0.1
draft-js@https://codeload.github.com/penpot/draft-js/tar.gz/ba3b26ed63a01227a3560e440531b69d79c03f35(encoding@0.1.13)(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
draft-js@https://codeload.github.com/penpot/draft-js/tar.gz/c58ebd9429a6359d72a88cff87e078aaf6fe285d(encoding@0.1.13)(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
dependencies:
fbjs: 3.0.5(encoding@0.1.13)
immutable: 3.8.3

View File

@ -11,7 +11,7 @@
[app.common.data.macros :as dm]
[app.common.schema :as sm]
[app.common.time :as ct]
[app.common.types.organization :as co]
[app.common.types.organization :as cto]
[app.common.types.team :as ctt]
[app.config :as cf]
[app.main.data.helpers :as dsh]
@ -255,7 +255,7 @@
(let [current-team-id (:current-team-id state)
organization (:organization team)
current-team? (= (:id team) current-team-id)]
(when (and (contains? cf/flags :nitrate)
(when (and (contains? cf/flags :admin-console)
current-team?)
(rx/concat
(when notification
@ -267,13 +267,13 @@
(check-team-sso current-team-id))))))
ptk/UpdateEvent
(update [_ state]
(if (contains? cf/flags :nitrate)
(if (contains? cf/flags :admin-console)
(let [team-id (:id team)
team-name (:name team)
organization (:organization team)]
(d/update-in-when state [:teams team-id]
(fn [team]
(cond-> (co/apply-organization team organization)
(cond-> (cto/apply-organization team organization)
team-name (assoc :name team-name)))))
state))))
@ -284,7 +284,7 @@
(ptk/reify ::handle-organization-change-sso
ptk/WatchEvent
(watch [_ state _]
(when (contains? cf/flags :nitrate)
(when (contains? cf/flags :admin-console)
(let [team-id (:current-team-id state)
team (dm/get-in state [:teams team-id])
team-organization-id (dm/get-in team [:organization :id])]

View File

@ -690,7 +690,7 @@
(ptk/reify ::handle-user-organization-change
ptk/WatchEvent
(watch [_ state _]
(when (and notification (contains? cf/flags :nitrate))
(when (and notification (contains? cf/flags :admin-console))
(let [team-id (:current-team-id state)
team (dm/get-in state [:teams team-id])]
(rx/of (ntf/show {:content (tr notification organization-name)
@ -708,7 +708,7 @@
(ptk/reify ::handle-organization-deleted
ptk/WatchEvent
(watch [_ state _]
(when (contains? cf/flags :nitrate)
(when (contains? cf/flags :admin-console)
(let [team-id (:current-team-id state)
current-team (dm/get-in state [:teams team-id])
current-organization-id (dm/get-in current-team [:organization :id])

View File

@ -1,7 +1,7 @@
(ns app.main.data.nitrate
(:require
[app.common.data.macros :as dm]
[app.common.types.nitrate-permissions :as nitrate-perms]
[app.common.types.organization :as cto]
[app.common.uri :as u]
[app.common.uuid :as uuid]
[app.config :as cf]
@ -84,9 +84,8 @@
(defn can-send-invitations?
[{:keys [organization profile-id team-permissions]}]
(nitrate-perms/can-send-invitations?
{:nitrate-enabled? (contains? cf/flags :nitrate)
:organization organization
(cto/can-send-invitations?
{:organization organization
:profile-id profile-id
:team-permissions team-permissions}))
@ -149,7 +148,7 @@
(defn is-valid-license?
[profile]
(and (contains? cf/flags :nitrate)
(and (contains? cf/flags :admin-console)
;; Possible values: "active" "canceled" "incomplete" "incomplete_expired" "past_due" "paused" "trialing" "unpaid"
(contains? #{"active" "past_due" "trialing"}
(dm/get-in profile [:subscription :status]))))
@ -279,7 +278,7 @@
(dt/with-refreshed-team team-id
(fn [team]
(let [source-organization (:organization team)
can-move? (nitrate-perms/allowed?
can-move? (cto/allowed?
:move-team
{:organization-perms {:owner-id (:owner-id source-organization)
:permissions (:permissions source-organization)}
@ -357,8 +356,8 @@
Organizations where :add-anybody-to-team is permitted are pre-approved;
the rest are verified via :all-team-members-in-organizations."
[team-id organizations]
(let [add-anybody-organizations (filterv #(nitrate-perms/allowed? :add-anybody-to-team {:organization-perms %}) organizations)
organizations-to-check (filterv #(not (nitrate-perms/allowed? :add-anybody-to-team {:organization-perms %})) organizations)
(let [add-anybody-organizations (filterv #(cto/allowed? :add-anybody-to-team {:organization-perms %}) organizations)
organizations-to-check (filterv #(not (cto/allowed? :add-anybody-to-team {:organization-perms %})) organizations)
organization-ids-to-check (mapv :id organizations-to-check)]
(if (empty? organization-ids-to-check)
(rx/of (into {} (map (fn [organization] [(:id organization) true])) organizations))

View File

@ -10,7 +10,7 @@
[app.common.data.macros :as dm]
[app.common.logging :as log]
[app.common.schema :as sm]
[app.common.types.nitrate-permissions :as nitrate-perms]
[app.common.types.organization :as cto]
[app.common.types.team :as ctt]
[app.common.uri :as u]
[app.config :as cf]
@ -42,7 +42,7 @@
default team when the candidate requires SSO and the user has no
valid SSO session for it."
[{:keys [team-id default-team-id]}]
(if (or (not (contains? cf/flags :nitrate))
(if (or (not (contains? cf/flags :admin-console))
(= team-id default-team-id))
(rx/of team-id)
(->> (rp/cmd! :check-nitrate-sso {:team-id team-id :url (rt/get-current-href)})
@ -116,13 +116,13 @@
(with-refreshed-team team-id
(fn [team]
(let [organization (:organization team)
in-organization? (and (contains? cf/flags :nitrate) organization)
in-organization? (and (contains? cf/flags :admin-console) organization)
can-create? (if in-organization?
(nitrate-perms/allowed? :create-team
{:organization-perms {:owner-id (:owner-id organization)
:permissions (:permissions organization)}
:profile-id profile-id
:team-perms (:permissions team)})
(cto/allowed? :create-team
{:organization-perms {:owner-id (:owner-id organization)
:permissions (:permissions organization)}
:profile-id profile-id
:team-perms (:permissions team)})
true)]
(rx/of (if can-create?
(modal/show :team-form (if in-organization?
@ -142,13 +142,13 @@
(with-refreshed-team team-id
(fn [team]
(let [organization (:organization team)
in-organization? (and (contains? cf/flags :nitrate) organization)
in-organization? (and (contains? cf/flags :admin-console) organization)
can-delete? (if in-organization?
(nitrate-perms/allowed? :delete-team
{:organization-perms {:owner-id (:owner-id organization)
:permissions (:permissions organization)}
:profile-id profile-id
:team-perms (:permissions team)})
(cto/allowed? :delete-team
{:organization-perms {:owner-id (:owner-id organization)
:permissions (:permissions organization)}
:profile-id profile-id
:team-perms (:permissions team)})
(boolean (dm/get-in team [:permissions :is-owner])))
message (if in-organization?
(tr "modals.delete-organization-team-confirm.message" (:name organization))
@ -173,9 +173,9 @@
:team team
:origin (or origin :team)
:invite-email invite-email}))]
(if (and (contains? cf/flags :nitrate)
(not (nitrate-perms/allowed? :add-anybody-to-team
{:organization-perms (:organization team)})))
(if (and (contains? cf/flags :admin-console)
(not (cto/allowed? :add-anybody-to-team
{:organization-perms (:organization team)})))
(->> (rp/cmd! :all-organization-members-in-team
{:team-id (:id team)
:organization-id (get-in team [:organization :id])})
@ -203,9 +203,8 @@
(with-refreshed-team team-id
(fn [team]
(let [organization (:organization team)
can-invite? (nitrate-perms/can-send-invitations?
{:nitrate-enabled? (contains? cf/flags :nitrate)
:organization organization
can-invite? (cto/can-send-invitations?
{:organization organization
:profile-id profile-id
:team-permissions (:permissions team)})]
(rx/of (if can-invite?
@ -615,11 +614,11 @@
(ptk/reify ::check-and-submit-invite-members
ptk/WatchEvent
(watch [_ _ _]
(if (contains? cf/flags :nitrate)
(if (contains? cf/flags :admin-console)
(with-refreshed-team team-id
(fn [team]
(if (not (nitrate-perms/allowed? :add-anybody-to-team
{:organization-perms (:organization team)}))
(if (not (cto/allowed? :add-anybody-to-team
{:organization-perms (:organization team)}))
(->> (rp/cmd! :check-organization-members {:organization-id (get-in team [:organization :id])
:emails (vec (:emails params))})
(rx/mapcat

View File

@ -418,7 +418,7 @@
(mf/use-fn
(mf/deps team)
(fn []
(if (contains? cf/flags :nitrate)
(if (contains? cf/flags :admin-console)
(st/emit! (dtm/check-and-create-team (:id team)))
(st/emit! (modal/show :team-form {})))))
@ -434,15 +434,15 @@
[:> dropdown-menu-item* {:on-click on-team-click
:data-value default-team-id
:class (stl/css-case :team-dropdown-item true
:team-dropdown-item-no-logo (contains? cf/flags :nitrate))}
(when-not (contains? cf/flags :nitrate)
:team-dropdown-item-no-logo (contains? cf/flags :admin-console))}
(when-not (contains? cf/flags :admin-console)
[:span {:class (stl/css :penpot-icon)} deprecated-icon/logo-icon])
[:span {:class (stl/css :team-text)} (if (contains? cf/flags :nitrate) (tr "dashboard.my-files") (tr "dashboard.your-penpot"))]
[:span {:class (stl/css :team-text)} (if (contains? cf/flags :admin-console) (tr "dashboard.my-files") (tr "dashboard.your-penpot"))]
(when (= default-team-id (:id team))
tick-icon)]
(when (and (contains? cf/flags :nitrate)
(when (and (contains? cf/flags :admin-console)
(seq (remove :is-default (vals teams))))
[:*
[:hr {:role "separator" :class (stl/css :team-separator)}]
@ -890,7 +890,7 @@
(mf/defc sidebar-team-switch*
[{:keys [team profile]}]
(let [nitrate? (contains? cf/flags :nitrate)
(let [nitrate? (contains? cf/flags :admin-console)
organization (:organization team)
organization-id (when nitrate? (:id organization))
teams (cond->> (mf/deref refs/teams)
@ -1039,7 +1039,7 @@
overflow* (mf/use-state false)
overflow? (deref overflow*)
nitrate? (contains? cf/flags :nitrate)
nitrate? (contains? cf/flags :admin-console)
focus-timer-ref (use-focus-timer-ref)
@ -1401,7 +1401,7 @@
(dom/open-new-window "https://penpot.app/pricing")))]
(mf/with-effect [teams]
(when (and (contains? cf/flags :nitrate)
(when (and (contains? cf/flags :admin-console)
(empty? teams))
(st/emit! (dtm/fetch-teams))))
@ -1410,7 +1410,7 @@
(reset! sub-menu* nil)))
[:*
(if (contains? cf/flags :nitrate)
(if (contains? cf/flags :admin-console)
[:*
[:> nitrate-sidebar* {:profile profile :teams teams}]
[:> nitrate-current-plan* {:profile profile}]]

View File

@ -1504,7 +1504,7 @@
(mf/defc team-settings-page*
[{:keys [team]}]
(let [nitrate? (contains? cfg/flags :nitrate)
(let [nitrate? (contains? cfg/flags :admin-console)
finput (mf/use-ref)
members (get team :members)

View File

@ -31,7 +31,7 @@
["/recovery" :auth-recovery]
["/verify-token" :auth-verify-token]]
(when (contains? cf/flags :nitrate)
(when (contains? cf/flags :admin-console)
["/subscribe-nitrate" :nitrate-entry])
["/settings"
@ -105,7 +105,7 @@
or redirects to the SSO provider URL."
[match send-event-info? url]
(let [route-name (name (get-in match [:data :name]))
relevant? (and (contains? cf/flags :nitrate)
relevant? (and (contains? cf/flags :admin-console)
(or (str/starts-with? route-name "dashboard")
(str/starts-with? route-name "workspace")))
team-id-str (when relevant?

View File

@ -48,7 +48,7 @@
(with-meta {} {:on-error on-error}))))]
(mf/with-effect []
(if (contains? cf/flags :nitrate)
(if (contains? cf/flags :admin-console)
(let [sub (->> (rp/cmd! :get-owned-organizations-summary {})
(rx/subs!
(fn [result] (reset! organizations* (or result [])))

View File

@ -457,7 +457,7 @@
(-> profile :props :subscription)
subscription-type
(if (and (contains? cf/flags :nitrate) nitrate?) (:type nitrate-license) (get-subscription-type subscription))
(if (and (contains? cf/flags :admin-console) nitrate?) (:type nitrate-license) (get-subscription-type subscription))
subscription-name
(cond
@ -724,7 +724,7 @@
(tr "subscription.settings.professional.teams-editors-benefit")
(tr "subscription.settings.professional.selfhost.community-support"))]
:cta-text (tr "subscription.settings.subscribe")
:cta-link (if (and (contains? cf/flags :nitrate) nitrate?)
:cta-link (if (and (contains? cf/flags :admin-console) nitrate?)
#(open-contact-sales-modal subscription-type "Professional"
(and (:licenses connectivity) (not (:manual nitrate-license))))
go-to-payments)
@ -742,14 +742,14 @@
(tr "subscription.settings.unlimited.autosave-benefit"),
(tr "subscription.settings.unlimited.bill")]
:cta-text (if (:type subscription) (tr "subscription.settings.subscribe") (tr "subscription.settings.try-it-free"))
:cta-link (if (and (contains? cf/flags :nitrate) nitrate?) #(open-contact-sales-modal subscription-type "Unlimited") #(open-subscription-modal "unlimited" subscription))
:cta-link (if (and (contains? cf/flags :admin-console) nitrate?) #(open-contact-sales-modal subscription-type "Unlimited") #(open-subscription-modal "unlimited" subscription))
:cta-text-with-icon (tr "subscription.settings.more-information")
:cta-link-with-icon go-to-pricing-page
:recommended (= subscription-type "professional")
:show-button-cta (= subscription-type "professional")
:current-plan false}])
(when (and (not= subscription-type "enterprise") cf/saas? (not (contains? cf/flags :nitrate)))
(when (and (not= subscription-type "enterprise") cf/saas? (not (contains? cf/flags :admin-console)))
[:> plan-card* {:card-title (tr "subscription.settings.enterprise")
:card-title-icon i/character-e
:price-value "$950"
@ -765,7 +765,7 @@
:show-button-cta (= subscription-type "professional")
:current-plan false}])
(when (and (contains? cf/flags :nitrate) (not nitrate?))
(when (and (contains? cf/flags :admin-console) (not nitrate?))
[:> plan-card* {:card-title (tr "subscription.settings.enterprise")
:price-value "$25"
:price-period (tr "subscription.settings.organization-member-month")

View File

@ -221,7 +221,12 @@
state (-> (ted/insert-text state text style)
(handle-change))]
(st/emit! (dwt/update-editor-state shape state))))
"handled")]
"handled")
handle-drop
(fn [_ _ drag-type]
(when (= drag-type "internal")
"handled"))]
(mf/use-layout-effect on-mount)
@ -246,6 +251,7 @@
:handle-return handle-return
:strip-pasted-styles true
:handle-pasted-text handle-pasted-text
:handle-drop handle-drop
:custom-style-fn (partial styles-fn shape)
:block-renderer-fn #(render-block % shape)
:ref on-editor

View File

@ -27,7 +27,7 @@
{:team {:id team-id :organization organization}
:notification nil})]
(mock/with-mocks
{cf/flags (conj cf/flags :nitrate)
{cf/flags (conj cf/flags :admin-console)
rp/cmd! (mock/stub
(fn [cmd params]
(if (= [:check-nitrate-sso
@ -61,7 +61,7 @@
event (dcm/handle-organization-change-sso
{:organization-id organization-id})]
(mock/with-mocks
{cf/flags (conj cf/flags :nitrate)
{cf/flags (conj cf/flags :admin-console)
rp/cmd! (mock/stub
(fn [cmd params]
(if (= [:check-nitrate-sso