Compare commits

..

1 Commits

Author SHA1 Message Date
Andrey Antukh
0da3e5b479 WIP 2025-12-15 08:05:05 +01:00
1320 changed files with 78987 additions and 201327 deletions

305
.circleci/config.yml Normal file
View File

@@ -0,0 +1,305 @@
version: 2.1
jobs:
lint:
docker:
- image: penpotapp/devenv:latest
working_directory: ~/repo
resource_class: medium+
steps:
- checkout
- run:
name: "fmt check"
working_directory: "."
command: |
yarn install
yarn run fmt:clj:check
- run:
name: "lint clj common"
working_directory: "."
command: |
yarn run lint:clj:common
- run:
name: "lint clj frontend"
working_directory: "."
command: |
yarn run lint:clj:frontend
- run:
name: "lint clj backend"
working_directory: "."
command: |
yarn run lint:clj:backend
- run:
name: "lint clj exporter"
working_directory: "."
command: |
yarn run lint:clj:exporter
- run:
name: "lint clj library"
working_directory: "."
command: |
yarn run lint:clj:library
test-common:
docker:
- image: penpotapp/devenv:latest
working_directory: ~/repo
resource_class: medium+
environment:
JAVA_OPTS: -Xmx4g -Xms100m -XX:+UseSerialGC
NODE_OPTIONS: --max-old-space-size=4096
steps:
- checkout
# Download and cache dependencies
- restore_cache:
keys:
- v1-dependencies-{{ checksum "common/deps.edn"}}-{{ checksum "common/yarn.lock" }}
- run:
name: "JVM tests"
working_directory: "./common"
command: |
clojure -M:dev:test
- run:
name: "NODE tests"
working_directory: "./common"
command: |
yarn install
yarn run test
- save_cache:
paths:
- ~/.m2
- ~/.yarn
- ~/.gitlibs
- ~/.cache/ms-playwright
key: v1-dependencies-{{ checksum "common/deps.edn"}}-{{ checksum "common/yarn.lock" }}
test-frontend:
docker:
- image: penpotapp/devenv:latest
working_directory: ~/repo
resource_class: medium+
environment:
JAVA_OPTS: -Xmx4g -Xms100m -XX:+UseSerialGC
NODE_OPTIONS: --max-old-space-size=4096
steps:
- checkout
# Download and cache dependencies
- restore_cache:
keys:
- v1-dependencies-{{ checksum "frontend/deps.edn"}}-{{ checksum "frontend/yarn.lock" }}
- run:
name: "install dependencies"
working_directory: "./frontend"
# We install playwright here because the dependent tasks
# uses the same cache as this task so we prepopulate it
command: |
yarn install
yarn run playwright install chromium --with-deps
- run:
name: "lint scss on frontend"
working_directory: "./frontend"
command: |
yarn run lint:scss
- run:
name: "unit tests"
working_directory: "./frontend"
command: |
yarn run test
- save_cache:
paths:
- ~/.m2
- ~/.yarn
- ~/.gitlibs
- ~/.cache/ms-playwright
key: v1-dependencies-{{ checksum "frontend/deps.edn"}}-{{ checksum "frontend/yarn.lock" }}
test-library:
docker:
- image: penpotapp/devenv:latest
working_directory: ~/repo
resource_class: medium+
environment:
JAVA_OPTS: -Xmx6g
NODE_OPTIONS: --max-old-space-size=4096
steps:
- checkout
# Download and cache dependencies
- restore_cache:
keys:
- v1-dependencies-{{ checksum "frontend/deps.edn"}}-{{ checksum "frontend/yarn.lock" }}
- run:
name: Install dependencies and build
working_directory: "./library"
command: |
yarn install
- run:
name: Build and Test
working_directory: "./library"
command: |
./scripts/build
yarn run test
test-components:
docker:
- image: penpotapp/devenv:latest
working_directory: ~/repo
resource_class: medium+
environment:
JAVA_OPTS: -Xmx6g -Xms2g
NODE_OPTIONS: --max-old-space-size=4096
steps:
- checkout
# Download and cache dependencies
- restore_cache:
keys:
- v1-dependencies-{{ checksum "frontend/deps.edn"}}-{{ checksum "frontend/yarn.lock" }}
- run:
name: Install dependencies
working_directory: "./frontend"
command: |
yarn install
yarn run playwright install chromium
- run:
name: Build Storybook
working_directory: "./frontend"
command: yarn run build:storybook
- run:
name: Serve Storybook and run tests
working_directory: "./frontend"
command: |
npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \
"npx http-server storybook-static --port 6006 --silent" \
"npx wait-on tcp:6006 && yarn test:storybook"
test-backend:
docker:
- image: penpotapp/devenv:latest
- image: cimg/postgres:14.5
environment:
POSTGRES_USER: penpot_test
POSTGRES_PASSWORD: penpot_test
POSTGRES_DB: penpot_test
- image: cimg/redis:7.0.5
working_directory: ~/repo
resource_class: medium+
environment:
JAVA_OPTS: -Xmx4g -Xms100m -XX:+UseSerialGC
NODE_OPTIONS: --max-old-space-size=4096
steps:
- checkout
- restore_cache:
keys:
- v1-dependencies-{{ checksum "backend/deps.edn" }}
- run:
name: "tests"
working_directory: "./backend"
command: |
clojure -M:dev:test --reporter kaocha.report/documentation
environment:
PENPOT_TEST_DATABASE_URI: "postgresql://localhost/penpot_test"
PENPOT_TEST_DATABASE_USERNAME: penpot_test
PENPOT_TEST_DATABASE_PASSWORD: penpot_test
PENPOT_TEST_REDIS_URI: "redis://localhost/1"
- save_cache:
paths:
- ~/.m2
- ~/.gitlibs
key: v1-dependencies-{{ checksum "backend/deps.edn" }}
test-render-wasm:
docker:
- image: penpotapp/devenv:latest
working_directory: ~/repo
resource_class: medium+
environment:
steps:
- checkout
- run:
name: "fmt check"
working_directory: "./render-wasm"
command: |
cargo fmt --check
- run:
name: "lint"
working_directory: "./render-wasm"
command: |
./lint
- run:
name: "cargo tests"
working_directory: "./render-wasm"
command: |
./test
workflows:
penpot:
jobs:
- test-frontend:
requires:
- lint: success
- test-library:
requires:
- lint: success
- test-components:
requires:
- lint: success
- test-backend:
requires:
- lint: success
- test-common:
requires:
- lint: success
- lint
- test-render-wasm

View File

@@ -45,15 +45,6 @@
:potok/reify-type :potok/reify-type
{:level :error} {:level :error}
:redundant-primitive-coercion
{:level :off}
:unused-excluded-var
{:level :off}
:unresolved-excluded-var
{:level :off}
:missing-protocol-method :missing-protocol-method
{:level :off} {:level :off}

View File

@@ -2,11 +2,6 @@
:remove-multiple-non-indenting-spaces? false :remove-multiple-non-indenting-spaces? false
:remove-surrounding-whitespace? true :remove-surrounding-whitespace? true
:remove-consecutive-blank-lines? false :remove-consecutive-blank-lines? false
:indent-line-comments? true
:parallel? true
:align-form-columns? false
;; :align-map-columns? false
;; :align-single-column-lines? false
:extra-indents {rumext.v2/fnc [[:inner 0]] :extra-indents {rumext.v2/fnc [[:inner 0]]
cljs.test/async [[:inner 0]] cljs.test/async [[:inner 0]]
promesa.exec/thread [[:inner 0]] promesa.exec/thread [[:inner 0]]

View File

@@ -1,38 +0,0 @@
---
name: New Render Bug Report
about: Create a report about the bugs you have found in the new render
title: ''
labels: new render
assignees: claragvinola
---
**Describe the bug**
A clear and concise description of what the bug is.
**Steps to Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots or screen recordings**
If applicable, add screenshots or screen recording to help illustrate your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -40,7 +40,7 @@ on:
jobs: jobs:
build-bundle: build-bundle:
name: Build and Upload Penpot Bundle name: Build and Upload Penpot Bundle
runs-on: penpot-runner-01 runs-on: ubuntu-24.04
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

View File

@@ -1,7 +1,6 @@
name: _DEVELOP name: _DEVELOP
on: on:
workflow_dispatch:
schedule: schedule:
- cron: '16 5-20 * * 1-5' - cron: '16 5-20 * * 1-5'

View File

@@ -7,14 +7,9 @@ jobs:
build-and-push: build-and-push:
name: Build and push DevEnv Docker image name: Build and push DevEnv Docker image
environment: release-admins environment: release-admins
runs-on: penpot-runner-02 runs-on: ubuntu-24.04
steps: steps:
- name: Set common environment variables
run: |
# Each job execution will use its own docker configuration.
echo "DOCKER_CONFIG=${{ runner.temp }}/.docker-${{ github.run_id }}-${{ github.job }}" >> $GITHUB_ENV
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4

View File

@@ -19,14 +19,9 @@ on:
jobs: jobs:
build-and-push: build-and-push:
name: Build and Push Penpot Docker Images name: Build and Push Penpot Docker Images
runs-on: penpot-runner-02 runs-on: ubuntu-24.04-arm
steps: steps:
- name: Set common environment variables
run: |
# Each job execution will use its own docker configuration.
echo "DOCKER_CONFIG=${{ runner.temp }}/.docker-${{ github.run_id }}-${{ github.job }}" >> $GITHUB_ENV
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
@@ -59,7 +54,6 @@ jobs:
mv penpot/frontend bundle-frontend mv penpot/frontend bundle-frontend
mv penpot/exporter bundle-exporter mv penpot/exporter bundle-exporter
mv penpot/storybook bundle-storybook mv penpot/storybook bundle-storybook
mv penpot/mcp bundle-mcp
popd popd
- name: Set up Docker Buildx - name: Set up Docker Buildx
@@ -72,15 +66,6 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
# To avoid the “429 Too Many Requests” error when downloading
# images from DockerHub for unregistered users.
# https://docs.docker.com/docker-hub/usage/
- name: Login to DockerHub Registry
uses: docker/login-action@v3
with:
username: ${{ secrets.PUB_DOCKER_USERNAME }}
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) - name: Extract metadata (tags, labels)
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
@@ -90,7 +75,6 @@ jobs:
backend backend
exporter exporter
storybook storybook
mcp
labels: | labels: |
bundle_version=${{ steps.bundles.outputs.bundle_version }} bundle_version=${{ steps.bundles.outputs.bundle_version }}
@@ -154,21 +138,6 @@ jobs:
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
- name: Build and push MCP Docker image
uses: docker/build-push-action@v6
env:
DOCKER_IMAGE: 'mcp'
BUNDLE_PATH: './bundle-mcp'
with:
context: ./docker/images/
file: ./docker/images/Dockerfile.mcp
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
- name: Notify Mattermost - name: Notify Mattermost
if: failure() if: failure()
uses: mattermost/action-mattermost-notify@master uses: mattermost/action-mattermost-notify@master

View File

@@ -1,15 +0,0 @@
name: _STAGING RENDER
on:
workflow_dispatch:
schedule:
- cron: '36 5-20 * * 1-5'
jobs:
build-bundle:
uses: ./.github/workflows/build-bundle.yml
secrets: inherit
with:
gh_ref: "staging-render"
build_wasm: "yes"
build_storybook: "yes"

View File

@@ -1,7 +1,6 @@
name: _STAGING name: _STAGING
on: on:
workflow_dispatch:
schedule: schedule:
- cron: '36 5-20 * * 1-5' - cron: '36 5-20 * * 1-5'

View File

@@ -1,7 +1,6 @@
name: _TAG name: _TAG
on: on:
workflow_dispatch:
push: push:
tags: tags:
- '*' - '*'
@@ -34,7 +33,7 @@ jobs:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }} MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd MATTERMOST_CHANNEL: bot-alerts-cicd
TEXT: | TEXT: |
🐳 *[PENPOT] Docker image available: ${{ github.ref_name }}* 🐳 *[PENPOT] Docker image available.*
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} 🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
@infra @infra

View File

@@ -26,7 +26,7 @@ jobs:
- name: Check Commit Type - name: Check Commit Type
uses: gsactions/commit-message-checker@v2 uses: gsactions/commit-message-checker@v2
with: with:
pattern: '^(((:(lipstick|globe_with_meridians|wrench|books|arrow_up|arrow_down|zap|ambulance|construction|boom|fire|whale|bug|sparkles|paperclip|tada|recycle|rewind|construction_worker):)\s[A-Z].*[^.])|(Merge|Revert|Reapply).+[^.])$' pattern: '^(((:(lipstick|globe_with_meridians|wrench|books|arrow_up|arrow_down|zap|ambulance|construction|boom|fire|whale|bug|sparkles|paperclip|tada|recycle|rewind|construction_worker):)\s[A-Z].*[^.])|(Merge|Revert).+[^.])$'
flags: 'gm' flags: 'gm'
error: 'Commit should match CONTRIBUTING.md guideline' error: 'Commit should match CONTRIBUTING.md guideline'
checkAllCommitMessages: 'true' # optional: this checks all commits associated with a pull request checkAllCommitMessages: 'true' # optional: this checks all commits associated with a pull request

View File

@@ -1,125 +0,0 @@
name: Plugins/api-doc deployer
on:
push:
branches:
- develop
- staging
- main
paths:
- 'plugins/libs/plugin-types/index.d.ts'
- 'plugins/libs/plugin-types/REAME.md'
- 'plugins/tools/typedoc.css'
- 'plugins/CHANGELOG.md'
- 'plugins/wrangler-penpot-plugins-api-doc.toml'
workflow_dispatch:
inputs:
gh_ref:
description: 'Name of the branch'
type: choice
required: true
default: 'develop'
options:
- develop
- staging
- main
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Extract some useful variables
id: vars
run: |
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ steps.vars.outputs.gh_ref }}
# START: Setup Node and PNPM enabling cache
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
- name: Enable PNPM
working-directory: ./plugins
shell: bash
run: |
corepack enable;
corepack install;
- name: Get pnpm store path
id: pnpm-store
working-directory: ./plugins
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-store.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('plugins/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
# END: Setup Node and PNPM enabling cache
- name: Install deps
working-directory: ./plugins
shell: bash
run: |
pnpm install --no-frozen-lockfile;
pnpm add -D -w wrangler@latest;
- name: Build docs
working-directory: plugins
shell: bash
run: pnpm run build:doc
- name: Select Worker name
run: |
REF="${{ steps.vars.outputs.gh_ref }}"
case "$REF" in
main)
echo "WORKER_NAME=penpot-plugins-api-doc-pro" >> $GITHUB_ENV
echo "WORKER_URI=doc.plugins.penpot.app" >> $GITHUB_ENV ;;
staging)
echo "WORKER_NAME=penpot-plugins-api-doc-pre" >> $GITHUB_ENV
echo "WORKER_URI=doc.plugins.penpot.dev" >> $GITHUB_ENV ;;
develop)
echo "WORKER_NAME=penpot-plugins-api-doc-hourly" >> $GITHUB_ENV
echo "WORKER_URI=doc.plugins.hourly.penpot.dev" >> $GITHUB_ENV ;;
*) echo "Unsupported branch ${REF}" && exit 1 ;;
esac
- name: Set the custom url
working-directory: plugins
shell: bash
run: |
sed -i "s/WORKER_URI/${{ env.WORKER_URI }}/g" wrangler-penpot-plugins-api-doc.toml
- name: Deploy to Cloudflare Workers
uses: cloudflare/wrangler-action@v3
with:
workingDirectory: plugins
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy --config wrangler-penpot-plugins-api-doc.toml --name ${{ env.WORKER_NAME }}
- name: Notify Mattermost
if: failure()
uses: mattermost/action-mattermost-notify@master
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
TEXT: |
❌ 🧩📚 *[PENPOT PLUGINS] Error deploying API documentation.*
📄 Triggered from ref: `${{ inputs.gh_ref }}`
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
@infra

View File

@@ -1,127 +0,0 @@
name: Plugins/package deployer
on:
# Deploy package from manual action
workflow_dispatch:
inputs:
gh_ref:
description: 'Name of the branch'
type: choice
required: true
default: 'develop'
options:
- develop
- staging
- main
plugin_name:
description: 'Pluging name (like plugins/apps/<plugin_name>-plugin)'
type: string
required: true
workflow_call:
inputs:
gh_ref:
description: 'Name of the branch'
type: string
required: true
default: 'develop'
plugin_name:
description: 'Publig name (from plugins/apps/<plugin_name>-plugin)'
type: string
required: true
permissions:
contents: read
jobs:
deploy:
runs-on: penpot-runner-01
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ inputs.gh_ref }}
# START: Setup Node and PNPM enabling cache
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
- name: Enable PNPM
working-directory: ./plugins
shell: bash
run: |
corepack enable;
corepack install;
- name: Get pnpm store path
id: pnpm-store
working-directory: ./plugins
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-store.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('plugins/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
# END: Setup Node and PNPM enabling cache
- name: Install deps
working-directory: ./plugins
shell: bash
run: |
pnpm install --no-frozen-lockfile;
pnpm add -D -w wrangler@latest;
- name: "Build package for ${{ inputs.plugin_name }}-plugin"
working-directory: plugins
shell: bash
run: pnpm --filter ${{ inputs.plugin_name }}-plugin build
- name: Select Worker name
run: |
REF="${{ inputs.gh_ref }}"
case "$REF" in
main)
echo "WORKER_NAME=${{ inputs.plugin_name }}-plugin-pro" >> $GITHUB_ENV
echo "WORKER_URI=${{ inputs.plugin_name }}.plugins.penpot.app" >> $GITHUB_ENV ;;
staging)
echo "WORKER_NAME=${{ inputs.plugin_name }}-plugin-pre" >> $GITHUB_ENV
echo "WORKER_URI=${{ inputs.plugin_name }}.plugins.penpot.dev" >> $GITHUB_ENV ;;
develop)
echo "WORKER_NAME=${{ inputs.plugin_name }}-plugin-hourly" >> $GITHUB_ENV
echo "WORKER_URI=${{ inputs.plugin_name }}.plugins.hourly.penpot.dev" >> $GITHUB_ENV ;;
*) echo "Unsupported branch ${REF}" && exit 1 ;;
esac
- name: Set the custom url
working-directory: plugins
shell: bash
run: |
sed -i "s/WORKER_URI/${{ env.WORKER_URI }}/g" apps/${{ inputs.plugin_name }}-plugin/wrangler.toml
- name: Deploy to Cloudflare Workers
uses: cloudflare/wrangler-action@v3
with:
workingDirectory: plugins
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy --config apps/${{ inputs.plugin_name }}-plugin/wrangler.toml --name ${{ env.WORKER_NAME }}
- name: Notify Mattermost
if: failure()
uses: mattermost/action-mattermost-notify@master
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
TEXT: |
❌ 🧩📦 *[PENPOT PLUGINS] Error deploying ${{ env.WORKER_NAME }}.*
📄 Triggered from ref: `${{ inputs.gh_ref }}`
Plugin name: `${{ inputs.plugin_name }}-plugin`
Cloudflare worker name: `${{ env.WORKER_NAME }}`
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
@infra

View File

@@ -1,143 +0,0 @@
name: Plugins/packages deployer
on:
push:
branches:
- develop
- staging
- main
paths:
- 'plugins/apps/*-plugin/**'
- 'libs/plugins-styles/**'
workflow_dispatch:
inputs:
gh_ref:
description: 'Name of the branch'
type: choice
required: true
default: 'develop'
options:
- develop
- staging
- main
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
colors_to_tokens: ${{ steps.filter.outputs.colors_to_tokens }}
create_palette: ${{ steps.filter.outputs.create_palette }}
lorem_ipsum: ${{ steps.filter.outputs.lorem_ipsum }}
rename_layers: ${{ steps.filter.outputs.rename_layers }}
contrast: ${{ steps.filter.outputs.contrast }}
icons: ${{ steps.filter.outputs.icons }}
poc_state: ${{ steps.filter.outputs.poc_state }}
table: ${{ steps.filter.outputs.table }}
# [For new plugins]
# Add more outputs here
steps:
- uses: actions/checkout@v4
- id: filter
uses: dorny/paths-filter@v3
with:
filters: |
colors_to_tokens:
- 'plugins/apps/colors-to-tokens-plugin/**'
- 'libs/plugins-styles/**'
contrast:
- 'plugins/apps/contrast-plugin/**'
- 'libs/plugins-styles/**'
create_palette:
- 'plugins/apps/create-palette-plugin/**'
- 'libs/plugins-styles/**'
icons:
- 'plugins/apps/icons-plugin/**'
- 'libs/plugins-styles/**'
lorem_ipsum:
- 'plugins/apps/lorem-ipsum-plugin/**'
- 'libs/plugins-styles/**'
rename_layers:
- 'plugins/apps/rename-layers-plugin/**'
- 'libs/plugins-styles/**'
table:
- 'plugins/apps/table-plugin/**'
- 'libs/plugins-styles/**'
# [For new plugins]
# Add more plugin filters here
# another_plugin:
# - 'plugins/apps/another-plugin/**'
# - 'libs/plugins-styles/**'
colors-to-tokens-plugin:
needs: detect-changes
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.colors_to_tokens == 'true'
uses: ./.github/workflows/plugins-deploy-package.yml
secrets: inherit
with:
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
plugin_name: colors-to-tokens
contrast-plugin:
needs: detect-changes
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.contrast == 'true'
uses: ./.github/workflows/plugins-deploy-package.yml
secrets: inherit
with:
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
plugin_name: contrast
create-palette-plugin:
needs: detect-changes
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.create_palette == 'true'
uses: ./.github/workflows/plugins-deploy-package.yml
secrets: inherit
with:
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
plugin_name: create-palette
icons-plugin:
needs: detect-changes
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.icons == 'true'
uses: ./.github/workflows/plugins-deploy-package.yml
secrets: inherit
with:
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
plugin_name: icons
lorem-ipsum-plugin:
needs: detect-changes
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.lorem_ipsum == 'true'
uses: ./.github/workflows/plugins-deploy-package.yml
secrets: inherit
with:
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
plugin_name: lorem-ipsum
rename-layers-plugin:
needs: detect-changes
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.rename_layers == 'true'
uses: ./.github/workflows/plugins-deploy-package.yml
secrets: inherit
with:
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
plugin_name: rename-layers
table-plugin:
needs: detect-changes
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.table == 'true'
uses: ./.github/workflows/plugins-deploy-package.yml
secrets: inherit
with:
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
plugin_name: table
# [For new plugins]
# Add more jobs for other plugins below, following the same pattern
# another-plugin:
# needs: detect-changes
# if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.another_plugin == 'true'
# uses: ./.github/workflows/plugins-deploy-package.yml
# secrets: inherit
# with:
# gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
# plugin_name: another

View File

@@ -1,123 +0,0 @@
name: Plugins/styles-doc deployer
on:
push:
branches:
- develop
- staging
- main
paths:
- 'plugins/apps/example-styles/**'
- 'plugins/libs/plugins-styles/**'
- 'plugins/wrangler-penpot-plugins-styles-doc.toml'
workflow_dispatch:
inputs:
gh_ref:
description: 'Name of the branch'
type: choice
required: true
default: 'develop'
options:
- develop
- staging
- main
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Extract some useful variables
id: vars
run: |
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ steps.vars.outputs.gh_ref }}
# START: Setup Node and PNPM enabling cache
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
- name: Enable PNPM
working-directory: ./plugins
shell: bash
run: |
corepack enable;
corepack install;
- name: Get pnpm store path
id: pnpm-store
working-directory: ./plugins
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-store.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('plugins/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
# END: Setup Node and PNPM enabling cache
- name: Install deps
working-directory: ./plugins
shell: bash
run: |
pnpm install --no-frozen-lockfile;
pnpm add -D -w wrangler@latest;
- name: Build styles
working-directory: plugins
shell: bash
run: pnpm run build:styles-example
- name: Select Worker name
run: |
REF="${{ steps.vars.outputs.gh_ref }}"
case "$REF" in
main)
echo "WORKER_NAME=penpot-plugins-styles-doc-pro" >> $GITHUB_ENV
echo "WORKER_URI=styles-doc.plugins.penpot.app" >> $GITHUB_ENV ;;
staging)
echo "WORKER_NAME=penpot-plugins-styles-doc-pre" >> $GITHUB_ENV
echo "WORKER_URI=styles-doc.plugins.penpot.dev" >> $GITHUB_ENV ;;
develop)
echo "WORKER_NAME=penpot-plugins-styles-doc-hourly" >> $GITHUB_ENV
echo "WORKER_URI=styles-doc.plugins.hourly.penpot.dev" >> $GITHUB_ENV ;;
*) echo "Unsupported branch ${REF}" && exit 1 ;;
esac
- name: Set the custom url
working-directory: plugins
shell: bash
run: |
sed -i "s/WORKER_URI/${{ env.WORKER_URI }}/g" wrangler-penpot-plugins-styles-doc.toml
- name: Deploy to Cloudflare Workers
uses: cloudflare/wrangler-action@v3
with:
workingDirectory: plugins
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy --config wrangler-penpot-plugins-styles-doc.toml --name ${{ env.WORKER_NAME }}
- name: Notify Mattermost
if: failure()
uses: mattermost/action-mattermost-notify@master
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
TEXT: |
❌ 🧩💅 *[PENPOT PLUGINS] Error deploying Styles documentation.*
📄 Triggered from ref: `${{ inputs.gh_ref }}`
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
@infra

View File

@@ -1,45 +0,0 @@
name: "MCP CI"
on:
pull_request:
branches:
- develop
- staging
- main
types:
- opened
- synchronize
paths:
- 'mcp/**'
push:
branches:
- develop
- staging
- main
paths:
- 'mcp/**'
jobs:
test:
name: "Test"
runs-on: penpot-runner-02
container: penpotapp/devenv:latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup
working-directory: ./mcp
run: ./scripts/setup
- name: Check
working-directory: ./mcp
run: |
pnpm run fmt:check;
pnpm -r run build;
pnpm -r run types:check;

View File

@@ -21,7 +21,7 @@ concurrency:
jobs: jobs:
lint: lint:
name: "Linter" name: "Linter"
runs-on: penpot-runner-02 runs-on: ubuntu-24.04
container: penpotapp/devenv:latest container: penpotapp/devenv:latest
steps: steps:
@@ -34,7 +34,7 @@ jobs:
test-common: test-common:
name: "Common Tests" name: "Common Tests"
runs-on: penpot-runner-02 runs-on: ubuntu-24.04
container: penpotapp/devenv:latest container: penpotapp/devenv:latest
steps: steps:
@@ -51,59 +51,9 @@ jobs:
run: | run: |
./scripts/test ./scripts/test
test-plugins:
name: Plugins Runtime Linter & Tests
runs-on: penpot-runner-02
container: penpotapp/devenv:latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
id: setup-node
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
- name: Install deps
working-directory: ./plugins
shell: bash
run: |
corepack enable;
corepack install;
pnpm install;
- name: Run Lint
working-directory: ./plugins
run: pnpm run lint
- name: Run Format Check
working-directory: ./plugins
run: pnpm run format:check
- name: Run Test
working-directory: ./plugins
run: pnpm run test
- name: Build runtime
working-directory: ./plugins
run: pnpm run build:runtime
- name: Build doc
working-directory: ./plugins
run: pnpm run build:doc
- name: Build plugins
working-directory: ./plugins
run: pnpm run build:plugins
- name: Build styles
working-directory: ./plugins
run: pnpm run build:styles-example
test-frontend: test-frontend:
name: "Frontend Tests" name: "Frontend Tests"
runs-on: penpot-runner-02 runs-on: ubuntu-24.04
container: penpotapp/devenv:latest container: penpotapp/devenv:latest
steps: steps:
@@ -117,14 +67,12 @@ jobs:
- name: Component Tests - name: Component Tests
working-directory: ./frontend working-directory: ./frontend
env:
VITEST_BROWSER_TIMEOUT: 120000
run: | run: |
./scripts/test-components ./scripts/test-components
test-render-wasm: test-render-wasm:
name: "Render WASM Tests" name: "Render WASM Tests"
runs-on: penpot-runner-02 runs-on: ubuntu-24.04
container: penpotapp/devenv:latest container: penpotapp/devenv:latest
steps: steps:
@@ -148,7 +96,7 @@ jobs:
test-backend: test-backend:
name: "Backend Tests" name: "Backend Tests"
runs-on: penpot-runner-02 runs-on: ubuntu-24.04
container: penpotapp/devenv:latest container: penpotapp/devenv:latest
services: services:
@@ -187,7 +135,7 @@ jobs:
test-library: test-library:
name: "Library Tests" name: "Library Tests"
runs-on: penpot-runner-02 runs-on: ubuntu-24.04
container: penpotapp/devenv:latest container: penpotapp/devenv:latest
steps: steps:
@@ -201,7 +149,7 @@ jobs:
build-integration: build-integration:
name: "Build Integration Bundle" name: "Build Integration Bundle"
runs-on: penpot-runner-02 runs-on: ubuntu-24.04
container: penpotapp/devenv:latest container: penpotapp/devenv:latest
steps: steps:
@@ -211,7 +159,17 @@ jobs:
- name: Build Bundle - name: Build Bundle
working-directory: ./frontend working-directory: ./frontend
run: | run: |
./scripts/build 0.0.0 corepack enable;
corepack install;
yarn install
yarn run build:app:assets
yarn run build:app
yarn run build:app:libs
- name: Build WASM
working-directory: "./render-wasm"
run: |
./build release
- name: Store Bundle Cache - name: Store Bundle Cache
uses: actions/cache@v4 uses: actions/cache@v4
@@ -219,10 +177,9 @@ jobs:
key: "integration-bundle-${{ github.sha }}" key: "integration-bundle-${{ github.sha }}"
path: frontend/resources/public path: frontend/resources/public
test-integration-1: test-integration-1:
name: "Integration Tests 1/4" name: "Integration Tests 1/4"
runs-on: penpot-runner-02 runs-on: ubuntu-24.04
container: penpotapp/devenv:latest container: penpotapp/devenv:latest
needs: build-integration needs: build-integration
@@ -252,7 +209,7 @@ jobs:
test-integration-2: test-integration-2:
name: "Integration Tests 2/4" name: "Integration Tests 2/4"
runs-on: penpot-runner-02 runs-on: ubuntu-24.04
container: penpotapp/devenv:latest container: penpotapp/devenv:latest
needs: build-integration needs: build-integration
@@ -282,7 +239,7 @@ jobs:
test-integration-3: test-integration-3:
name: "Integration Tests 3/4" name: "Integration Tests 3/4"
runs-on: penpot-runner-02 runs-on: ubuntu-24.04
container: penpotapp/devenv:latest container: penpotapp/devenv:latest
needs: build-integration needs: build-integration
@@ -312,7 +269,7 @@ jobs:
test-integration-4: test-integration-4:
name: "Integration Tests 4/4" name: "Integration Tests 4/4"
runs-on: penpot-runner-02 runs-on: ubuntu-24.04
container: penpotapp/devenv:latest container: penpotapp/devenv:latest
needs: build-integration needs: build-integration

7
.gitignore vendored
View File

@@ -5,7 +5,6 @@
!.yarn/releases !.yarn/releases
!.yarn/sdks !.yarn/sdks
!.yarn/versions !.yarn/versions
.pnpm-store
*-init.clj *-init.clj
*.css.json *.css.json
*.jar *.jar
@@ -21,7 +20,6 @@
.rebel_readline_history .rebel_readline_history
.repl .repl
.shadow-cljs .shadow-cljs
.pnpm-store/
/*.jpg /*.jpg
/*.md /*.md
/*.png /*.png
@@ -45,7 +43,6 @@
/backend/resources/public/media /backend/resources/public/media
/backend/target/ /backend/target/
/backend/experiments /backend/experiments
/backend/scripts/_env.local
/bundle* /bundle*
/cd.md /cd.md
/clj-profiler/ /clj-profiler/
@@ -56,8 +53,6 @@
/exporter/target /exporter/target
/frontend/.storybook/preview-body.html /frontend/.storybook/preview-body.html
/frontend/.storybook/preview-head.html /frontend/.storybook/preview-head.html
/frontend/playwright-report/
/frontend/text-editor/src/wasm/
/frontend/dist/ /frontend/dist/
/frontend/npm-debug.log /frontend/npm-debug.log
/frontend/out/ /frontend/out/
@@ -66,7 +61,6 @@
/frontend/resources/public/* /frontend/resources/public/*
/frontend/storybook-static/ /frontend/storybook-static/
/frontend/target/ /frontend/target/
/frontend/test-results/
/other/ /other/
/scripts/ /scripts/
/telemetry/ /telemetry/
@@ -77,7 +71,6 @@
/library/target/ /library/target/
/library/*.zip /library/*.zip
/external /external
/penpot-nitrate
clj-profiler/ clj-profiler/
node_modules node_modules

105
.gitpod.yml Normal file
View File

@@ -0,0 +1,105 @@
image:
file: docker/gitpod/Dockerfile
ports:
# nginx
- port: 3449
onOpen: open-preview
# frontend nREPL
- port: 3447
onOpen: ignore
visibility: private
# frontend shadow server
- port: 3448
onOpen: ignore
visibility: private
# backend
- port: 6060
onOpen: ignore
# exporter shadow server
- port: 9630
onOpen: ignore
visibility: private
# exporter http server
- port: 6061
onOpen: ignore
# mailhog web interface
- port: 8025
onOpen: ignore
# mailhog postfix
- port: 1025
onOpen: ignore
# postgres
- port: 5432
onOpen: ignore
# redis
- port: 6379
onOpen: ignore
# openldap
- port: 389
onOpen: ignore
tasks:
# https://github.com/gitpod-io/gitpod/issues/666#issuecomment-534347856
- name: gulp
command: >
cd $GITPOD_REPO_ROOT/frontend/;
yarn && gp sync-done 'frontend-yarn';
npx gulp --theme=${PENPOT_THEME} watch
- name: frontend shadow watch
command: >
cd $GITPOD_REPO_ROOT/frontend/;
gp sync-await 'frontend-yarn';
npx shadow-cljs watch main
- init: gp await-port 5432 && psql -f $GITPOD_REPO_ROOT/docker/gitpod/files/postgresql_init.sql
name: backend
command: >
cd $GITPOD_REPO_ROOT/backend/;
./scripts/start-dev
- name: exporter shadow watch
command:
cd $GITPOD_REPO_ROOT/exporter/;
gp sync-await 'frontend-yarn';
yarn && npx shadow-cljs watch main
- name: exporter web server
command: >
cd $GITPOD_REPO_ROOT/exporter/;
./scripts/wait-and-start.sh
- name: signed terminal
before: >
[[ ! -z ${GNUGPG} ]] &&
cd ~ &&
rm -rf .gnupg &&
echo ${GNUGPG} | base64 -d | tar --no-same-owner -xzvf -
init: >
[[ ! -z ${GNUGPG_KEY} ]] &&
git config --global commit.gpgsign true &&
git config --global user.signingkey ${GNUGPG_KEY}
command: cd $GITPOD_REPO_ROOT
- name: redis
command: redis-server
- before: go get github.com/mailhog/MailHog
name: mailhog
command: MailHog
- name: Nginx
command: >
nginx &&
multitail /var/log/nginx/access.log -I /var/log/nginx/error.log

2
.nvmrc
View File

@@ -1 +1 @@
v22.22.0 v22.19.0

40
.travis.yml Normal file
View File

@@ -0,0 +1,40 @@
dist: xenial
language: generic
sudo: required
cache:
directories:
- $HOME/.m2
services:
- docker
branches:
only:
- master
- develop
install:
- curl -O https://download.clojure.org/install/linux-install-1.10.1.447.sh
- chmod +x linux-install-1.10.1.447.sh
- sudo ./linux-install-1.10.1.447.sh
before_script:
- env | sort
script:
- ./manage.sh build-devenv
- ./manage.sh run-frontend-tests
- ./manage.sh run-backend-tests
- ./manage.sh build-images
- ./manage.sh run
after_script:
- docker images
notifications:
email: false
env:
- NODE_VERSION=10.16.0

11
.yarnrc.yml Normal file
View File

@@ -0,0 +1,11 @@
enableGlobalCache: true
enableImmutableCache: false
enableImmutableInstalls: false
enableTelemetry: false
httpTimeout: 600000
nodeLinker: node-modules

View File

@@ -1,124 +1,6 @@
# CHANGELOG # CHANGELOG
## 2.15.0 (Unreleased) ## 2.12.0 (Unreleased)
### :boom: Breaking changes & Deprecations
### :rocket: Epics and highlights
### :heart: Community contributions (Thank you!)
### :sparkles: New features & Enhancements
- Add MCP server integration [Taiga #13112](https://tree.taiga.io/project/penpot/us/13112), [Taiga #13114](https://tree.taiga.io/project/penpot/us/13114)
- Add woff2 support on user uploaded fonts (by @Nivl) [Github #8248](https://github.com/penpot/penpot/pull/8248)
- Option to download custom fonts (by @dfelinto) [Github #8320](https://github.com/penpot/penpot/issues/8320)
- Add copy as image to clipboard option to workspace context menu (by @dfelinto) [Github #8313](https://github.com/penpot/penpot/pull/8313)
### :bug: Bugs fixed
- Fix Alt/Option to draw shapes from center point (by @offreal) [Github #8361](https://github.com/penpot/penpot/pull/8361)
## 2.14.0 (Unreleased)
### :sparkles: New features & Enhancements
- Access to design tokens in Penpot Plugins [Taiga #8990](https://tree.taiga.io/project/penpot/us/8990)
- Remap references when renaming tokens [Taiga #10202](https://tree.taiga.io/project/penpot/us/10202)
- Tokens panel nested path view [Taiga #9966](https://tree.taiga.io/project/penpot/us/9966)
- Improve usability of lock and hide buttons in the layer panel. [Taiga #12916](https://tree.taiga.io/project/penpot/issue/12916)
- Optimize sidebar performance for deeply nested shapes [Taiga #13017](https://tree.taiga.io/project/penpot/task/13017)
- Remove tokens path node and bulk remove tokens [Taiga #13007](https://tree.taiga.io/project/penpot/us/13007)
- Replace themes management modal radio buttons for switches [Taiga #9215](https://tree.taiga.io/project/penpot/us/9215)
### :bug: Bugs fixed
- Remove whitespaces from asset export filename [Github #8133](https://github.com/penpot/penpot/pull/8133)
- Fix prototype connections lost when switching between variants [Taiga #12812](https://tree.taiga.io/project/penpot/issue/12812)
- Fix wrong image in the onboarding invitation block [Taiga #13040](https://tree.taiga.io/project/penpot/issue/13040)
- Fix wrong register image [Taiga #12955](https://tree.taiga.io/project/penpot/task/12955)
- Fix error message on components doesn't close automatically [Taiga #12012](https://tree.taiga.io/project/penpot/issue/12012)
- Fix incorrect handling of input values on layout gap and padding inputs [Github #8113](https://github.com/penpot/penpot/issues/8113)
- Fix incorrect default option on tokens import dialog [Github #8051](https://github.com/penpot/penpot/pull/8051)
- Fix unhandled exception tokens creation dialog [Github #8110](https://github.com/penpot/penpot/issues/8110)
- Fix displaying a hidden user avatar when there is only one more [Taiga #13058](https://tree.taiga.io/project/penpot/issue/13058)
- Fix unhandled exception on open-new-window helper [Github #7787](https://github.com/penpot/penpot/issues/7787)
- Fix exception on uploading large fonts [Github #8135](https://github.com/penpot/penpot/pull/8135)
- Fix boolean operators in menu for boards [Taiga #13174](https://tree.taiga.io/project/penpot/issue/13174)
- Fix viewer can update library [Taiga #13186](https://tree.taiga.io/project/penpot/issue/13186)
- Fix remove fill affects different element than selected [Taiga #13128](https://tree.taiga.io/project/penpot/issue/13128)
- Fix unable to finish the create account form using keyboard [Taiga #11333](https://tree.taiga.io/project/penpot/issue/11333)
## 2.13.3
### :bug: Bugs fixed
- Revert yetti (http server) update, because that caused a regression on multipart uploads
## 2.13.2
### :bug: Bugs fixed
- Fix modifying shapes by apply negative tokens to border radius [Taiga #13317](https://tree.taiga.io/project/penpot/issue/13317)
- Fix arbitrary file read security issue on create-font-variant rpc method (https://github.com/penpot/penpot/security/advisories/GHSA-xp3f-g8rq-9px2)
## 2.13.1
### :bug: Bugs fixed
- Fix PDF Exporter outputs empty page when board has A4 format [Taiga #13181](https://tree.taiga.io/project/penpot/issue/13181)
## 2.13.0
### :heart: Community contributions (Thank you!)
- Fix mask issues with component swap (by @dfelinto) [Github #7675](https://github.com/penpot/penpot/issues/7675)
### :sparkles: New features & Enhancements
- Add new Box Shadow Tokens [Taiga #10201](https://tree.taiga.io/project/penpot/us/10201)
- Make i18n translation files load on-demand [Taiga #11474](https://tree.taiga.io/project/penpot/us/11474)
- Add deleted files to dashboard [Taiga #8149](https://tree.taiga.io/project/penpot/us/8149)
### :bug: Bugs fixed
- Fix problem when drag+duplicate a full grid [Taiga #12565](https://tree.taiga.io/project/penpot/issue/12565)
- Fix problem when pasting elements in reverse flex layout [Taiga #12460](https://tree.taiga.io/project/penpot/issue/12460)
- Fix wrong board size presets in Android [Taiga #12339](https://tree.taiga.io/project/penpot/issue/12339)
- Fix problem with grid layout components and auto sizing [Github #7797](https://github.com/penpot/penpot/issues/7797)
- Fix some alignments on inspect tab [Taiga #12915](https://tree.taiga.io/project/penpot/issue/12915)
- Fix problem with text editor maintaining previous styles [Taiga #12835](https://tree.taiga.io/project/penpot/issue/12835)
- Fix color assets from shared libraries not appearing as assets in Selected colors panel [Taiga #12957](https://tree.taiga.io/project/penpot/issue/12957)
- Fix CSS generated box-shadow property [Taiga #12997](https://tree.taiga.io/project/penpot/issue/12997)
- Fix inner shadow selector on shadow token [Taiga #12951](https://tree.taiga.io/project/penpot/issue/12951)
- Fix missing text color token from selected shapes in selected colors list [Taiga #12956](https://tree.taiga.io/project/penpot/issue/12956)
- Fix dropdown option width in Guides columns dropdown [Taiga #12959](https://tree.taiga.io/project/penpot/issue/12959)
- Fix typos on download modal [Taiga #12865](https://tree.taiga.io/project/penpot/issue/12865)
- Fix problem with text editor maintaining previous styles [Taiga #12835](https://tree.taiga.io/project/penpot/issue/12835)
- Fix unhandled exception tokens creation dialog [Github #8110](https://github.com/penpot/penpot/issues/8110)
- Fix allow negative spread values on shadow token creation [Taiga #13167](https://tree.taiga.io/project/penpot/issue/13167)
- Fix spanish translations on import export token modal [Taiga #13171](https://tree.taiga.io/project/penpot/issue/13171)
- Remove whitespaces from asset export filename [Github #8133](https://github.com/penpot/penpot/pull/8133)
- Fix exception on uploading large fonts [Github #8135](https://github.com/penpot/penpot/pull/8135)
- Fix unhandled exception on open-new-window helper [Github #7787](https://github.com/penpot/penpot/issues/7787)
- Fix incorrect handling of input values on layout gap and padding inputs [Github #8113](https://github.com/penpot/penpot/issues/8113)
- Fix several race conditions on path editor [Github #8187](https://github.com/penpot/penpot/pull/8187)
- Fix app freeze when introducing an error on a very long token name [Taiga #13214](https://tree.taiga.io/project/penpot/issue/13214)
- Fix import a file with shadow tokens [Taiga #13229](https://tree.taiga.io/project/penpot/issue/13229)
- Fix allow spaces on token description [Taiga #13184](https://tree.taiga.io/project/penpot/issue/13184)
- Fix error when creating a token with an invalid name [Taiga #13219](https://tree.taiga.io/project/penpot/issue/13219)
## 2.12.1
### :bug: Bugs fixed
- Fix setting a portion of text as bold or underline messes things up [Github #7980](https://github.com/penpot/penpot/issues/7980)
- Fix problem with style in fonts input [Taiga #12935](https://tree.taiga.io/project/penpot/issue/12935)
- Fix problem with path editor and right click [Github #7917](https://github.com/penpot/penpot/issues/7917)
## 2.12.0
### :boom: Breaking changes & Deprecations ### :boom: Breaking changes & Deprecations
@@ -129,6 +11,7 @@ The backend RPC API URLS are changed from `/api/rpc/command/<name>` to
compatibility; however, if you are a user of this API, it is strongly compatibility; however, if you are a user of this API, it is strongly
recommended that you adapt your code to use the new PATH. recommended that you adapt your code to use the new PATH.
#### Updated SSO Callback URL #### Updated SSO Callback URL
The OAuth / Single Sign-On (SSO) callback endpoint has changed to The OAuth / Single Sign-On (SSO) callback endpoint has changed to
@@ -161,6 +44,7 @@ This update standardizes all authentication flows under the single URL
and makis it more modular, enabling the ability to configure SSO auth and makis it more modular, enabling the ability to configure SSO auth
provider dinamically. provider dinamically.
#### Changes on default docker compose #### Changes on default docker compose
We have updated the `docker/images/docker-compose.yaml` with a small We have updated the `docker/images/docker-compose.yaml` with a small
@@ -178,7 +62,6 @@ example. It's still usable as before, we just removed the example.
- Ensure consistent snap behavior across all zoom levels [Github #7774](https://github.com/penpot/penpot/pull/7774) by [@Tokytome](https://github.com/Tokytome) - Ensure consistent snap behavior across all zoom levels [Github #7774](https://github.com/penpot/penpot/pull/7774) by [@Tokytome](https://github.com/Tokytome)
- Fix crash in token grid view due to tooltip validation (by @dfelinto) [Github #7887](https://github.com/penpot/penpot/pull/7887) - Fix crash in token grid view due to tooltip validation (by @dfelinto) [Github #7887](https://github.com/penpot/penpot/pull/7887)
- Enable Hindi translations on the application
### :sparkles: New features & Enhancements ### :sparkles: New features & Enhancements
@@ -212,7 +95,6 @@ example. It's still usable as before, we just removed the example.
- Fix switch variants with paths [Taiga #12841](https://tree.taiga.io/project/penpot/issue/12841) - Fix switch variants with paths [Taiga #12841](https://tree.taiga.io/project/penpot/issue/12841)
- Fix referencing typography tokens on font-family tokens [Taiga #12492](https://tree.taiga.io/project/penpot/issue/12492) - Fix referencing typography tokens on font-family tokens [Taiga #12492](https://tree.taiga.io/project/penpot/issue/12492)
- Fix horizontal scroll on layer panel [Taiga #12843](https://tree.taiga.io/project/penpot/issue/12843) - Fix horizontal scroll on layer panel [Taiga #12843](https://tree.taiga.io/project/penpot/issue/12843)
- Fix unicode handling on email template abbreviation filter [Github #7966](https://github.com/penpot/penpot/pull/7966)
## 2.11.1 ## 2.11.1
@@ -224,6 +106,7 @@ example. It's still usable as before, we just removed the example.
- Deprecated configuration variables with the prefix `PENPOT_ASSETS_*`, and will be - Deprecated configuration variables with the prefix `PENPOT_ASSETS_*`, and will be
removed in future versions: removed in future versions:
- The `PENPOT_ASSETS_STORAGE_BACKEND` becomes `PENPOT_OBJECTS_STORAGE_BACKEND` and its - The `PENPOT_ASSETS_STORAGE_BACKEND` becomes `PENPOT_OBJECTS_STORAGE_BACKEND` and its
values passes from (`assets-fs` or `assets-s3`) to (`fs` or `s3`) values passes from (`assets-fs` or `assets-s3`) to (`fs` or `s3`)
- The `PENPOT_STORAGE_ASSETS_FS_DIRECTORY` becomes `PENPOT_OBJECTS_STORAGE_FS_DIRECTORY` - The `PENPOT_STORAGE_ASSETS_FS_DIRECTORY` becomes `PENPOT_OBJECTS_STORAGE_FS_DIRECTORY`

View File

@@ -120,12 +120,17 @@ them on your system, you can run them with:
```bash ```bash
# Check formatting # Check formatting
./scripts/fmt yarn fmt:clj:check
# Lint # Check and fix formatting
./scripts/lint yarn fmt:clj
# Run the linter
yarn lint:clj
``` ```
There are more choices in `package.json`.
Ideally, you should run these commands as git pre-commit hooks. A convenient way Ideally, you should run these commands as git pre-commit hooks. A convenient way
of defining them is to use [Husky](https://typicode.github.io/husky/#/). of defining them is to use [Husky](https://typicode.github.io/husky/#/).

View File

@@ -2,30 +2,4 @@
## Reporting a Vulnerability ## Reporting a Vulnerability
We take the security of this project seriously. If you have discovered Please report security issues to `support@penpot.app`
a security vulnerability, please do **not** open a public issue.
Please report vulnerabilities via email to: **[support@penpot.app]**
### What to include:
* A brief description of the vulnerability.
* Steps to reproduce the issue.
* Potential impact if exploited.
We appreciate your patience and your commitment to **responsible disclosure**.
---
## Security Contributors
We are incredibly grateful to the following individuals and
organizations for their help in keeping this project safe.
* **Ali Maharramli** for identifying critical path traversal vulnerability
> **Note:** This list is a work in progress. If you have contributed
> to the security of this project and would like to be recognized (or
> prefer to remain anonymous), please let us know.

7
backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

View File

@@ -3,7 +3,7 @@
:deps :deps
{penpot/common {:local/root "../common"} {penpot/common {:local/root "../common"}
org.clojure/clojure {:mvn/version "1.12.4"} org.clojure/clojure {:mvn/version "1.12.2"}
org.clojure/tools.namespace {:mvn/version "1.5.0"} org.clojure/tools.namespace {:mvn/version "1.5.0"}
com.github.luben/zstd-jni {:mvn/version "1.5.7-4"} com.github.luben/zstd-jni {:mvn/version "1.5.7-4"}
@@ -39,7 +39,7 @@
metosin/reitit-core {:mvn/version "0.9.1"} metosin/reitit-core {:mvn/version "0.9.1"}
nrepl/nrepl {:mvn/version "1.4.0"} nrepl/nrepl {:mvn/version "1.4.0"}
org.postgresql/postgresql {:mvn/version "42.7.9"} org.postgresql/postgresql {:mvn/version "42.7.7"}
org.xerial/sqlite-jdbc {:mvn/version "3.50.3.0"} org.xerial/sqlite-jdbc {:mvn/version "3.50.3.0"}
com.zaxxer/HikariCP {:mvn/version "7.0.2"} com.zaxxer/HikariCP {:mvn/version "7.0.2"}
@@ -49,7 +49,7 @@
buddy/buddy-hashers {:mvn/version "2.0.167"} buddy/buddy-hashers {:mvn/version "2.0.167"}
buddy/buddy-sign {:mvn/version "3.6.1-359"} buddy/buddy-sign {:mvn/version "3.6.1-359"}
com.github.ben-manes.caffeine/caffeine {:mvn/version "3.2.3"} com.github.ben-manes.caffeine/caffeine {:mvn/version "3.2.2"}
org.jsoup/jsoup {:mvn/version "1.21.2"} org.jsoup/jsoup {:mvn/version "1.21.2"}
org.im4java/im4java org.im4java/im4java
@@ -66,7 +66,7 @@
;; Pretty Print specs ;; Pretty Print specs
pretty-spec/pretty-spec {:mvn/version "0.1.4"} pretty-spec/pretty-spec {:mvn/version "0.1.4"}
software.amazon.awssdk/s3 {:mvn/version "2.41.21"}} software.amazon.awssdk/s3 {:mvn/version "2.33.10"}}
:paths ["src" "resources" "target/classes"] :paths ["src" "resources" "target/classes"]
:aliases :aliases
@@ -97,8 +97,8 @@
:jmx-remote :jmx-remote
{:jvm-opts ["-Dcom.sun.management.jmxremote" {:jvm-opts ["-Dcom.sun.management.jmxremote"
"-Dcom.sun.management.jmxremote.port=9000" "-Dcom.sun.management.jmxremote.port=9090"
"-Dcom.sun.management.jmxremote.rmi.port=9000" "-Dcom.sun.management.jmxremote.rmi.port=9090"
"-Dcom.sun.management.jmxremote.local.only=false" "-Dcom.sun.management.jmxremote.local.only=false"
"-Dcom.sun.management.jmxremote.authenticate=false" "-Dcom.sun.management.jmxremote.authenticate=false"
"-Dcom.sun.management.jmxremote.ssl=false" "-Dcom.sun.management.jmxremote.ssl=false"

View File

@@ -4,7 +4,7 @@
"license": "MPL-2.0", "license": "MPL-2.0",
"author": "Kaleidos INC", "author": "Kaleidos INC",
"private": true, "private": true,
"packageManager": "pnpm@10.26.2+sha512.0e308ff2005fc7410366f154f625f6631ab2b16b1d2e70238444dd6ae9d630a8482d92a451144debc492416896ed16f7b114a86ec68b8404b2443869e68ffda6", "packageManager": "yarn@4.9.2+sha512.1fc009bc09d13cfd0e19efa44cbfc2b9cf6ca61482725eb35bbc5e257e093ebf4130db6dfe15d604ff4b79efd8e1e8e99b25fa7d0a6197c9f9826358d4d65c3c",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/penpot/penpot" "url": "https://github.com/penpot/penpot"

306
backend/pnpm-lock.yaml generated
View File

@@ -1,306 +0,0 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
luxon:
specifier: ^3.4.4
version: 3.7.2
sax:
specifier: ^1.4.1
version: 1.4.3
devDependencies:
nodemon:
specifier: ^3.1.2
version: 3.1.11
source-map-support:
specifier: ^0.5.21
version: 0.5.21
ws:
specifier: ^8.17.0
version: 8.18.3
packages:
anymatch@3.1.3:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
brace-expansion@1.1.12:
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'}
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
has-flag@3.0.0:
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
engines: {node: '>=4'}
ignore-by-default@1.0.1:
resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==}
is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'}
is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
luxon@3.7.2:
resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==}
engines: {node: '>=12'}
minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
nodemon@3.1.11:
resolution: {integrity: sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==}
engines: {node: '>=10'}
hasBin: true
normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
pstree.remy@1.1.8:
resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==}
readdirp@3.6.0:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
sax@1.4.3:
resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==}
semver@7.7.3:
resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
engines: {node: '>=10'}
hasBin: true
simple-update-notifier@2.0.0:
resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
engines: {node: '>=10'}
source-map-support@0.5.21:
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
source-map@0.6.1:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
supports-color@5.5.0:
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
engines: {node: '>=4'}
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
touch@3.1.1:
resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==}
hasBin: true
undefsafe@2.0.5:
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
ws@8.18.3:
resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
snapshots:
anymatch@3.1.3:
dependencies:
normalize-path: 3.0.0
picomatch: 2.3.1
balanced-match@1.0.2: {}
binary-extensions@2.3.0: {}
brace-expansion@1.1.12:
dependencies:
balanced-match: 1.0.2
concat-map: 0.0.1
braces@3.0.3:
dependencies:
fill-range: 7.1.1
buffer-from@1.1.2: {}
chokidar@3.6.0:
dependencies:
anymatch: 3.1.3
braces: 3.0.3
glob-parent: 5.1.2
is-binary-path: 2.1.0
is-glob: 4.0.3
normalize-path: 3.0.0
readdirp: 3.6.0
optionalDependencies:
fsevents: 2.3.3
concat-map@0.0.1: {}
debug@4.4.3(supports-color@5.5.0):
dependencies:
ms: 2.1.3
optionalDependencies:
supports-color: 5.5.0
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
fsevents@2.3.3:
optional: true
glob-parent@5.1.2:
dependencies:
is-glob: 4.0.3
has-flag@3.0.0: {}
ignore-by-default@1.0.1: {}
is-binary-path@2.1.0:
dependencies:
binary-extensions: 2.3.0
is-extglob@2.1.1: {}
is-glob@4.0.3:
dependencies:
is-extglob: 2.1.1
is-number@7.0.0: {}
luxon@3.7.2: {}
minimatch@3.1.2:
dependencies:
brace-expansion: 1.1.12
ms@2.1.3: {}
nodemon@3.1.11:
dependencies:
chokidar: 3.6.0
debug: 4.4.3(supports-color@5.5.0)
ignore-by-default: 1.0.1
minimatch: 3.1.2
pstree.remy: 1.1.8
semver: 7.7.3
simple-update-notifier: 2.0.0
supports-color: 5.5.0
touch: 3.1.1
undefsafe: 2.0.5
normalize-path@3.0.0: {}
picomatch@2.3.1: {}
pstree.remy@1.1.8: {}
readdirp@3.6.0:
dependencies:
picomatch: 2.3.1
sax@1.4.3: {}
semver@7.7.3: {}
simple-update-notifier@2.0.0:
dependencies:
semver: 7.7.3
source-map-support@0.5.21:
dependencies:
buffer-from: 1.1.2
source-map: 0.6.1
source-map@0.6.1: {}
supports-color@5.5.0:
dependencies:
has-flag: 3.0.0
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
touch@3.1.1: {}
undefsafe@2.0.5: {}
ws@8.18.3: {}

View File

View File

@@ -240,4 +240,4 @@
</div> </div>
</body> </body>
</html> </html>

View File

@@ -5,6 +5,7 @@
<meta name="robots" content="noindex,nofollow"> <meta name="robots" content="noindex,nofollow">
<meta http-equiv="x-ua-compatible" content="ie=edge" /> <meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>{% block title %}{% endblock %}</title> <title>{% block title %}{% endblock %}</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=JetBrains+Mono">
<style> <style>
{% include "app/templates/styles.css" %} {% include "app/templates/styles.css" %}
</style> </style>

View File

@@ -12,22 +12,43 @@ Debug Main Page
</nav> </nav>
<main class="dashboard"> <main class="dashboard">
<section class="widget"> <section class="widget">
<fieldset>
<legend>Error reports</legend>
<desc><a href="/dbg/error">CLICK HERE TO SEE THE ERROR REPORTS</a> </desc>
</fieldset>
<fieldset> <fieldset>
<legend>CURRENT PROFILE</legend> <legend>Profile Management</legend>
<desc> <form method="post" action="/dbg/actions/resend-email-verification">
<p> <div class="row">
Name: <b>{{profile.fullname}}</b> <br /> <input type="email" name="email" placeholder="example@example.com" value="" />
Email: <b>{{profile.email}}</b> </div>
</p>
</desc> <div class="row">
<label for="force-verify">Are you sure?</label>
<input id="force-verify" type="checkbox" name="force" />
<br />
<small>
This is a just a security double check for prevent non intentional submits.
</small>
</div>
<div class="row">
<input type="submit" name="resend" value="Resend Verification" />
<input type="submit" name="verify" value="Verify" />
</div>
<div class="row">
<input type="submit" class="danger" name="block" value="Block" />
<input type="submit" class="danger" name="unblock" value="Unblock" />
</div>
</form>
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>VIRTUAL CLOCK</legend> <legend>VIRTUAL CLOCK</legend>
<desc> <desc>
<p><b>IMPORTANT:</b> The virtual clock is profile based and only affects the currently logged-in profile.</p>
<p> <p>
CURRENT CLOCK: <b>{{current-clock}}</b> CURRENT CLOCK: <b>{{current-clock}}</b>
<br /> <br />
@@ -60,93 +81,8 @@ Debug Main Page
</form> </form>
</fieldset> </fieldset>
<fieldset>
<legend>ERROR REPORTS</legend>
<desc><a href="/dbg/error">CLICK HERE TO SEE THE ERROR REPORTS</a> </desc>
</fieldset>
</section> </section>
<section class="widget">
<fieldset>
<legend>Profile Management</legend>
<form method="post" action="/dbg/actions/resend-email-verification">
<div class="row">
<input type="email" name="email" placeholder="example@example.com" value="" />
</div>
<div class="row">
<label for="force-verify">Are you sure?</label>
<input id="force-verify" type="checkbox" name="force" />
<br />
<small>
This is a just a security double check for prevent non intentional submits.
</small>
</div>
<div class="row">
<input type="submit" name="resend" value="Resend Verification" />
<input type="submit" name="verify" value="Verify" />
</div>
<div class="row">
<input type="submit" class="danger" name="block" value="Block" />
<input type="submit" class="danger" name="unblock" value="Unblock" />
</div>
</form>
</fieldset>
<fieldset>
<legend>Feature Flags for Team</legend>
<desc>Add a feature flag to a team</desc>
<form method="post" action="/dbg/actions/handle-team-features">
<div class="row">
<input type="text" style="width:300px" name="team-id" placeholder="team-id" />
</div>
<div class="row">
<select type="text" style="width:100px" name="feature">
{% for feature in supported-features %}
<option value="{{feature}}">{{feature}}</option>
{% endfor %}
</select>
</div>
<div class="row">
<select style="width:100px" name="action">
<option value="">Action...</option>
<option value="show">Show</option>
<option value="enable">Enable</option>
<option value="disable">Disable</option>
</select>
</div>
<div class="row">
<label for="check-feature">Skip feature check</label>
<input id="check-feature" type="checkbox" name="skip-check" />
<br />
<small>
Do not check if the feature is supported
</small>
</div>
<div class="row">
<label for="force-version">Are you sure?</label>
<input id="force-version" type="checkbox" name="force" />
<br />
<small>
This is a just a security double check for prevent non intentional submits.
</small>
</div>
<div class="row">
<input type="submit" value="Submit" />
</div>
</form>
</fieldset>
</section>
<section class="widget"> <section class="widget">
<fieldset> <fieldset>
@@ -237,5 +173,55 @@ Debug Main Page
</form> </form>
</fieldset> </fieldset>
</section> </section>
<section class="widget">
<fieldset>
<legend>Feature Flags for Team</legend>
<desc>Add a feature flag to a team</desc>
<form method="post" action="/dbg/actions/handle-team-features">
<div class="row">
<input type="text" style="width:300px" name="team-id" placeholder="team-id" />
</div>
<div class="row">
<select type="text" style="width:100px" name="feature">
{% for feature in supported-features %}
<option value="{{feature}}">{{feature}}</option>
{% endfor %}
</select>
</div>
<div class="row">
<select style="width:100px" name="action">
<option value="">Action...</option>
<option value="show">Show</option>
<option value="enable">Enable</option>
<option value="disable">Disable</option>
</select>
</div>
<div class="row">
<label for="check-feature">Skip feature check</label>
<input id="check-feature" type="checkbox" name="skip-check" />
<br />
<small>
Do not check if the feature is supported
</small>
</div>
<div class="row">
<label for="force-version">Are you sure?</label>
<input id="force-version" type="checkbox" name="force" />
<br />
<small>
This is a just a security double check for prevent non intentional submits.
</small>
</div>
<div class="row">
<input type="submit" value="Submit" />
</div>
</form>
</fieldset>
</section>
</main> </main>
{% endblock %} {% endblock %}

View File

@@ -5,26 +5,23 @@ penpot - error list
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<nav> <nav>
<div class="title"> <div class="title">
<a href="/dbg"> [BACK]</a> <h1>Error reports (last 200)
<h1>Error reports (last 300)</h1> <a href="/dbg">[GO BACK]</a>
</h1>
<a class="{% if version = 3 %}strong{% endif %}" href="?version=3">[BACKEND ERRORS]</a> </div>
<a class="{% if version = 4 %}strong{% endif %}" href="?version=4">[FRONTEND ERRORS]</a> </nav>
<a class="{% if version = 5 %}strong{% endif %}" href="?version=5">[RLIMIT REPORTS]</a> <main class="horizontal-list">
</div> <ul>
</nav> {% for item in items %}
<main class="horizontal-list"> <li>
<ul> <a class="date" href="/dbg/error/{{item.id}}">{{item.created-at}}</a>
{% for item in items %} <a class="hint" href="/dbg/error/{{item.id}}">
<li> <span class="title">{{item.hint|abbreviate:150}}</span>
<a class="date" href="/dbg/error/{{item.id}}">{{item.created-at}}</a> </a>
<a class="hint" href="/dbg/error/{{item.id}}"> </li>
<span class="title">{{item.hint|abbreviate:150}}</span> {% endfor %}
</a> </ul>
</li> </main>
{% endfor %}
</ul>
</main>
{% endblock %} {% endblock %}

View File

@@ -6,7 +6,7 @@ Report: {{hint|abbreviate:150}} - {{id}} - Penpot Error Report (v3)
{% block content %} {% block content %}
<nav> <nav>
<div>[<a href="/dbg/error?version={{version}}">⮜</a>]</div> <div>[<a href="/dbg/error">⮜</a>]</div>
<div>[<a href="#head">head</a>]</div> <div>[<a href="#head">head</a>]</div>
<div>[<a href="#props">props</a>]</div> <div>[<a href="#props">props</a>]</div>
<div>[<a href="#context">context</a>]</div> <div>[<a href="#context">context</a>]</div>

View File

@@ -1,46 +0,0 @@
{% extends "app/templates/base.tmpl" %}
{% block title %}
Report: {{hint|abbreviate:150}} - {{id}} - Penpot Error Report (v4)
{% endblock %}
{% block content %}
<nav>
<div>[<a href="/dbg/error?version={{version}}">⮜</a>]</div>
<div>[<a href="#head">head</a>]</div>
<!-- <div>[<a href="#props">props</a>]</div> -->
<div>[<a href="#context">context</a>]</div>
{% if report %}
<div>[<a href="#report">report</a>]</div>
{% endif %}
</nav>
<main>
<div class="table">
<div class="table-row multiline">
<div id="head" class="table-key">HEAD</div>
<div class="table-val">
<h1><span class="not-important">Hint:</span> <br/> {{hint}}</h1>
<h2><span class="not-important">Reported at:</span> <br/> {{created-at}}</h2>
<h2><span class="not-important">Report ID:</span> <br/> {{id}}</h2>
</div>
</div>
<div class="table-row multiline">
<div id="context" class="table-key">CONTEXT: </div>
<div class="table-val">
<pre>{{context}}</pre>
</div>
</div>
{% if report %}
<div class="table-row multiline">
<div id="report" class="table-key">REPORT:</div>
<div class="table-val">
<pre>{{report}}</pre>
</div>
</div>
{% endif %}
</div>
</main>
{% endblock %}

View File

@@ -1,40 +0,0 @@
{% extends "app/templates/base.tmpl" %}
{% block title %}
Report: {{hint|abbreviate:150}} - {{id}} - Penpot Rate Limit Report
{% endblock %}
{% block content %}
<nav>
<div>[<a href="/dbg/error?version={{version}}">⮜</a>]</div>
<div>[<a href="#head">head</a>]</div>
<div>[<a href="#context">context</a>]</div>
<div>[<a href="#result">result</a>]</div>
</nav>
<main>
<div class="table">
<div class="table-row multiline">
<div id="head" class="table-key">HEAD:</div>
<div class="table-val">
<h1><span class="not-important">Hint:</span> <br/> {{hint}}</h1>
<h2><span class="not-important">Reported at:</span> <br/> {{created-at}}</h2>
<h2><span class="not-important">Report ID:</span> <br/> {{id}}</h2>
</div>
</div>
<div class="table-row multiline">
<div id="context" class="table-key">CONTEXT: </div>
<div class="table-val">
<pre>{{context}}</pre>
</div>
</div>
<div class="table-row multiline">
<div id="result" class="table-key">RESULT: </div>
<div class="table-val">
<pre>{{result}}</pre>
</div>
</div>
</div>
</main>
{% endblock %}

View File

@@ -1,5 +1,5 @@
* { * {
font-family: monospace; font-family: "JetBrains Mono", monospace;
font-size: 12px; font-size: 12px;
} }
@@ -36,10 +36,6 @@ small {
color: #888; color: #888;
} }
.strong {
font-weight: 900;
}
.not-important { .not-important {
color: #888; color: #888;
font-weight: 200; font-weight: 200;
@@ -61,26 +57,14 @@ nav {
nav > .title { nav > .title {
display: flex; display: flex;
justify-content: center;
width: 100%; width: 100%;
} }
nav > .title > a {
color: black;
text-decoration: none;
}
nav > .title > a.strong {
text-decoration: underline;
}
nav > .title > h1 { nav > .title > h1 {
padding: 0px;
margin: 0px; margin: 0px;
font-size: 11px; font-size: 11px;
display: block;
}
nav > .title > * {
padding: 0px 6px;
} }
nav > div { nav > div {

View File

@@ -3,9 +3,9 @@
{:default {:default
[[:default :window "200000/h"]] [[:default :window "200000/h"]]
;; #{:main/get-teams} ;; #{:command/get-teams}
;; [[:burst :bucket "5/5/5s"]] ;; [[:burst :bucket "5/5/5s"]]
;; #{:main/get-profile} ;; #{:command/get-profile}
;; [[:burst :bucket "60/60/1m"]] ;; [[:burst :bucket "60/60/1m"]]
} }

View File

@@ -1,12 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
export PENPOT_NITRATE_SHARED_KEY=super-secret-nitrate-api-key
export PENPOT_EXPORTER_SHARED_KEY=super-secret-exporter-api-key
export PENPOT_SECRET_KEY=super-secret-devenv-key
# DEPRECATED: only used for subscriptions
export PENPOT_MANAGEMENT_API_KEY=super-secret-management-api-key export PENPOT_MANAGEMENT_API_KEY=super-secret-management-api-key
export PENPOT_SECRET_KEY=super-secret-devenv-key
export PENPOT_HOST=devenv export PENPOT_HOST=devenv
export PENPOT_PUBLIC_URI=https://localhost:3449 export PENPOT_PUBLIC_URI=https://localhost:3449
@@ -18,7 +13,6 @@ export PENPOT_FLAGS="\
disable-login-with-google \ disable-login-with-google \
disable-login-with-github \ disable-login-with-github \
disable-login-with-gitlab \ disable-login-with-gitlab \
disable-telemetry \
enable-backend-worker \ enable-backend-worker \
enable-backend-asserts \ enable-backend-asserts \
disable-feature-fdata-pointer-map \ disable-feature-fdata-pointer-map \
@@ -61,8 +55,6 @@ export PENPOT_OBJECTS_STORAGE_BACKEND=s3
export PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=http://minio:9000 export PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=http://minio:9000
export PENPOT_OBJECTS_STORAGE_S3_BUCKET=penpot export PENPOT_OBJECTS_STORAGE_S3_BUCKET=penpot
export PENPOT_NITRATE_BACKEND_URI=http://localhost:3000/control-center
export JAVA_OPTS="\ export JAVA_OPTS="\
-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \ -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
-Djdk.attach.allowAttachSelf \ -Djdk.attach.allowAttachSelf \

View File

@@ -3,10 +3,6 @@
SCRIPT_DIR=$(dirname $0); SCRIPT_DIR=$(dirname $0);
source $SCRIPT_DIR/_env; source $SCRIPT_DIR/_env;
if [ -f $SCRIPT_DIR/_env.local ]; then
source $SCRIPT_DIR/_env.local;
fi
# Initialize MINIO config # Initialize MINIO config
setup_minio; setup_minio;

View File

@@ -3,11 +3,6 @@
SCRIPT_DIR=$(dirname $0); SCRIPT_DIR=$(dirname $0);
source $SCRIPT_DIR/_env; source $SCRIPT_DIR/_env;
if [ -f $SCRIPT_DIR/_env.local ]; then
source $SCRIPT_DIR/_env.local;
fi
export OPTIONS="-A:dev" export OPTIONS="-A:dev"
entrypoint=${1:-app.main}; entrypoint=${1:-app.main};

View File

@@ -3,10 +3,6 @@
SCRIPT_DIR=$(dirname $0); SCRIPT_DIR=$(dirname $0);
source $SCRIPT_DIR/_env; source $SCRIPT_DIR/_env;
if [ -f $SCRIPT_DIR/_env.local ]; then
source $SCRIPT_DIR/_env.local;
fi
# Initialize MINIO config # Initialize MINIO config
setup_minio; setup_minio;

View File

@@ -36,6 +36,17 @@
[integrant.core :as ig] [integrant.core :as ig]
[yetti.response :as-alias yres])) [yetti.response :as-alias yres]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn obfuscate-string
[s]
(if (< (count s) 10)
(apply str (take (count s) (repeat "*")))
(str (subs s 0 5)
(apply str (take (- (count s) 5) (repeat "*"))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; OIDC PROVIDER (GENERIC) ;; OIDC PROVIDER (GENERIC)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -166,7 +177,7 @@
(l/inf :hint "provider initialized" (l/inf :hint "provider initialized"
:provider (:id provider) :provider (:id provider)
:client-id (:client-id provider) :client-id (:client-id provider)
:client-secret (d/obfuscate-string (:client-secret provider))) :client-secret (obfuscate-string (:client-secret provider)))
provider) provider)
(catch Throwable cause (catch Throwable cause
@@ -211,7 +222,7 @@
(l/inf :hint "provider initialized" (l/inf :hint "provider initialized"
:provider (:id provider) :provider (:id provider)
:client-id (:client-id provider) :client-id (:client-id provider)
:client-secret (d/obfuscate-string (:client-secret provider))) :client-secret (obfuscate-string (:client-secret provider)))
provider) provider)
(catch Throwable cause (catch Throwable cause
@@ -288,7 +299,7 @@
(l/inf :hint "provider initialized" (l/inf :hint "provider initialized"
:provider (:id provider) :provider (:id provider)
:client-id (:client-id provider) :client-id (:client-id provider)
:client-secret (d/obfuscate-string (:client-secret provider))) :client-secret (obfuscate-string (:client-secret provider)))
provider) provider)
(catch Throwable cause (catch Throwable cause
@@ -330,7 +341,7 @@
:provider "gitlab" :provider "gitlab"
:base-uri (:base-uri provider) :base-uri (:base-uri provider)
:client-id (:client-id provider) :client-id (:client-id provider)
:client-secret (d/obfuscate-string (:client-secret provider))) :client-secret (obfuscate-string (:client-secret provider)))
provider) provider)
(catch Throwable cause (catch Throwable cause
(ex/raise :type ::internal (ex/raise :type ::internal
@@ -350,7 +361,7 @@
(l/inf :hint "provider initialized" (l/inf :hint "provider initialized"
:provider (:id provider) :provider (:id provider)
:client-id (:client-id provider) :client-id (:client-id provider)
:client-secret (d/obfuscate-string (:client-secret provider))) :client-secret (obfuscate-string (:client-secret provider)))
provider) provider)
(catch Throwable cause (catch Throwable cause
@@ -448,7 +459,7 @@
(l/trc :hint "fetch access token" (l/trc :hint "fetch access token"
:provider (:id provider) :provider (:id provider)
:client-id (:client-id provider) :client-id (:client-id provider)
:client-secret (d/obfuscate-string (:client-secret provider)) :client-secret (obfuscate-string (:client-secret provider))
:grant-type (:grant_type params) :grant-type (:grant_type params)
:redirect-uri (:redirect_uri params)) :redirect-uri (:redirect_uri params))
@@ -501,7 +512,7 @@
[cfg provider tdata] [cfg provider tdata]
(l/trc :hint "fetch user info" (l/trc :hint "fetch user info"
:uri (:user-uri provider) :uri (:user-uri provider)
:token (d/obfuscate-string (:token/access tdata))) :token (obfuscate-string (:token/access tdata)))
(let [params {:uri (:user-uri provider) (let [params {:uri (:user-uri provider)
:headers {"Authorization" (str (:token/type tdata) " " (:token/access tdata))} :headers {"Authorization" (str (:token/type tdata) " " (:token/access tdata))}

View File

@@ -331,81 +331,6 @@
(set/difference cfeat/backend-only-features)) (set/difference cfeat/backend-only-features))
#{})))) #{}))))
(defn check-file-exists
[cfg id & {:keys [include-deleted?]
:or {include-deleted? false}
:as options}]
(db/get-with-sql cfg [sql:get-minimal-file id]
{:db/remove-deleted (not include-deleted?)}))
(def ^:private sql:file-permissions
"select fpr.is_owner,
fpr.is_admin,
fpr.can_edit
from file_profile_rel as fpr
inner join file as f on (f.id = fpr.file_id)
where fpr.file_id = ?
and fpr.profile_id = ?
union all
select tpr.is_owner,
tpr.is_admin,
tpr.can_edit
from team_profile_rel as tpr
inner join project as p on (p.team_id = tpr.team_id)
inner join file as f on (p.id = f.project_id)
where f.id = ?
and tpr.profile_id = ?
union all
select ppr.is_owner,
ppr.is_admin,
ppr.can_edit
from project_profile_rel as ppr
inner join file as f on (f.project_id = ppr.project_id)
where f.id = ?
and ppr.profile_id = ?")
(defn- get-file-permissions*
[conn profile-id file-id]
(when (and profile-id file-id)
(db/exec! conn [sql:file-permissions
file-id profile-id
file-id profile-id
file-id profile-id])))
(defn get-file-permissions
([conn profile-id file-id]
(let [rows (get-file-permissions* conn profile-id file-id)
is-owner (boolean (some :is-owner rows))
is-admin (boolean (some :is-admin rows))
can-edit (boolean (some :can-edit rows))]
(when (seq rows)
{:type :membership
:is-owner is-owner
:is-admin (or is-owner is-admin)
:can-edit (or is-owner is-admin can-edit)
:can-read true
:is-logged (some? profile-id)})))
([conn profile-id file-id share-id]
(let [perms (get-file-permissions conn profile-id file-id)
ldata (some-> (db/get* conn :share-link {:id share-id :file-id file-id})
(dissoc :flags)
(update :pages db/decode-pgarray #{}))]
;; NOTE: in a future when share-link becomes more powerful and
;; will allow us specify which parts of the app is available, we
;; will probably need to tweak this function in order to expose
;; this flags to the frontend.
(cond
(some? perms) perms
(some? ldata) {:type :share-link
:can-read true
:pages (:pages ldata)
:is-logged (some? profile-id)
:who-comment (:who-comment ldata)
:who-inspect (:who-inspect ldata)}))))
(defn get-project (defn get-project
[cfg project-id] [cfg project-id]
(db/get cfg :project {:id project-id})) (db/get cfg :project {:id project-id}))

View File

@@ -821,10 +821,9 @@
entries (keep (match-storage-entry-fn) entries)] entries (keep (match-storage-entry-fn) entries)]
(doseq [{:keys [id entry]} entries] (doseq [{:keys [id entry]} entries]
(let [object (-> (read-entry input entry) (let [object (->> (read-entry input entry)
(decode-storage-object) (decode-storage-object)
(update :bucket d/nilv sto/default-bucket) (validate-storage-object))
(validate-storage-object))
ext (cmedia/mtype->extension (:content-type object)) ext (cmedia/mtype->extension (:content-type object))
path (str "objects/" id ext) path (str "objects/" id ext)
@@ -873,8 +872,11 @@
(import-storage-objects cfg) (import-storage-objects cfg)
(let [files (get manifest :files) (let [files (get manifest :files)
result (reduce (fn [result file] result (reduce (fn [result {:keys [id] :as file}]
(let [name' (get file :name) (let [name' (get file :name)
name' (if (map? name)
(get name id)
name')
file (assoc file :name name')] file (assoc file :name name')]
(conj result (import-file cfg file)))) (conj result (import-file cfg file))))
[] []

View File

@@ -102,8 +102,6 @@
[:http-server-io-threads {:optional true} ::sm/int] [:http-server-io-threads {:optional true} ::sm/int]
[:http-server-max-worker-threads {:optional true} ::sm/int] [:http-server-max-worker-threads {:optional true} ::sm/int]
[:exporter-shared-key {:optional true} :string]
[:nitrate-shared-key {:optional true} :string]
[:management-api-key {:optional true} :string] [:management-api-key {:optional true} :string]
[:telemetry-uri {:optional true} :string] [:telemetry-uri {:optional true} :string]
@@ -227,8 +225,6 @@
[:netty-io-threads {:optional true} ::sm/int] [:netty-io-threads {:optional true} ::sm/int]
[:executor-threads {:optional true} ::sm/int] [:executor-threads {:optional true} ::sm/int]
[:nitrate-backend-uri {:optional true} ::sm/uri]
;; DEPRECATED ;; DEPRECATED
[:assets-storage-backend {:optional true} :keyword] [:assets-storage-backend {:optional true} :keyword]
[:storage-assets-fs-directory {:optional true} :string] [:storage-assets-fs-directory {:optional true} :string]

View File

@@ -124,6 +124,8 @@
(throw (IllegalArgumentException. "invalid email body provided"))) (throw (IllegalArgumentException. "invalid email body provided")))
(doseq [[name content] attachments] (doseq [[name content] attachments]
(prn "attachment" name)
(let [attachment-part (MimeBodyPart.)] (let [attachment-part (MimeBodyPart.)]
(.setFileName attachment-part ^String name) (.setFileName attachment-part ^String name)
(.setContent attachment-part ^String content (str "text/plain; charset=" charset)) (.setContent attachment-part ^String content (str "text/plain; charset=" charset))

View File

@@ -30,7 +30,7 @@
(defn- get-file-media-object (defn- get-file-media-object
[pool id] [pool id]
(db/get pool :file-media-object {:id id} {::db/remove-deleted false})) (db/get pool :file-media-object {:id id}))
(defn- serve-object-from-s3 (defn- serve-object-from-s3
[{:keys [::sto/storage] :as cfg} obj] [{:keys [::sto/storage] :as cfg} obj]

View File

@@ -49,16 +49,13 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn index-handler (defn index-handler
[cfg request] [_cfg _request]
(let [profile-id (::session/profile-id request) (let [{:keys [clock offset]} @clock/current]
offset (clock/get-offset profile-id)
profile (profile/get-profile cfg profile-id)]
{::yres/status 200 {::yres/status 200
::yres/headers {"content-type" "text/html"} ::yres/headers {"content-type" "text/html"}
::yres/body (-> (io/resource "app/templates/debug.tmpl") ::yres/body (-> (io/resource "app/templates/debug.tmpl")
(tmpl/render {:version (:full cf/version) (tmpl/render {:version (:full cf/version)
:profile profile :current-clock (str clock)
:current-clock ct/*clock*
:current-offset (if offset :current-offset (if offset
(ct/format-duration offset) (ct/format-duration offset)
"NO OFFSET") "NO OFFSET")
@@ -232,30 +229,13 @@
(-> (io/resource "app/templates/error-report.v3.tmpl") (-> (io/resource "app/templates/error-report.v3.tmpl")
(tmpl/render (-> content (tmpl/render (-> content
(assoc :id id) (assoc :id id)
(assoc :version 3)
(assoc :created-at (ct/format-inst created-at :rfc1123))))))
(render-template-v4 [{:keys [content id created-at]}]
(-> (io/resource "app/templates/error-report.v4.tmpl")
(tmpl/render (-> content
(assoc :id id)
(assoc :version 4)
(assoc :created-at (ct/format-inst created-at :rfc1123))))))
(render-template-v5 [{:keys [content id created-at]}]
(-> (io/resource "app/templates/error-report.v5.tmpl")
(tmpl/render (-> content
(assoc :id id)
(assoc :version 5)
(assoc :created-at (ct/format-inst created-at :rfc1123))))))] (assoc :created-at (ct/format-inst created-at :rfc1123))))))]
(if-let [report (get-report request)] (if-let [report (get-report request)]
(let [result (case (:version report) (let [result (case (:version report)
1 (render-template-v1 report) 1 (render-template-v1 report)
2 (render-template-v2 report) 2 (render-template-v2 report)
3 (render-template-v3 report) 3 (render-template-v3 report))]
4 (render-template-v4 report)
5 (render-template-v5 report))]
{::yres/status 200 {::yres/status 200
::yres/body result ::yres/body result
::yres/headers {"content-type" "text/html; charset=utf-8" ::yres/headers {"content-type" "text/html; charset=utf-8"
@@ -263,22 +243,20 @@
{::yres/status 404 {::yres/status 404
::yres/body "not found"}))) ::yres/body "not found"})))
(def ^:private sql:error-reports (def sql:error-reports
"SELECT id, created_at, "SELECT id, created_at,
content->>'~:hint' AS hint content->>'~:hint' AS hint
FROM server_error_report FROM server_error_report
WHERE version = ?
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT 300") LIMIT 200")
(defn- error-list-handler (defn error-list-handler
[{:keys [::db/pool]} {:keys [params]}] [{:keys [::db/pool]} _request]
(let [version (or (some-> (get params :version) parse-long) 3) (let [items (->> (db/exec! pool [sql:error-reports])
items (->> (db/exec! pool [sql:error-reports version]) (map #(update % :created-at ct/format-inst :rfc1123)))]
(map #(update % :created-at ct/format-inst :rfc1123)))]
{::yres/status 200 {::yres/status 200
::yres/body (-> (io/resource "app/templates/error-list.tmpl") ::yres/body (-> (io/resource "app/templates/error-list.tmpl")
(tmpl/render {:items items :version version})) (tmpl/render {:items items}))
::yres/headers {"content-type" "text/html; charset=utf-8" ::yres/headers {"content-type" "text/html; charset=utf-8"
"x-robots-tag" "noindex"}})) "x-robots-tag" "noindex"}}))
@@ -469,16 +447,15 @@
(defn- set-virtual-clock (defn- set-virtual-clock
[_ {:keys [params] :as request}] [_ {:keys [params] :as request}]
(let [offset (some-> params :offset str/trim not-empty ct/duration) (let [offset (some-> params :offset str/trim not-empty ct/duration)
profile-id (::session/profile-id request) reset? (contains? params :reset)]
reset? (contains? params :reset)]
(if (= "production" (cf/get :tenant)) (if (= "production" (cf/get :tenant))
{::yres/status 501 {::yres/status 501
::yres/body "OPERATION NOT ALLOWED"} ::yres/body "OPERATION NOT ALLOWED"}
(do (do
(if (or reset? (zero? (inst-ms offset))) (if (or reset? (zero? (inst-ms offset)))
(clock/assign-offset profile-id nil) (clock/set-offset! nil)
(clock/assign-offset profile-id offset)) (clock/set-offset! offset))
{::yres/status 302 {::yres/status 302
::yres/headers {"location" "/dbg"}})))) ::yres/headers {"location" "/dbg"}}))))
@@ -518,7 +495,7 @@
(defn authorized? (defn authorized?
[pool {:keys [::session/profile-id]}] [pool {:keys [::session/profile-id]}]
(or (and (= "devenv" (cf/get :host)) profile-id) (or (= "devenv" (cf/get :host))
(let [profile (ex/ignoring (profile/get-profile pool profile-id)) (let [profile (ex/ignoring (profile/get-profile pool profile-id))
admins (or (cf/get :admins) #{})] admins (or (cf/get :admins) #{})]
(contains? admins (:email profile))))) (contains? admins (:email profile)))))

View File

@@ -32,7 +32,7 @@
(assoc :request/ip-addr (inet/parse-request request)) (assoc :request/ip-addr (inet/parse-request request))
(assoc :request/profile-id (get claims :uid)) (assoc :request/profile-id (get claims :uid))
(assoc :request/auth-data auth) (assoc :request/auth-data auth)
(assoc :frontend/version (or (yreq/get-header request "x-frontend-version") "unknown"))))) (assoc :version/frontend (or (yreq/get-header request "x-frontend-version") "unknown")))))
(defmulti handle-error (defmulti handle-error
(fn [cause _ _] (fn [cause _ _]

View File

@@ -13,13 +13,13 @@
[app.common.time :as ct] [app.common.time :as ct]
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.http.middleware :as mw]
[app.main :as-alias main] [app.main :as-alias main]
[app.rpc.commands.profile :as cmd.profile] [app.rpc.commands.profile :as cmd.profile]
[app.setup :as-alias setup] [app.setup :as-alias setup]
[app.tokens :as tokens] [app.tokens :as tokens]
[app.worker :as-alias wrk] [app.worker :as-alias wrk]
[integrant.core :as ig] [integrant.core :as ig]
[yetti.request :as yreq]
[yetti.response :as-alias yres])) [yetti.response :as-alias yres]))
;; ---- ROUTES ;; ---- ROUTES
@@ -49,40 +49,28 @@
(fn [cfg request] (fn [cfg request]
(db/tx-run! cfg handler request)))))}) (db/tx-run! cfg handler request)))))})
(def ^:private shared-key-auth
{:name ::shared-key-auth
:compile
(fn [_ _]
(fn [handler key]
(if key
(fn [request]
(if-let [key' (yreq/get-header request "x-shared-key")]
(if (= key key')
(handler request)
{::yres/status 403})
{::yres/status 403}))
(fn [_ _]
{::yres/status 403}))))})
(defmethod ig/init-key ::routes (defmethod ig/init-key ::routes
[_ cfg] [_ {:keys [::setup/props] :as cfg}]
["" {:middleware [[shared-key-auth (cf/get :management-api-key)] (let [management-key (or (cf/get :management-api-key)
[default-system cfg] (get props :management-key))]
[transaction]]}
["/authenticate"
{:handler authenticate
:allowed-methods #{:post}}]
["/get-customer" ["" {:middleware [[mw/shared-key-auth management-key]
{:handler get-customer [default-system cfg]
:transaction true [transaction]]}
:allowed-methods #{:post}}] ["/authenticate"
{:handler authenticate
:allowed-methods #{:post}}]
["/update-customer" ["/get-customer"
{:handler update-customer {:handler get-customer
:allowed-methods #{:post} :transaction true
:transaction true}]]) :allowed-methods #{:post}}]
["/update-customer"
{:handler update-customer
:allowed-methods #{:post}
:transaction true}]]))
;; ---- HELPERS ;; ---- HELPERS

View File

@@ -16,6 +16,7 @@
[app.http.errors :as errors] [app.http.errors :as errors]
[app.tokens :as tokens] [app.tokens :as tokens]
[app.util.pointer-map :as pmap] [app.util.pointer-map :as pmap]
[buddy.core.codecs :as bc]
[cuerdas.core :as str] [cuerdas.core :as str]
[yetti.adapter :as yt] [yetti.adapter :as yt]
[yetti.middleware :as ymw] [yetti.middleware :as ymw]
@@ -213,14 +214,14 @@
(assoc "access-control-allow-origin" origin) (assoc "access-control-allow-origin" origin)
(assoc "access-control-allow-methods" "GET,POST,DELETE,OPTIONS,PUT,HEAD,PATCH") (assoc "access-control-allow-methods" "GET,POST,DELETE,OPTIONS,PUT,HEAD,PATCH")
(assoc "access-control-allow-credentials" "true") (assoc "access-control-allow-credentials" "true")
(assoc "access-control-expose-headers" "content-type, set-cookie") (assoc "access-control-expose-headers" "x-requested-with, content-type, cookie")
(assoc "access-control-allow-headers" "x-frontend-version, x-client, x-requested-width, content-type, accept, cookie"))) (assoc "access-control-allow-headers" "x-frontend-version, content-type, accept, x-requested-width")))
(defn wrap-cors (defn wrap-cors
[handler] [handler]
(fn [request] (fn [request]
(let [response (if (= (yreq/method request) :options) (let [response (if (= (yreq/method request) :options)
{::yres/status 204} {::yres/status 200}
(handler request)) (handler request))
origin (yreq/get-header request "origin")] origin (yreq/get-header request "origin")]
(update response ::yres/headers with-cors-headers origin)))) (update response ::yres/headers with-cors-headers origin))))
@@ -300,20 +301,16 @@
:compile (constantly wrap-auth)}) :compile (constantly wrap-auth)})
(defn- wrap-shared-key-auth (defn- wrap-shared-key-auth
[handler keys] [handler shared-key]
(if (seq keys) (if shared-key
(fn [request] (let [shared-key (if (string? shared-key)
(if-let [[key-id key] (some-> (yreq/get-header request "x-shared-key") shared-key
(str/split #"\s+" 2))] (bc/bytes->b64-str shared-key true))]
(let [key-id (-> key-id str/lower keyword)] (fn [request]
(if (and (string? key) (let [key (yreq/get-header request "x-shared-key")]
(contains? keys key-id) (if (= key shared-key)
(= key (get keys key-id))) (handler request)
(-> request {::yres/status 403}))))
(assoc ::http/auth-key-id key-id)
(handler))
{::yres/status 403}))
{::yres/status 403}))
(fn [_ _] (fn [_ _]
{::yres/status 403}))) {::yres/status 403})))

View File

@@ -20,7 +20,6 @@
[app.http.session.tasks :as-alias tasks] [app.http.session.tasks :as-alias tasks]
[app.main :as-alias main] [app.main :as-alias main]
[app.setup :as-alias setup] [app.setup :as-alias setup]
[app.setup.clock :as clock]
[app.tokens :as tokens] [app.tokens :as tokens]
[integrant.core :as ig] [integrant.core :as ig]
[yetti.request :as yreq] [yetti.request :as yreq]
@@ -230,22 +229,18 @@
(let [{:keys [type token claims metadata]} (get request ::http/auth-data)] (let [{:keys [type token claims metadata]} (get request ::http/auth-data)]
(cond (cond
(= type :cookie) (= type :cookie)
(let [session (let [session (case (:ver metadata)
(case (:ver metadata) ;; BACKWARD COMPATIBILITY WITH OLD TOKENS
;; BACKWARD COMPATIBILITY WITH OLD TOKENS 0 (read-session manager token)
0 (read-session manager token) 1 (some->> (:sid claims) (read-session manager))
1 (some->> (:sid claims) (read-session manager)) nil)
nil)
request request (cond-> request
(cond-> request (some? session)
(some? session) (-> (assoc ::profile-id (:profile-id session))
(-> (assoc ::profile-id (:profile-id session)) (assoc ::session session)))
(assoc ::session session)))
response response (handler request)]
(binding [ct/*clock* (clock/get-clock (:profile-id session))]
(handler request))]
(if (and session (renew-session? session)) (if (and session (renew-session? session))
(let [session (->> session (let [session (->> session

View File

@@ -6,6 +6,7 @@
(ns app.http.sse (ns app.http.sse
"SSE (server sent events) helpers" "SSE (server sent events) helpers"
(:refer-clojure :exclude [tap])
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.logging :as l] [app.common.logging :as l]
@@ -53,7 +54,6 @@
::yres/status 200 ::yres/status 200
::yres/body (yres/stream-body ::yres/body (yres/stream-body
(fn [_ output] (fn [_ output]
(let [channel (sp/chan :buf buf :xf (keep encode)) (let [channel (sp/chan :buf buf :xf (keep encode))
listener (events/spawn-listener listener (events/spawn-listener
channel channel

View File

@@ -9,7 +9,6 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.json :as json]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.time :as ct] [app.common.time :as ct]
@@ -113,8 +112,6 @@
;; COLLECTOR API ;; COLLECTOR API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare ^:private prepare-context-from-request)
;; Defines a service that collects the audit/activity log using ;; Defines a service that collects the audit/activity log using
;; internal database. Later this audit log can be transferred to ;; internal database. Later this audit log can be transferred to
;; an external storage and data cleared. ;; an external storage and data cleared.
@@ -128,8 +125,6 @@
[::props {:optional true} [:map-of :keyword :any]] [::props {:optional true} [:map-of :keyword :any]]
[::context {:optional true} [:map-of :keyword :any]] [::context {:optional true} [:map-of :keyword :any]]
[::tracked-at {:optional true} ::ct/inst] [::tracked-at {:optional true} ::ct/inst]
[::created-at {:optional true} ::ct/inst]
[::source {:optional true} ::sm/text]
[::webhooks/event? {:optional true} ::sm/boolean] [::webhooks/event? {:optional true} ::sm/boolean]
[::webhooks/batch-timeout {:optional true} ::ct/duration] [::webhooks/batch-timeout {:optional true} ::ct/duration]
[::webhooks/batch-key {:optional true} [::webhooks/batch-key {:optional true}
@@ -138,8 +133,32 @@
(def ^:private check-event (def ^:private check-event
(sm/check-fn schema:event)) (sm/check-fn schema:event))
(def valid-event? (defn- prepare-context-from-request
(sm/validator schema:event)) [request]
(let [client-event-origin (get-client-event-origin request)
client-version (get-client-version request)
client-user-agent (get-client-user-agent request)
session-id (get-external-session-id request)
token-id (::actoken/id request)]
(d/without-nils
{:external-session-id session-id
:access-token-id (some-> token-id str)
:client-event-origin client-event-origin
:client-user-agent client-user-agent
:client-version client-version
:version (:full cf/version)})))
(defn event-from-rpc-params
"Create a base event skeleton with pre-filled some important
data that can be extracted from RPC params object"
[params]
(let [context (some-> params meta ::http/request prepare-context-from-request)
event {::type "action"
::profile-id (or (::rpc/profile-id params) uuid/zero)
::ip-addr (::rpc/ip-addr params)}]
(cond-> event
(some? context)
(assoc ::context context))))
(defn prepare-event (defn prepare-event
[cfg mdata params result] [cfg mdata params result]
@@ -151,22 +170,20 @@
uuid/zero) uuid/zero)
props (-> (or (::replace-props resultm) props (-> (or (::replace-props resultm)
(merge params (::props resultm))) (-> params
(merge (::props resultm))
(dissoc :profile-id)
(dissoc :type)))
(clean-props)) (clean-props))
context (merge (::context resultm) context (merge (::context resultm)
(prepare-context-from-request request)) (prepare-context-from-request request))
ip-addr (inet/parse-request request) ip-addr (inet/parse-request request)]
module (get cfg ::rpc/module)]
{::type (or (::type resultm) {::type (or (::type resultm)
(::rpc/type cfg)) (::rpc/type cfg))
::name (or (::name resultm) ::name (or (::name resultm)
(let [sname (::sv/name mdata)] (::sv/name mdata))
(if (not= module "main")
(str module "-" sname)
sname)))
::profile-id profile-id ::profile-id profile-id
::ip-addr ip-addr ::ip-addr ip-addr
::props props ::props props
@@ -190,38 +207,6 @@
(::webhooks/event? resultm) (::webhooks/event? resultm)
false)})) false)}))
(defn- prepare-context-from-request
"Prepare backend event context from request"
[request]
(let [client-event-origin (get-client-event-origin request)
client-version (get-client-version request)
client-user-agent (get-client-user-agent request)
session-id (get-external-session-id request)
key-id (::http/auth-key-id request)
token-id (::actoken/id request)
token-type (::actoken/type request)]
(d/without-nils
{:external-session-id session-id
:initiator (or key-id "app")
:access-token-id (some-> token-id str)
:access-token-type (some-> token-type str)
:client-event-origin client-event-origin
:client-user-agent client-user-agent
:client-version client-version
:version (:full cf/version)})))
(defn event-from-rpc-params
"Create a base event skeleton with pre-filled some important
data that can be extracted from RPC params object"
[params]
(let [context (some-> params meta ::http/request prepare-context-from-request)
event {::type "action"
::profile-id (or (::rpc/profile-id params) uuid/zero)
::ip-addr (::rpc/ip-addr params)}]
(cond-> event
(some? context)
(assoc ::context context))))
(defn- event->params (defn- event->params
[event] [event]
(let [params {:id (uuid/next) (let [params {:id (uuid/next)
@@ -238,7 +223,7 @@
(some? tnow) (some? tnow)
(assoc :tracked-at tnow)))) (assoc :tracked-at tnow))))
(defn- append-audit-entry (defn- append-audit-entry!
[cfg params] [cfg params]
(let [params (-> params (let [params (-> params
(update :props db/tjson) (update :props db/tjson)
@@ -248,26 +233,17 @@
(defn- handle-event! (defn- handle-event!
[cfg event] [cfg event]
(let [tnow (ct/now) (let [params (event->params event)
params (-> (event->params event) tnow (ct/now)]
(assoc :created-at tnow)
(update :tracked-at #(or % tnow)))]
(when (contains? cf/flags :audit-log-logger)
(l/log! ::l/logger "app.audit"
::l/level :info
:profile-id (str (::profile-id event))
:ip-addr (str (::ip-addr event))
:type (::type event)
:name (::name event)
:props (json/encode (::props event) :key-fn json/write-camel-key)
:context (json/encode (::context event) :key-fn json/write-camel-key)))
(when (contains? cf/flags :audit-log) (when (contains? cf/flags :audit-log)
;; NOTE: this operation may cause primary key conflicts on inserts ;; NOTE: this operation may cause primary key conflicts on inserts
;; because of the timestamp precission (two concurrent requests), in ;; because of the timestamp precission (two concurrent requests), in
;; this case we just retry the operation. ;; this case we just retry the operation.
(append-audit-entry cfg params)) (let [params (-> params
(assoc :created-at tnow)
(update :tracked-at #(or % tnow)))]
(append-audit-entry! cfg params)))
(when (and (or (contains? cf/flags :telemetry) (when (and (or (contains? cf/flags :telemetry)
(cf/get :telemetry-enabled)) (cf/get :telemetry-enabled))
@@ -278,9 +254,11 @@
;; ;;
;; NOTE: this is only executed when general audit log is disabled ;; NOTE: this is only executed when general audit log is disabled
(let [params (-> params (let [params (-> params
(assoc :created-at tnow)
(update :tracked-at #(or % tnow))
(assoc :props {}) (assoc :props {})
(assoc :context {}))] (assoc :context {}))]
(append-audit-entry cfg params))) (append-audit-entry! cfg params)))
(when (and (contains? cf/flags :webhooks) (when (and (contains? cf/flags :webhooks)
(::webhooks/event? event)) (::webhooks/event? event))
@@ -334,4 +312,4 @@
params (-> (event->params event) params (-> (event->params event)
(assoc :created-at tnow) (assoc :created-at tnow)
(update :tracked-at #(or % tnow)))] (update :tracked-at #(or % tnow)))]
(append-audit-entry cfg params))))))) (append-audit-entry! cfg params)))))))

View File

@@ -14,8 +14,6 @@
[app.common.schema :as sm] [app.common.schema :as sm]
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.loggers.audit :as audit]
[app.rpc.rlimit :as-alias rlimit]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[integrant.core :as ig] [integrant.core :as ig]
[promesa.exec :as px] [promesa.exec :as px]
@@ -30,144 +28,69 @@
(defonce enabled (atom true)) (defonce enabled (atom true))
(defn- persist-on-database! (defn- persist-on-database!
[pool id version report] [pool id report]
(when-not (db/read-only? pool) (when-not (db/read-only? pool)
(db/insert! pool :server-error-report (db/insert! pool :server-error-report
{:id id {:id id
:version version :version 3
:content (db/tjson report)}))) :content (db/tjson report)})))
(defn- concurrent-exception? (defn record->report
[cause]
(or (instance? java.util.concurrent.CompletionException cause)
(instance? java.util.concurrent.ExecutionException cause)))
(defn- log-record->report
[{:keys [::l/context ::l/message ::l/props ::l/logger ::l/level ::l/cause] :as record}] [{:keys [::l/context ::l/message ::l/props ::l/logger ::l/level ::l/cause] :as record}]
(assert (l/valid-record? record) "expectd valid log record") (assert (l/valid-record? record) "expectd valid log record")
(let [data (if (concurrent-exception? cause) (if (or (instance? java.util.concurrent.CompletionException cause)
(ex-data (ex-cause cause)) (instance? java.util.concurrent.ExecutionException cause))
(ex-data cause)) (-> record
(assoc ::trace (ex/format-throwable cause :data? true :explain? false :header? false :summary? false))
(assoc ::l/cause (ex-cause cause))
(record->report))
ctx (-> context (let [data (ex-data cause)
(assoc :service/tenant (cf/get :tenant)) ctx (-> context
(assoc :service/host (cf/get :host)) (assoc :tenant (cf/get :tenant))
(assoc :service/public-uri (str (cf/get :public-uri))) (assoc :host (cf/get :host))
(assoc :backend/version (:full cf/version)) (assoc :public-uri (str (cf/get :public-uri)))
(assoc :logger/name logger) (assoc :logger/name logger)
(assoc :logger/level level) (assoc :logger/level level)
(dissoc :request/params :value :params :data))] (dissoc :request/params :value :params :data))]
(merge (merge
{:context (-> (into (sorted-map) ctx) {:context (-> (into (sorted-map) ctx)
(pp/pprint-str :length 50)) (pp/pprint-str :length 50))
:props (pp/pprint-str props :length 50) :props (pp/pprint-str props :length 50)
:hint (or (when-let [message (ex-message cause)] :hint (or (when-let [message (ex-message cause)]
(if-let [props-hint (:hint props)] (if-let [props-hint (:hint props)]
(str props-hint ": " message) (str props-hint ": " message)
message)) message))
@message) @message)
:trace (or (::trace record) :trace (or (::trace record)
(some-> cause (ex/format-throwable :data? true :explain? false :header? false :summary? false)))} (some-> cause (ex/format-throwable :data? true :explain? false :header? false :summary? false)))}
(when-let [params (or (:request/params context) (:params context))] (when-let [params (or (:request/params context) (:params context))]
{:params (pp/pprint-str params :length 20 :level 20)}) {:params (pp/pprint-str params :length 20 :level 20)})
(when-let [value (:value context)] (when-let [value (:value context)]
{:value (pp/pprint-str value :length 30 :level 13)}) {:value (pp/pprint-str value :length 30 :level 13)})
(when-let [data (some-> data (dissoc ::s/problems ::s/value ::s/spec ::sm/explain :hint))] (when-let [data (some-> data (dissoc ::s/problems ::s/value ::s/spec ::sm/explain :hint))]
{:data (pp/pprint-str data :length 30 :level 13)}) {:data (pp/pprint-str data :length 30 :level 13)})
(when-let [explain (ex/explain data :length 30 :level 13)] (when-let [explain (ex/explain data :length 30 :level 13)]
{:explain explain})))) {:explain explain})))))
(defn- handle-log-record (defn error-record?
"Convert the log record into a report object and persist it on the database" [{:keys [::l/level]}]
(= :error level))
(defn- handle-event
[{:keys [::db/pool]} {:keys [::l/id] :as record}] [{:keys [::db/pool]} {:keys [::l/id] :as record}]
(try (try
(let [uri (cf/get :public-uri) (let [uri (cf/get :public-uri)
report (-> record log-record->report d/without-nils)] report (-> record record->report d/without-nils)]
(l/dbg :hint "registering error on database" (l/debug :hint "registering error on database" :id id
:id (str id) :uri (str uri "/dbg/error/" id))
:src "logging"
:uri (str uri "/dbg/error/" id))
(persist-on-database! pool id 3 report))
(catch Throwable cause
(l/warn :hint "unexpected exception on database error logger" :cause cause))))
(defn- audit-event->report (persist-on-database! pool id report))
[{:keys [::audit/context ::audit/props ::audit/ip-addr] :as record}]
(let [context
(reduce-kv (fn [context k v]
(let [k' (keyword "frontend" (name k))]
(-> context
(dissoc k)
(assoc k' v))))
context
context)
context
(-> context
(assoc :backend/tenant (cf/get :tenant))
(assoc :backend/host (cf/get :host))
(assoc :backend/public-uri (str (cf/get :public-uri)))
(assoc :backend/version (:full cf/version))
(assoc :frontend/ip-addr ip-addr))]
{:context (-> (into (sorted-map) context)
(pp/pprint-str :length 50))
:props (pp/pprint-str props :length 50)
:hint (get props :hint)
:report (get props :report)}))
(defn- handle-audit-event
"Convert the log record into a report object and persist it on the database"
[{:keys [::db/pool]} {:keys [::audit/id] :as event}]
(try
(let [uri (cf/get :public-uri)
report (-> event audit-event->report d/without-nils)]
(l/dbg :hint "registering error on database"
:id (str id)
:src "audit-log"
:uri (str uri "/dbg/error/" id))
(persist-on-database! pool id 4 report))
(catch Throwable cause
(l/warn :hint "unexpected exception on database error logger" :cause cause))))
(defn- rlimit-event->report
[event]
(let [context
(-> {}
(assoc :rlimit/uid (::rlimit/uid event))
(assoc :rlimit/method (::rlimit/method event))
(assoc :backend/tenant (cf/get :tenant))
(assoc :backend/host (cf/get :host))
(assoc :backend/public-uri (str (cf/get :public-uri)))
(assoc :backend/version (:full cf/version)))
result
(->> (::rlimit/results event)
(mapv (fn [result]
(-> (into (sorted-map) result)
(dissoc ::rlimit/method)))))]
{:hint (str "Rate Limit Rejection: " (::rlimit/method event) " for " (::rlimit/uid event))
:context (-> (into (sorted-map) context)
(pp/pprint-str :length 50))
:result (pp/pprint-str result :length 50)}))
(defn- handle-rlimit-event
"Convert the log record into a report object and persist it on the database"
[{:keys [::db/pool]} {:keys [::rlimit/id] :as event}]
(try
(let [uri (cf/get :public-uri)
report (-> event rlimit-event->report d/without-nils)]
(l/dbg :hint "registering rate limit rejection"
:id (str id)
:src "rlimit"
:uri (str uri "/dbg/error/" id))
(persist-on-database! pool id 5 report))
(catch Throwable cause (catch Throwable cause
(l/warn :hint "unexpected exception on database error logger" :cause cause)))) (l/warn :hint "unexpected exception on database error logger" :cause cause))))
@@ -177,52 +100,26 @@
(defmethod ig/init-key ::reporter (defmethod ig/init-key ::reporter
[_ cfg] [_ cfg]
(let [input (sp/chan :buf (sp/sliding-buffer 256)) (let [input (sp/chan :buf (sp/sliding-buffer 64)
thread (px/thread :xf (filter error-record?))]
{:name "penpot/reporter/database"} (add-watch l/log-record ::reporter #(sp/put! input %4))
(l/info :hint "initializing database error persistence")
(try
(loop []
(when-let [item (sp/take! input)]
(cond
(::l/id item)
(handle-log-record cfg item)
(::audit/id item) (px/thread {:name "penpot/database-reporter"}
(handle-audit-event cfg item) (l/info :hint "initializing database error persistence")
(try
(::rlimit/id item) (loop []
(handle-rlimit-event cfg item) (when-let [record (sp/take! input)]
(handle-event cfg record)
:else (recur)))
(l/warn :hint "received unexpected item" :item item)) (catch InterruptedException _
(l/debug :hint "reporter interrupted"))
(recur))) (catch Throwable cause
(l/error :hint "unexpected error" :cause cause))
(catch InterruptedException _ (finally
(l/debug :hint "reporter interrupted")) (sp/close! input)
(catch Throwable cause (remove-watch l/log-record ::reporter)
(l/error :hint "unexpected error" :cause cause)) (l/info :hint "reporter terminated"))))))
(finally
(l/info :hint "reporter terminated"))))]
(add-watch l/log-record ::reporter
(fn [_ _ _ record]
(when (= :error (::l/level record))
(sp/put! input record))))
{::input input
::thread thread}))
(defmethod ig/halt-key! ::reporter (defmethod ig/halt-key! ::reporter
[_ {:keys [::input ::thread]}] [_ thread]
(remove-watch l/log-record ::reporter) (some-> thread px/interrupt!))
(sp/close! input)
(px/interrupt! thread))
(defn emit
"Emit an event/report into the database reporter"
[cfg event]
(when-let [{:keys [::input]} (get cfg ::reporter)]
(sp/put! input event)))

View File

@@ -9,12 +9,9 @@
(:require (:require
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.logging :as l] [app.common.logging :as l]
[app.common.pprint :as pp]
[app.common.uri :as u]
[app.config :as cf] [app.config :as cf]
[app.http.client :as http] [app.http.client :as http]
[app.loggers.audit :as audit] [app.loggers.database :as ldb]
[app.rpc.rlimit :as-alias rlimit]
[app.util.json :as json] [app.util.json :as json]
[integrant.core :as ig] [integrant.core :as ig]
[promesa.exec :as px] [promesa.exec :as px]
@@ -22,111 +19,62 @@
(defonce enabled (atom true)) (defonce enabled (atom true))
(defn- send-mattermost-notification! (defn- send-mattermost-notification
[cfg {:keys [id] :as report}] [cfg text]
(let [type (get report :type) (let [resp (http/req! cfg
text (str "#" type " | " (get report :hint) "\n"
(when id
(str (u/join (cf/get :public-uri) "/dbg/error/" id) " "))
(when-let [pid (:profile-id report)]
(if (uuid? pid)
(str "(pid: #uuid-" pid ")")
(str "(pid: #ip-" pid ")")))
"\n"
"- host: #" (:host report) "\n"
"- tenant: #" (:tenant report) "\n"
"- origin: #" (:origin report) "\n"
(when-let [href (get report :href)]
(str "- href: `" href "`\n"))
(when-let [version (get report :frontend-version)]
(str "- frontend-version: `" version "`\n"))
(when-let [version (get report :backend-version)]
(str "- backend-version: `" version "`\n"))
"\n"
(when-let [info (:info report)]
(str "```\n" info "```"))
(when-let [trace (:trace report)]
(str "```\n"
"Trace:\n"
trace
"```")))
resp (http/req! cfg
{:uri (cf/get :error-report-webhook) {:uri (cf/get :error-report-webhook)
:method :post :method :post
:headers {"content-type" "application/json"} :headers {"content-type" "application/json"}
:body (json/encode-str {:text text})} :body (json/encode-str {:text text})})]
{:sync? true})]
(when (not= 200 (:status resp)) (when (not= 200 (:status resp))
(l/warn :hint "error on sending data" (l/warn :hint "error on sending data to mattermost"
:response (pr-str resp))))) :response (pr-str resp)))))
(defn- log-record->report (defn- log-record->report
[{:keys [::l/context ::l/id ::l/cause ::l/message] :as record}] [{:keys [::l/context ::l/id ::l/cause] :as record}]
(assert (l/valid-record? record) "expectd valid log record") (assert (l/valid-record? record) "expectd valid log record")
(let [tenant (cf/get :tenant)
host (cf/get :host)
public-uri (cf/get :public-uri)
backend-version (or (:version/backend context) (:full cf/version))
frontend-version (:version/frontend context)
profile-id (:request/profile-id context)
request-path (:request/path context)
logger (::l/logger record)
trace (ex/format-throwable cause :detail? false :header? false)]
(let [public-uri (cf/get :public-uri)] (str "#exception => " public-uri "/dbg/error/" id " "
{:id id (when-let [pid (:profile-id report)]
:type "exception" (str "(pid: #uuid-" pid ")"))
:origin "logging" "\n"
:hint (or (some-> cause ex-message) @message) "- host: #" (:host report) "\n"
:tenant (cf/get :tenant) "- tenant: #" (:tenant report) "\n"
:host (cf/get :host) "- logger: #" (:logger report) "\n"
:backend-version (:full cf/version) "- request-path: `" (:request-path report) "`\n"
:frontend-version (:frontend/version context) "- frontend-version: `" (:frontend-version report) "`\n"
:profile-id (:request/profile-id context) "- backend-version: `" (:backend-version report) "`\n"
:href (-> public-uri "\n"
(assoc :path (:request/path context)) "```\n"
(str)) "Trace:\n"
:trace (ex/format-throwable cause :detail? false :header? false)})) (:trace report)
"```")))
(defn- audit-event->report (defn- process-log-record
[{:keys [::audit/context ::audit/props ::audit/id] :as event}] [cfg record]
{:id id (when (ldb/error-record? record)
:type "exception" (try
:origin "audit-log" (let [report (record->report record)]
:hint (get props :hint) (send-mattermost-notification! cfg report))
:tenant (cf/get :tenant) (catch Throwable cause
:host (cf/get :host) (l/warn :hint "error on processing log record" :cause cause)))))
:backend-version (:full cf/version)
:frontend-version (:version context)
:profile-id (:audit/profile-id event)
:href (get props :href)})
(defn- rlimit-event->report (defn- process-event
[event] [cfg [type event]]
{:id (::rlimit/id event) (when @enabled
:type "notification" (case type
:origin "rlimit" :log-record (process-log-record cfg event))))
:hint (str "rlimit reject of "
(::rlimit/method event)
" for "
(::rlimit/uid event))
:tenant (cf/get :tenant)
:host (cf/get :host)
:backend-version (:full cf/version)
:profile-id (::rlimit/profile-id event)
:info (with-out-str
(println "Rejected by:")
(println "------------")
(println "Method: " (::rlimit/method event))
(println "Limit Name: " (::rlimit/name event))
(println "Limit Strategy:" (::rlimit/strategy event))
(println)
(println "Results & Config:")
(println "-----------------")
(doseq [result (::rlimit/results event)]
(pp/pprint (into (sorted-map) result))))})
(defn- handle-event ;; :xf (filter ldb/error-record?))]
[cfg event event->report]
(try
(let [report (event->report event)]
(send-mattermost-notification! cfg report))
(catch Throwable cause
(l/warn :hint "unhandled error" :cause cause))))
(defmethod ig/assert-key ::reporter (defmethod ig/assert-key ::reporter
[_ params] [_ params]
@@ -135,52 +83,29 @@
(defmethod ig/init-key ::reporter (defmethod ig/init-key ::reporter
[_ cfg] [_ cfg]
(when-let [uri (cf/get :error-report-webhook)] (when-let [uri (cf/get :error-report-webhook)]
(let [input (sp/chan :buf (sp/sliding-buffer 256)) (let [input (sp/chan :buf (sp/sliding-buffer 128)
:xf (keep process-event))
thread (px/thread thread (px/thread
{:name "penpot/reporter/mattermost"} {:name "penpot/mattermost-reporter"}
(l/info :hint "initializing error reporter" :uri uri) (l/info :hint "initializing mattermost reporter thread" :uri uri)
(try (try
(loop [] (loop []
(when-let [item (sp/take! input)] (when-let [msg (sp/take! input)]
(when @enabled (handle-event cfg msg)
(cond
(::l/id item)
(handle-event cfg item log-record->report)
(::audit/id item)
(handle-event cfg item audit-event->report)
(::rlimit/id item)
(handle-event cfg item rlimit-event->report)
:else
(l/warn :hint "received unexpected item" :item item)))
(recur))) (recur)))
(catch InterruptedException _ (catch InterruptedException _
(l/debug :hint "reporter interrupted")) (l/debug :hint "mattermost reporter interrupted"))
(catch Throwable cause (catch Throwable cause
(l/error :hint "unexpected error" :cause cause)) (l/error :hint "unexpected error on mattermost reporter" :cause cause))
(finally (finally
(l/info :hint "reporter terminated"))))] (sp/close! input)
(remove-watch l/log-record ::reporter)
(l/info :hint "mattermost reporter terminated"))))]
(add-watch l/log-record ::reporter (add-watch l/log-record ::reporter #(sp/put! input [:log-record %4]))
(fn [_ _ _ record]
(when (= :error (::l/level record))
(sp/put! input record))))
{::input input input)))
::thread thread})))
(defmethod ig/halt-key! ::reporter (defmethod ig/halt-key! ::reporter
[_ {:keys [::input ::thread]}] [_ thread]
(remove-watch l/log-record ::reporter)
(some-> input sp/close!)
(some-> thread px/interrupt!)) (some-> thread px/interrupt!))
(defn emit
"Emit an event/report into the mattermost reporter"
[cfg event]
(when-let [{:keys [::input]} (get cfg ::reporter)]
(sp/put! input event)))

View File

@@ -317,19 +317,12 @@
::climit/enabled (contains? cf/flags :rpc-climit)} ::climit/enabled (contains? cf/flags :rpc-climit)}
:app.rpc/rlimit :app.rpc/rlimit
{::wrk/executor (ig/ref ::wrk/netty-executor) {::wrk/executor (ig/ref ::wrk/netty-executor)}
:app.loggers.mattermost/reporter
(ig/ref :app.loggers.mattermost/reporter)
:app.loggers.database/reporter
(ig/ref :app.loggers.database/reporter)}
:app.rpc/methods :app.rpc/methods
{::http.client/client (ig/ref ::http.client/client) {::http.client/client (ig/ref ::http.client/client)
::db/pool (ig/ref ::db/pool) ::db/pool (ig/ref ::db/pool)
::rds/pool (ig/ref ::rds/pool) ::rds/pool (ig/ref ::rds/pool)
:app.nitrate/client (ig/ref :app.nitrate/client)
::wrk/executor (ig/ref ::wrk/netty-executor) ::wrk/executor (ig/ref ::wrk/netty-executor)
::session/manager (ig/ref ::session/manager) ::session/manager (ig/ref ::session/manager)
::ldap/provider (ig/ref ::ldap/provider) ::ldap/provider (ig/ref ::ldap/provider)
@@ -344,17 +337,7 @@
::setup/props (ig/ref ::setup/props) ::setup/props (ig/ref ::setup/props)
::email/blacklist (ig/ref ::email/blacklist) ::email/blacklist (ig/ref ::email/blacklist)
::email/whitelist (ig/ref ::email/whitelist) ::email/whitelist (ig/ref ::email/whitelist)}
:app.loggers.database/reporter
(ig/ref :app.loggers.database/reporter)
:app.loggers.mattermost/reporter
(ig/ref :app.loggers.mattermost/reporter)}
:app.nitrate/client
{::http.client/client (ig/ref ::http.client/client)
::setup/shared-keys (ig/ref ::setup/shared-keys)}
:app.rpc/management-methods :app.rpc/management-methods
{::http.client/client (ig/ref ::http.client/client) {::http.client/client (ig/ref ::http.client/client)
@@ -365,19 +348,17 @@
::sto/storage (ig/ref ::sto/storage) ::sto/storage (ig/ref ::sto/storage)
::mtx/metrics (ig/ref ::mtx/metrics) ::mtx/metrics (ig/ref ::mtx/metrics)
::mbus/msgbus (ig/ref ::mbus/msgbus) ::mbus/msgbus (ig/ref ::mbus/msgbus)
:app.nitrate/client (ig/ref :app.nitrate/client)
::rds/client (ig/ref ::rds/client) ::rds/client (ig/ref ::rds/client)
::setup/props (ig/ref ::setup/props)} ::setup/props (ig/ref ::setup/props)}
::rpc/routes ::rpc/routes
{::rpc/methods (ig/ref :app.rpc/methods) {::rpc/methods (ig/ref :app.rpc/methods)
::rpc/management-methods (ig/ref :app.rpc/management-methods) ::rpc/management-methods (ig/ref :app.rpc/management-methods)
;; FIXME: revisit if db/pool is necessary here ;; FIXME: revisit if db/pool is necessary here
::db/pool (ig/ref ::db/pool) ::db/pool (ig/ref ::db/pool)
::session/manager (ig/ref ::session/manager) ::session/manager (ig/ref ::session/manager)
::setup/props (ig/ref ::setup/props) ::setup/props (ig/ref ::setup/props)}
::setup/shared-keys (ig/ref ::setup/shared-keys)}
::wrk/registry ::wrk/registry
{::mtx/metrics (ig/ref ::mtx/metrics) {::mtx/metrics (ig/ref ::mtx/metrics)
@@ -465,11 +446,6 @@
;; module requires the migrations to run before initialize. ;; module requires the migrations to run before initialize.
::migrations (ig/ref :app.migrations/migrations)} ::migrations (ig/ref :app.migrations/migrations)}
::setup/shared-keys
{::setup/props (ig/ref ::setup/props)
:nitrate (cf/get :nitrate-shared-key)
:exporter (cf/get :exporter-shared-key)}
::setup/clock ::setup/clock
{} {}

View File

@@ -35,7 +35,8 @@
javax.xml.parsers.SAXParserFactory javax.xml.parsers.SAXParserFactory
org.apache.commons.io.IOUtils org.apache.commons.io.IOUtils
org.im4java.core.ConvertCmd org.im4java.core.ConvertCmd
org.im4java.core.IMOperation)) org.im4java.core.IMOperation
org.im4java.core.Info))
(def default-max-file-size (def default-max-file-size
(* 1024 1024 10)) ; 10 MiB (* 1024 1024 10)) ; 10 MiB
@@ -54,7 +55,7 @@
[:path ::fs/path] [:path ::fs/path]
[:mtype {:optional true} ::sm/text]]) [:mtype {:optional true} ::sm/text]])
(def check-input (def ^:private check-input
(sm/check-fn schema:input)) (sm/check-fn schema:input))
(defn validate-media-type! (defn validate-media-type!
@@ -223,18 +224,17 @@
;; If we are processing an animated gif we use the first frame with -scene 0 ;; If we are processing an animated gif we use the first frame with -scene 0
(let [dim-result (sh/sh "identify" "-format" "%w %h\n" path) (let [dim-result (sh/sh "identify" "-format" "%w %h\n" path)
orient-result (sh/sh "identify" "-format" "%[EXIF:Orientation]\n" path)] orient-result (sh/sh "identify" "-format" "%[EXIF:Orientation]\n" path)]
(when (= 0 (:exit dim-result)) (if (and (= 0 (:exit dim-result))
(= 0 (:exit orient-result)))
(let [[w h] (-> (:out dim-result) (let [[w h] (-> (:out dim-result)
str/trim str/trim
(clojure.string/split #"\s+") (clojure.string/split #"\s+")
(->> (mapv #(Integer/parseInt %)))) (->> (mapv #(Integer/parseInt %))))
orientation-exit (:exit orient-result) orientation (-> orient-result :out str/trim)]
orientation (-> orient-result :out str/trim)] (case orientation
(if (= 0 orientation-exit) ("6" "8") {:width h :height w} ; Rotated 90 or 270 degrees
(case orientation {:width w :height h})) ; Normal or unknown orientation
("6" "8") {:width h :height w} ; Rotated 90 or 270 degrees nil)))
{:width w :height h}) ; Normal or unknown orientation
{:width w :height h}))))) ; If orientation can't be read, use dimensions as-is
(defmethod process :info (defmethod process :info
[{:keys [input] :as params}] [{:keys [input] :as params}]
@@ -247,37 +247,26 @@
:hint "uploaded svg does not provides dimensions")) :hint "uploaded svg does not provides dimensions"))
(merge input info {:ts (ct/now) :size (fs/size path)})) (merge input info {:ts (ct/now) :size (fs/size path)}))
(let [path-str (str path) (let [instance (Info. (str path))
identify-res (sh/sh "identify" "-format" "image/%[magick]\n" path-str) mtype' (.getProperty instance "Mime type")]
;; identify prints one line per frame (animated GIFs, etc.); we take the first one
mtype' (if (zero? (:exit identify-res))
(-> identify-res
:out
str/trim
(str/split #"\s+" 2)
first
str/lower)
(ex/raise :type :validation
:code :invalid-image
:hint "invalid image"))
{:keys [width height]}
(or (get-dimensions-with-orientation path-str)
(do
(l/warn "Failed to read image dimensions with orientation" {:path path})
(ex/raise :type :validation
:code :invalid-image
:hint "invalid image")))]
(when (and (string? mtype) (when (and (string? mtype)
(not= (str/lower mtype) mtype')) (not= mtype mtype'))
(ex/raise :type :validation (ex/raise :type :validation
:code :media-type-mismatch :code :media-type-mismatch
:hint (str "Seems like you are uploading a file whose content does not match the extension." :hint (str "Seems like you are uploading a file whose content does not match the extension."
"Expected: " mtype ". Got: " mtype'))) "Expected: " mtype ". Got: " mtype')))
(assoc input (let [{:keys [width height]}
:width width (or (get-dimensions-with-orientation (str path))
:height height (do
:size (fs/size path) (l/warn "Failed to read image dimensions with orientation; falling back to im4java"
:ts (ct/now)))))) {:path path})
{:width (.getPageWidth instance)
:height (.getPageHeight instance)}))]
(assoc input
:width width
:height height
:size (fs/size path)
:ts (ct/now)))))))
(defmethod process-error org.im4java.core.InfoException (defmethod process-error org.im4java.core.InfoException
[error] [error]
@@ -381,22 +370,6 @@
(when (zero? (:exit res)) (when (zero? (:exit res))
(:out res)))) (:out res))))
(woff2->sfnt [data]
;; woff2_decompress outputs to same directory with .ttf extension
(let [finput (tmp/tempfile :prefix "penpot.font." :suffix ".woff2")
foutput (fs/path (str/replace (str finput) #"\.woff2$" ".ttf"))]
(try
(io/write* finput data)
(let [res (sh/sh "woff2_decompress" (str finput))]
(if (zero? (:exit res))
foutput
(do
(when (fs/exists? foutput)
(fs/delete foutput))
nil)))
(finally
(fs/delete finput)))))
;; Documented here: ;; Documented here:
;; https://docs.microsoft.com/en-us/typography/opentype/spec/otff#table-directory ;; https://docs.microsoft.com/en-us/typography/opentype/spec/otff#table-directory
(get-sfnt-type [data] (get-sfnt-type [data]
@@ -446,27 +419,4 @@
(= stype :ttf) (= stype :ttf)
(-> (assoc "font/otf" (ttf->otf sfnt)) (-> (assoc "font/otf" (ttf->otf sfnt))
(assoc "font/ttf" sfnt))))) (assoc "font/ttf" sfnt)))))))))
(contains? current "font/woff2")
(let [data (get input "font/woff2")
foutput (woff2->sfnt data)]
(when-not foutput
(ex/raise :type :validation
:code :invalid-woff2-file
:hint "invalid woff2 file"))
(try
(let [sfnt (io/read* foutput)
type (get-sfnt-type sfnt)]
(cond-> input
(= type :otf)
(-> (assoc "font/otf" sfnt)
(assoc "font/ttf" (otf->ttf sfnt))
(update "font/woff" gen-if-nil #(ttf-or-otf->woff sfnt)))
(= type :ttf)
(-> (assoc "font/ttf" sfnt)
(assoc "font/otf" (ttf->otf sfnt))
(update "font/woff" gen-if-nil #(ttf-or-otf->woff sfnt)))))
(finally
(fs/delete foutput))))))))

View File

@@ -10,7 +10,6 @@
[app.common.logging :as l] [app.common.logging :as l]
[app.db :as db] [app.db :as db]
[app.migrations.clj.migration-0023 :as mg0023] [app.migrations.clj.migration-0023 :as mg0023]
[app.migrations.clj.migration-0145 :as mg0145]
[app.util.migrations :as mg] [app.util.migrations :as mg]
[integrant.core :as ig])) [integrant.core :as ig]))
@@ -457,16 +456,7 @@
:fn (mg/resource "app/migrations/sql/0142-add-sso-provider-table.sql")} :fn (mg/resource "app/migrations/sql/0142-add-sso-provider-table.sql")}
{:name "0143-http-session-v2-table" {:name "0143-http-session-v2-table"
:fn (mg/resource "app/migrations/sql/0143-add-http-session-v2-table.sql")} :fn (mg/resource "app/migrations/sql/0143-add-http-session-v2-table.sql")}])
{:name "0144-mod-server-error-report-table"
:fn (mg/resource "app/migrations/sql/0144-mod-server-error-report-table.sql")}
{:name "0145-fix-plugins-uri-on-profile"
:fn mg0145/migrate}
{:name "0146-mod-access-token-table"
:fn (mg/resource "app/migrations/sql/0146-mod-access-token-table.sql")}])
(defn apply-migrations! (defn apply-migrations!
[pool name migrations] [pool name migrations]

View File

@@ -1,83 +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.migrations.clj.migration-0145
"Migrate plugins references on profiles"
(:require
[app.common.data :as d]
[app.common.logging :as l]
[app.db :as db]
[cuerdas.core :as str]))
(def ^:private replacements
{"https://colors-to-tokens-plugin.pages.dev"
"https://colors-to-tokens.plugins.penpot.app"
"https://contrast-penpot-plugin.pages.dev"
"https://contrast.plugins.penpot.app"
"https://create-palette-penpot-plugin.pages.dev"
"https://create-palette.plugins.penpot.app"
"https://icons-penpot-plugin.pages.dev"
"https://icons.plugins.penpot.app"
"https://lorem-ipsum-penpot-plugin.pages.dev"
"https://lorem-ipsum.plugins.penpot.app"
"https://rename-layers-penpot-plugin.pages.dev"
"https://rename-layers.plugins.penpot.app"
"https://table-penpot-plugin.pages.dev"
"https://table.plugins.penpot.app"})
(defn- fix-url
[url]
(reduce-kv (fn [url prefix replacement]
(if (str/starts-with? url prefix)
(reduced (str replacement (subs url (count prefix))))
url))
url
replacements))
(defn- fix-manifest
[manifest]
(-> manifest
(d/update-when :url fix-url)
(d/update-when :host fix-url)))
(defn- fix-plugins-data
[props]
(d/update-in-when props [:plugins :data]
(fn [data]
(reduce-kv (fn [data id manifest]
(let [manifest' (fix-manifest manifest)]
(if (= manifest manifest')
data
(assoc data id manifest'))))
data
data))))
(def ^:private sql:get-profiles
"SELECT id, props FROM profile
WHERE props ?? '~:plugins'
ORDER BY created_at
FOR UPDATE")
(defn migrate
[conn]
(->> (db/plan conn [sql:get-profiles])
(run! (fn [{:keys [id props]}]
(when-let [props (some-> props db/decode-transit-pgobject)]
(let [props' (fix-plugins-data props)]
(when (not= props props')
(l/inf :hint "fixing plugins data on profile props" :profile-id (str id))
(db/update! conn :profile
{:props (db/tjson props')}
{:id id}
{::db/return-keys false}))))))))

View File

@@ -1,11 +0,0 @@
ALTER TABLE server_error_report DROP CONSTRAINT server_error_report_pkey;
DELETE FROM server_error_report a
USING server_error_report b
WHERE a.id = b.id
AND a.ctid < b.ctid;
ALTER TABLE server_error_report ADD PRIMARY KEY (id);
CREATE INDEX server_error_report__version__idx
ON server_error_report ( version );

View File

@@ -1,2 +0,0 @@
ALTER TABLE access_token
ADD COLUMN type text NULL;

View File

@@ -1,130 +0,0 @@
(ns app.nitrate
"Module that make calls to the external nitrate aplication"
(:require
[app.common.logging :as l]
[app.common.schema :as sm]
[app.config :as cf]
[app.http.client :as http]
[app.rpc :as-alias rpc]
[app.setup :as-alias setup]
[app.util.json :as json]
[clojure.core :as c]
[integrant.core :as ig]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- request-builder
[cfg method uri shared-key profile-id]
(fn []
(http/req! cfg {:method method
:headers {"content-type" "application/json"
"accept" "application/json"
"x-shared-key" shared-key
"x-profile-id" (str profile-id)}
:uri uri
:version :http1.1})))
(defn- with-retries
[handler max-retries]
(fn []
(loop [attempt 1]
(let [result (try
(handler)
(catch Exception e
(if (< attempt max-retries)
::retry
(do
;; TODO Error handling
(l/error :hint "request fail after multiple retries" :cause e)
nil))))]
(if (= result ::retry)
(recur (inc attempt))
result)))))
(defn- with-validate [handler uri schema]
(fn []
(let [coercer-http (sm/coercer schema
:type :validation
:hint (str "invalid data received calling " uri))]
(try
(coercer-http (-> (handler) :body json/decode))
(catch Exception e
;; TODO Error handling
(l/error :hint "error validating json response" :cause e)
nil)))))
(defn- request-to-nitrate
[cfg method uri schema {:keys [::rpc/profile-id] :as params}]
(let [shared-key (-> cfg ::setup/shared-keys :nitrate)
full-http-call (-> (request-builder cfg method uri shared-key profile-id)
(with-retries 3)
(with-validate uri schema))]
(full-http-call)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; API
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn call
[cfg method params]
(when (contains? cf/flags :nitrate)
(let [client (get cfg ::client)
method (get client method)]
(method params))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private schema:organization
[:map
[:id ::sm/text]
[:name ::sm/text]])
(def ^:private schema:user
[:map
[:valid ::sm/boolean]])
(defn- get-team-org
[cfg {:keys [team-id] :as params}]
(let [baseuri (cf/get :nitrate-backend-uri)]
(request-to-nitrate cfg :get (str baseuri "/api/teams/" (str team-id)) schema:organization params)))
(defn- is-valid-user
[cfg {:keys [profile-id] :as params}]
(let [baseuri (cf/get :nitrate-backend-uri)]
(request-to-nitrate cfg :get (str baseuri "/api/users/" (str profile-id)) schema:user params)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; INITIALIZATION
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defmethod ig/init-key ::client
[_ cfg]
(when (contains? cf/flags :nitrate)
{:get-team-org (partial get-team-org cfg)
:is-valid-user (partial is-valid-user cfg)}))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; UTILS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn add-nitrate-licence-to-profile
[cfg profile]
(try
(let [nitrate-licence (call cfg :is-valid-user {:profile-id (:id profile)})]
(assoc profile :nitrate-licence (:valid nitrate-licence)))
(catch Throwable cause
(l/error :hint "failed to get nitrate licence"
:profile-id (:id profile)
:cause cause)
profile)))
(defn add-org-to-team
[cfg team params]
(let [params (assoc (or params {}) :team-id (:id team))
org (call cfg :get-team-org params)]
(assoc team :organization-id (:id org) :organization-name (:name org))))

View File

@@ -14,7 +14,6 @@
[app.common.spec :as us] [app.common.spec :as us]
[app.common.time :as ct] [app.common.time :as ct]
[app.common.uri :as u] [app.common.uri :as u]
[app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.http :as-alias http] [app.http :as-alias http]
@@ -73,13 +72,9 @@
(if (nil? result) (if (nil? result)
204 204
200)) 200))
headers (cond-> (::http/headers mdata {})
headers (::http/headers mdata {}) (yres/stream-body? result)
headers (cond-> headers
(and (yres/stream-body? result)
(not (contains? headers "content-type")))
(assoc "content-type" "application/octet-stream"))] (assoc "content-type" "application/octet-stream"))]
{::yres/status status {::yres/status status
::yres/headers headers ::yres/headers headers
::yres/body result}))] ::yres/body result}))]
@@ -94,14 +89,10 @@
[methods] [methods]
(let [methods (update-vals methods peek)] (let [methods (update-vals methods peek)]
(fn [{:keys [params path-params method] :as request}] (fn [{:keys [params path-params method] :as request}]
(let [handler-name (:method-name path-params) (let [handler-name (:type path-params)
etag (yreq/get-header request "if-none-match") etag (yreq/get-header request "if-none-match")
key-id (get request ::http/auth-key-id)
profile-id (or (::session/profile-id request) profile-id (or (::session/profile-id request)
(::actoken/profile-id request) (::actoken/profile-id request))
(if key-id uuid/zero nil))
ip-addr (inet/parse-request request) ip-addr (inet/parse-request request)
data (-> params data (-> params
@@ -231,8 +222,8 @@
(wrap-authentication cfg $ mdata))) (wrap-authentication cfg $ mdata)))
(defn- process-method (defn- process-method
[cfg wrap-fn [f mdata]] [cfg module wrap-fn [f mdata]]
(l/trc :hint "add method" :module (::module cfg) :type (::type cfg) :name (::sv/name mdata)) (l/trc :hint "add method" :module module :name (::sv/name mdata))
(let [f (wrap-fn cfg f mdata) (let [f (wrap-fn cfg f mdata)
k (keyword (::sv/name mdata))] k (keyword (::sv/name mdata))]
[k [mdata (partial f cfg)]])) [k [mdata (partial f cfg)]]))
@@ -243,7 +234,7 @@
(defn- resolve-methods (defn- resolve-methods
[cfg] [cfg]
(let [cfg (assoc cfg ::module "main" ::type "command" ::metrics-id :rpc-main-timing)] (let [cfg (assoc cfg ::type "command" ::metrics-id :rpc-command-timing)]
(->> (sv/scan-ns (->> (sv/scan-ns
'app.rpc.commands.access-token 'app.rpc.commands.access-token
'app.rpc.commands.audit 'app.rpc.commands.audit
@@ -270,7 +261,7 @@
'app.rpc.commands.verify-token 'app.rpc.commands.verify-token
'app.rpc.commands.viewer 'app.rpc.commands.viewer
'app.rpc.commands.webhooks) 'app.rpc.commands.webhooks)
(map (partial process-method cfg wrap)) (map (partial process-method cfg "rpc" wrap))
(into {})))) (into {}))))
(def ^:private schema:methods-params (def ^:private schema:methods-params
@@ -302,13 +293,11 @@
(defn- resolve-management-methods (defn- resolve-management-methods
[cfg] [cfg]
(let [cfg (assoc cfg ::module "management" ::type "command" ::metrics-id :rpc-management-timing) (let [cfg (assoc cfg ::type "management" ::metrics-id :rpc-management-timing)]
mods (cond->> (list 'app.rpc.management.exporter) (->> (sv/scan-ns
(contains? cf/flags :nitrate) 'app.rpc.management.subscription
(cons 'app.rpc.management.nitrate))] 'app.rpc.management.exporter)
(map (partial process-method cfg "management" wrap-management))
(->> (apply sv/scan-ns mods)
(map (partial process-method cfg wrap-management))
(into {})))) (into {}))))
(def ^:private schema:management-methods-params (def ^:private schema:management-methods-params
@@ -351,20 +340,23 @@
(defmethod ig/assert-key ::routes (defmethod ig/assert-key ::routes
[_ params] [_ params]
(assert (map? (::setup/shared-keys params)))
(assert (db/pool? (::db/pool params)) "expect valid database pool") (assert (db/pool? (::db/pool params)) "expect valid database pool")
(assert (some? (::setup/props params)))
(assert (session/manager? (::session/manager params)) "expect valid session manager") (assert (session/manager? (::session/manager params)) "expect valid session manager")
(assert (valid-methods? (::methods params)) "expect valid methods map") (assert (valid-methods? (::methods params)) "expect valid methods map")
(assert (valid-methods? (::management-methods params)) "expect valid methods map")) (assert (valid-methods? (::management-methods params)) "expect valid methods map"))
(defmethod ig/init-key ::routes (defmethod ig/init-key ::routes
[_ {:keys [::methods ::management-methods ::setup/shared-keys] :as cfg}] [_ {:keys [::methods ::management-methods ::setup/props] :as cfg}]
(let [public-uri (cf/get :public-uri)
management-key (or (cf/get :management-api-key)
(get props :management-key))]
(let [public-uri (cf/get :public-uri)]
["/api" ["/api"
["/management" ["/management"
["/methods/:method-name" ["/methods/:type"
{:middleware [[mw/shared-key-auth shared-keys] {:middleware [[mw/shared-key-auth management-key]
[session/authz cfg]] [session/authz cfg]]
:handler (make-rpc-handler management-methods)}] :handler (make-rpc-handler management-methods)}]
@@ -374,7 +366,7 @@
:description "MANAGEMENT API")] :description "MANAGEMENT API")]
["/main" ["/main"
["/methods/:method-name" ["/methods/:type"
{:middleware [[mw/cors] {:middleware [[mw/cors]
[sec/client-header-check] [sec/client-header-check]
[session/authz cfg] [session/authz cfg]
@@ -392,7 +384,7 @@
["/openapi" {:handler (redirect (u/join public-uri "/api/main/doc/openapi"))}] ["/openapi" {:handler (redirect (u/join public-uri "/api/main/doc/openapi"))}]
["/openapi.join" {:handler (redirect (u/join public-uri "/api/main/doc/openapi.json"))}] ["/openapi.join" {:handler (redirect (u/join public-uri "/api/main/doc/openapi.json"))}]
["/rpc/command/:method-name" ["/rpc/command/:type"
{:middleware [[mw/cors] {:middleware [[mw/cors]
[sec/client-header-check] [sec/client-header-check]
[session/authz cfg] [session/authz cfg]

View File

@@ -23,7 +23,7 @@
(dissoc row :perms)) (dissoc row :perms))
(defn create-access-token (defn create-access-token
[{:keys [::db/conn] :as cfg} profile-id name expiration type] [{:keys [::db/conn] :as cfg} profile-id name expiration]
(let [token-id (uuid/next) (let [token-id (uuid/next)
expires-at (some-> expiration (ct/in-future)) expires-at (some-> expiration (ct/in-future))
created-at (ct/now) created-at (ct/now)
@@ -36,7 +36,6 @@
{:id token-id {:id token-id
:name name :name name
:token token :token token
:type type
:profile-id profile-id :profile-id profile-id
:created-at created-at :created-at created-at
:updated-at created-at :updated-at created-at
@@ -51,18 +50,17 @@
(def ^:private schema:create-access-token (def ^:private schema:create-access-token
[:map {:title "create-access-token"} [:map {:title "create-access-token"}
[:name [:string {:max 250 :min 1}]] [:name [:string {:max 250 :min 1}]]
[:expiration {:optional true} ::ct/duration] [:expiration {:optional true} ::ct/duration]])
[:type {:optional true} :string]])
(sv/defmethod ::create-access-token (sv/defmethod ::create-access-token
{::doc/added "1.18" {::doc/added "1.18"
::sm/params schema:create-access-token} ::sm/params schema:create-access-token}
[cfg {:keys [::rpc/profile-id name expiration type]}] [cfg {:keys [::rpc/profile-id name expiration]}]
(quotes/check! cfg {::quotes/id ::quotes/access-tokens-per-profile (quotes/check! cfg {::quotes/id ::quotes/access-tokens-per-profile
::quotes/profile-id profile-id}) ::quotes/profile-id profile-id})
(db/tx-run! cfg create-access-token profile-id name expiration type)) (db/tx-run! cfg create-access-token profile-id name expiration))
(def ^:private schema:delete-access-token (def ^:private schema:delete-access-token
[:map {:title "delete-access-token"} [:map {:title "delete-access-token"}
@@ -85,22 +83,5 @@
(->> (db/query pool :access-token (->> (db/query pool :access-token
{:profile-id profile-id} {:profile-id profile-id}
{:order-by [[:expires-at :asc] [:created-at :asc]] {:order-by [[:expires-at :asc] [:created-at :asc]]
:columns [:id :name :perms :type :created-at :updated-at :expires-at]}) :columns [:id :name :perms :created-at :updated-at :expires-at]})
(mapv decode-row))) (mapv decode-row)))
(def ^:private schema:get-current-mcp-token
[:map {:title "get-current-mcp-token"}])
(sv/defmethod ::get-current-mcp-token
{::doc/added "2.15"
::sm/params schema:get-current-mcp-token}
[{:keys [::db/pool]} {:keys [::rpc/profile-id ::rpc/request-at]}]
(->> (db/query pool :access-token
{:profile-id profile-id
:type "mcp"}
{:order-by [[:expires-at :asc] [:created-at :asc]]
:columns [:token :expires-at]})
(remove #(ct/is-after? (:expires-at %) request-at))
(map decode-row)
(first)))

View File

@@ -16,8 +16,6 @@
[app.db :as db] [app.db :as db]
[app.http :as-alias http] [app.http :as-alias http]
[app.loggers.audit :as-alias audit] [app.loggers.audit :as-alias audit]
[app.loggers.database :as loggers.db]
[app.loggers.mattermost :as loggers.mm]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.rpc.climit :as-alias climit] [app.rpc.climit :as-alias climit]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
@@ -38,79 +36,52 @@
:context]) :context])
(defn- event->row [event] (defn- event->row [event]
[(::audit/id event) [(uuid/next)
(::audit/name event) (:name event)
(::audit/source event) (:source event)
(::audit/type event) (:type event)
(::audit/tracked-at event) (:timestamp event)
(::audit/created-at event) (:created-at event)
(::audit/profile-id event) (:profile-id event)
(db/inet (::audit/ip-addr event)) (db/inet (:ip-addr event))
(db/tjson (::audit/props event)) (db/tjson (:props event))
(db/tjson (d/without-nils (::audit/context event)))]) (db/tjson (d/without-nils (:context event)))])
(defn- adjust-timestamp (defn- adjust-timestamp
[{:keys [::audit/tracked-at ::audit/created-at] :as event}] [{:keys [timestamp created-at] :as event}]
(let [margin (inst-ms (ct/diff tracked-at created-at))] (let [margin (inst-ms (ct/diff timestamp created-at))]
(if (or (neg? margin) (if (or (neg? margin)
(> margin 3600000)) (> margin 3600000))
;; If event is in future or lags more than 1 hour, we reasign ;; If event is in future or lags more than 1 hour, we reasign
;; tracked-at to the server creation date ;; timestamp to the server creation date
(-> event (-> event
(assoc ::audit/tracked-at created-at) (assoc :timestamp created-at)
(update ::audit/context assoc :original-tracked-at tracked-at)) (update :context assoc :original-timestamp timestamp))
event))) event)))
(defn- exception-event? (defn- handle-events
[{:keys [::audit/type ::audit/name] :as ev}] [{:keys [::db/pool]} {:keys [::rpc/profile-id events] :as params}]
(and (= "action" type)
(or (= "unhandled-exception" name)
(= "exception-page" name))))
(def ^:private xf:map-event-row
(comp
(map adjust-timestamp)
(map event->row)))
(defn- get-events
[{:keys [::rpc/request-at ::rpc/profile-id events] :as params}]
(let [request (-> params meta ::http/request) (let [request (-> params meta ::http/request)
ip-addr (inet/parse-request request) ip-addr (inet/parse-request request)
tnow (ct/now)
xform (map (fn [event] xform (comp
{::audit/id (uuid/next) (map (fn [event]
::audit/type (:type event) (-> event
::audit/name (:name event) (assoc :created-at tnow)
::audit/props (:props event) (assoc :profile-id profile-id)
::audit/context (:context event) (assoc :ip-addr ip-addr)
::audit/profile-id profile-id (assoc :source "frontend"))))
::audit/ip-addr ip-addr (filter :profile-id)
::audit/source "frontend" (map adjust-timestamp)
::audit/tracked-at (:timestamp event) (map event->row))
::audit/created-at request-at}))] events (sequence xform events)]
(sequence xform events)))
(defn- handle-events
[{:keys [::db/pool] :as cfg} params]
(let [events (get-events params)]
;; Look for error reports and save them on internal reports table
(when-let [events (->> events
(sequence (filter exception-event?))
(not-empty))]
(run! (partial loggers.db/emit cfg) events)
(run! (partial loggers.mm/emit cfg) events))
;; Process and save events
(when (seq events) (when (seq events)
(let [rows (sequence xf:map-event-row events)] (db/insert-many! pool :audit-log event-columns events))))
(db/insert-many! pool :audit-log event-columns rows)))))
(def ^:private valid-event-types (def valid-event-types
#{"action" "identify" "trigger"}) #{"action" "identify"})
(def ^:private schema:frontend-event (def schema:event
[:map {:title "Event"} [:map {:title "Event"}
[:name [:name
[:and {:gen/elements ["update-file", "get-profile"]} [:and {:gen/elements ["update-file", "get-profile"]}
@@ -122,13 +93,12 @@
[::sm/one-of {:format "string"} valid-event-types]]] [::sm/one-of {:format "string"} valid-event-types]]]
[:props [:props
[:map-of :keyword ::sm/any]] [:map-of :keyword ::sm/any]]
[:timestamp ::ct/inst]
[:context {:optional true} [:context {:optional true}
[:map-of :keyword ::sm/any]]]) [:map-of :keyword ::sm/any]]])
(def ^:private schema:push-audit-events (def schema:push-audit-events
[:map {:title "push-audit-events"} [:map {:title "push-audit-events"}
[:events [:vector schema:frontend-event]]]) [:events [:vector schema:event]]])
(sv/defmethod ::push-audit-events (sv/defmethod ::push-audit-events
{::climit/id :submit-audit-events/by-profile {::climit/id :submit-audit-events/by-profile

View File

@@ -307,8 +307,7 @@
:content-type (:mtype input)})] :content-type (:mtype input)})]
(:id sobject)) (:id sobject))
(catch Throwable cause (catch Throwable cause
(l/wrn :hint "unable to import profile picture" (l/err :hint "unable to import profile picture"
:uri uri
:cause cause) :cause cause)
nil))) nil)))

View File

@@ -79,14 +79,85 @@
;; --- FILE PERMISSIONS ;; --- FILE PERMISSIONS
(def ^:private sql:file-permissions
"select fpr.is_owner,
fpr.is_admin,
fpr.can_edit
from file_profile_rel as fpr
inner join file as f on (f.id = fpr.file_id)
where fpr.file_id = ?
and fpr.profile_id = ?
and f.deleted_at is null
union all
select tpr.is_owner,
tpr.is_admin,
tpr.can_edit
from team_profile_rel as tpr
inner join project as p on (p.team_id = tpr.team_id)
inner join file as f on (p.id = f.project_id)
where f.id = ?
and tpr.profile_id = ?
and f.deleted_at is null
union all
select ppr.is_owner,
ppr.is_admin,
ppr.can_edit
from project_profile_rel as ppr
inner join file as f on (f.project_id = ppr.project_id)
where f.id = ?
and ppr.profile_id = ?
and f.deleted_at is null")
(defn get-file-permissions
[conn profile-id file-id]
(when (and profile-id file-id)
(db/exec! conn [sql:file-permissions
file-id profile-id
file-id profile-id
file-id profile-id])))
(defn get-permissions
([conn profile-id file-id]
(let [rows (get-file-permissions conn profile-id file-id)
is-owner (boolean (some :is-owner rows))
is-admin (boolean (some :is-admin rows))
can-edit (boolean (some :can-edit rows))]
(when (seq rows)
{:type :membership
:is-owner is-owner
:is-admin (or is-owner is-admin)
:can-edit (or is-owner is-admin can-edit)
:can-read true
:is-logged (some? profile-id)})))
([conn profile-id file-id share-id]
(let [perms (get-permissions conn profile-id file-id)
ldata (some-> (db/get* conn :share-link {:id share-id :file-id file-id})
(dissoc :flags)
(update :pages db/decode-pgarray #{}))]
;; NOTE: in a future when share-link becomes more powerful and
;; will allow us specify which parts of the app is available, we
;; will probably need to tweak this function in order to expose
;; this flags to the frontend.
(cond
(some? perms) perms
(some? ldata) {:type :share-link
:can-read true
:pages (:pages ldata)
:is-logged (some? profile-id)
:who-comment (:who-comment ldata)
:who-inspect (:who-inspect ldata)}))))
(def has-edit-permissions? (def has-edit-permissions?
(perms/make-edition-predicate-fn bfc/get-file-permissions)) (perms/make-edition-predicate-fn get-permissions))
(def has-read-permissions? (def has-read-permissions?
(perms/make-read-predicate-fn bfc/get-file-permissions)) (perms/make-read-predicate-fn get-permissions))
(def has-comment-permissions? (def has-comment-permissions?
(perms/make-comment-predicate-fn bfc/get-file-permissions)) (perms/make-comment-predicate-fn get-permissions))
(def check-edition-permissions! (def check-edition-permissions!
(perms/make-check-fn has-edit-permissions?)) (perms/make-check-fn has-edit-permissions?))
@@ -99,7 +170,7 @@
(defn check-comment-permissions! (defn check-comment-permissions!
[conn profile-id file-id share-id] [conn profile-id file-id share-id]
(let [perms (bfc/get-file-permissions conn profile-id file-id share-id) (let [perms (get-permissions conn profile-id file-id share-id)
can-read (has-read-permissions? perms) can-read (has-read-permissions? perms)
can-comment (has-comment-permissions? perms)] can-comment (has-comment-permissions? perms)]
(when-not (or can-read can-comment) (when-not (or can-read can-comment)
@@ -151,7 +222,7 @@
(defn- get-minimal-file-with-perms (defn- get-minimal-file-with-perms
[cfg {:keys [:id ::rpc/profile-id]}] [cfg {:keys [:id ::rpc/profile-id]}]
(let [mfile (get-minimal-file cfg id) (let [mfile (get-minimal-file cfg id)
perms (bfc/get-file-permissions cfg profile-id id)] perms (get-permissions cfg profile-id id)]
(assoc mfile :permissions perms))) (assoc mfile :permissions perms)))
(defn get-file-etag (defn get-file-etag
@@ -177,7 +248,7 @@
;; will be already prefetched and we just reuse them instead ;; will be already prefetched and we just reuse them instead
;; of making an additional database queries. ;; of making an additional database queries.
(let [perms (or (:permissions (::cond/object params)) (let [perms (or (:permissions (::cond/object params))
(bfc/get-file-permissions conn profile-id id))] (get-permissions conn profile-id id))]
(check-read-permissions! perms) (check-read-permissions! perms)
(let [team (teams/get-team conn (let [team (teams/get-team conn
@@ -240,7 +311,7 @@
::sm/result schema:file-fragment} ::sm/result schema:file-fragment}
[cfg {:keys [::rpc/profile-id file-id fragment-id share-id]}] [cfg {:keys [::rpc/profile-id file-id fragment-id share-id]}]
(db/run! cfg (fn [cfg] (db/run! cfg (fn [cfg]
(let [perms (bfc/get-file-permissions cfg profile-id file-id share-id)] (let [perms (get-permissions cfg profile-id file-id share-id)]
(check-read-permissions! perms) (check-read-permissions! perms)
(-> (get-file-fragment cfg file-id fragment-id) (-> (get-file-fragment cfg file-id fragment-id)
(rph/with-http-cache long-cache-duration)))))) (rph/with-http-cache long-cache-duration))))))
@@ -385,7 +456,8 @@
:code :params-validation :code :params-validation
:hint "page-id is required when object-id is provided")) :hint "page-id is required when object-id is provided"))
(let [perms (bfc/get-file-permissions conn profile-id file-id share-id) (let [perms (get-permissions conn profile-id file-id share-id)
file (bfc/get-file cfg file-id :read-only? true) file (bfc/get-file cfg file-id :read-only? true)
proj (db/get conn :project {:id (:project-id file)}) proj (db/get conn :project {:id (:project-id file)})
@@ -616,10 +688,11 @@
"Get libraries used by the specified file." "Get libraries used by the specified file."
{::doc/added "1.17" {::doc/added "1.17"
::sm/params schema:get-file-libraries} ::sm/params schema:get-file-libraries}
[cfg {:keys [::rpc/profile-id file-id]}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id]}]
(bfc/check-file-exists cfg file-id) (dm/with-open [conn (db/open pool)]
(check-read-permissions! cfg profile-id file-id) (check-read-permissions! conn profile-id file-id)
(bfc/get-file-libraries cfg file-id)) (bfc/get-file-libraries conn file-id)))
;; --- COMMAND QUERY: Files that use this File library ;; --- COMMAND QUERY: Files that use this File library
@@ -704,6 +777,7 @@
f.created_at, f.created_at,
f.modified_at, f.modified_at,
f.name, f.name,
f.is_shared,
f.deleted_at AS will_be_deleted_at, f.deleted_at AS will_be_deleted_at,
ft.media_id AS thumbnail_id, ft.media_id AS thumbnail_id,
row_number() OVER w AS row_num, row_number() OVER w AS row_num,
@@ -711,7 +785,8 @@
FROM file AS f FROM file AS f
INNER JOIN project AS p ON (p.id = f.project_id) INNER JOIN project AS p ON (p.id = f.project_id)
LEFT JOIN file_thumbnail AS ft on (ft.file_id = f.id LEFT JOIN file_thumbnail AS ft on (ft.file_id = f.id
AND ft.revn = f.revn) AND ft.revn = f.revn
AND ft.deleted_at is null)
WHERE p.team_id = ? WHERE p.team_id = ?
AND (p.deleted_at > ?::timestamptz OR AND (p.deleted_at > ?::timestamptz OR
f.deleted_at > ?::timestamptz) f.deleted_at > ?::timestamptz)
@@ -813,7 +888,7 @@
AND (f.deleted_at IS NULL OR f.deleted_at > now()) AND (f.deleted_at IS NULL OR f.deleted_at > now())
ORDER BY f.created_at ASC;") ORDER BY f.created_at ASC;")
(defn- absorb-library-by-file (defn- absorb-library-by-file!
[cfg ldata file-id] [cfg ldata file-id]
(assert (db/connection-map? cfg) (assert (db/connection-map? cfg)
@@ -837,7 +912,7 @@
:modified-at (ct/now) :modified-at (ct/now)
:has-media-trimmed false})))) :has-media-trimmed false}))))
(defn- absorb-library* (defn- absorb-library
"Find all files using a shared library, and absorb all library assets "Find all files using a shared library, and absorb all library assets
into the file local libraries" into the file local libraries"
[cfg {:keys [id data] :as library}] [cfg {:keys [id data] :as library}]
@@ -852,10 +927,10 @@
:library-id (str id) :library-id (str id)
:files (str/join "," (map str ids))) :files (str/join "," (map str ids)))
(run! (partial absorb-library-by-file cfg data) ids) (run! (partial absorb-library-by-file! cfg data) ids)
library)) library))
(defn absorb-library (defn absorb-library!
[{:keys [::db/conn] :as cfg} id] [{:keys [::db/conn] :as cfg} id]
(let [file (-> (bfc/get-file cfg id (let [file (-> (bfc/get-file cfg id
:realize? true :realize? true
@@ -872,7 +947,7 @@
(-> (cfeat/get-team-enabled-features cf/flags team) (-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-file-features! (:features file))) (cfeat/check-file-features! (:features file)))
(absorb-library* cfg file))) (absorb-library cfg file)))
(defn- set-file-shared (defn- set-file-shared
[{:keys [::db/conn] :as cfg} {:keys [profile-id id] :as params}] [{:keys [::db/conn] :as cfg} {:keys [profile-id id] :as params}]
@@ -885,14 +960,14 @@
;; file, we need to perform more complex operation, ;; file, we need to perform more complex operation,
;; so in this case we retrieve the complete file and ;; so in this case we retrieve the complete file and
;; perform all required validations. ;; perform all required validations.
(let [file (-> (absorb-library cfg id) (let [file (-> (absorb-library! cfg id)
(assoc :is-shared false))] (assoc :is-shared false))]
(db/delete! conn :file-library-rel {:library-file-id id}) (db/delete! conn :file-library-rel {:library-file-id id})
(db/update! conn :file (db/update! conn :file
{:is-shared false {:is-shared false
:modified-at (ct/now)} :modified-at (ct/now)}
{:id id}) {:id id})
file) (select-keys file [:id :name :is-shared]))
(and (false? (:is-shared file)) (and (false? (:is-shared file))
(true? (:is-shared params))) (true? (:is-shared params)))
@@ -939,11 +1014,6 @@
{:id file-id} {:id file-id}
{::db/return-keys [:id :name :is-shared :deleted-at {::db/return-keys [:id :name :is-shared :deleted-at
:project-id :created-at :modified-at]})] :project-id :created-at :modified-at]})]
;; Remove all possible relations for that file
(db/delete! conn :file-library-rel
{:library-file-id file-id})
(wrk/submit! {::db/conn conn (wrk/submit! {::db/conn conn
::wrk/task :delete-object ::wrk/task :delete-object
::wrk/params {:object :file ::wrk/params {:object :file
@@ -1094,53 +1164,47 @@
;; --- MUTATION COMMAND: delete-files-immediatelly ;; --- MUTATION COMMAND: delete-files-immediatelly
(def ^:private sql:get-delete-team-files-candidates (def ^:private sql:delete-team-files
"SELECT f.id "UPDATE file AS uf SET deleted_at = ?::timestamptz
FROM file AS f FROM (
JOIN project AS p ON (p.id = f.project_id) SELECT f.id
JOIN team AS t ON (t.id = p.team_id) FROM file AS f
WHERE t.deleted_at IS NULL JOIN project AS p ON (p.id = f.project_id)
AND t.id = ? JOIN team AS t ON (t.id = p.team_id)
AND f.id = ANY(?::uuid[])") WHERE t.deleted_at IS NULL
AND t.id = ?
AND f.id = ANY(?::uuid[])
) AS subquery
WHERE uf.id = subquery.id
RETURNING uf.id, uf.deleted_at;")
(def ^:private schema:permanently-delete-team-files (def ^:private schema:permanently-delete-team-files
[:map {:title "permanently-delete-team-files"} [:map {:title "permanently-delete-team-files"}
[:team-id ::sm/uuid] [:team-id ::sm/uuid]
[:ids [::sm/set ::sm/uuid]]]) [:ids [::sm/set ::sm/uuid]]])
(defn- permanently-delete-team-files
[{:keys [::db/conn]} {:keys [::rpc/request-at team-id ids]}]
(let [ids (into #{}
d/xf:map-id
(db/exec! conn [sql:get-delete-team-files-candidates team-id
(db/create-array conn "uuid" ids)]))]
(reduce (fn [acc id]
(events/tap :progress {:file-id id :index (inc (count acc)) :total (count ids)})
(db/update! conn :file
{:deleted-at request-at}
{:id id}
{::db/return-keys false})
(wrk/submit! {::db/conn conn
::wrk/task :delete-object
::wrk/params {:object :file
:deleted-at request-at
:id id}})
(conj acc id))
#{}
ids)))
(sv/defmethod ::permanently-delete-team-files (sv/defmethod ::permanently-delete-team-files
"Mark the specified files to be deleted immediatelly on the "Mark the specified files to be deleted immediatelly on the
specified team. The team-id on params will be used to filter and specified team. The team-id on params will be used to filter and
check writable permissons on team." check writable permissons on team."
{::doc/added "2.13" {::doc/added "2.12"
::sm/params schema:permanently-delete-team-files} ::sm/params schema:permanently-delete-team-files
::db/transaction true}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}] [{:keys [::db/conn]} {:keys [::rpc/profile-id ::rpc/request-at team-id ids]}]
(teams/check-edition-permissions! pool profile-id team-id) (teams/check-edition-permissions! conn profile-id team-id)
(sse/response #(db/tx-run! cfg permanently-delete-team-files params)))
(reduce (fn [acc {:keys [id deleted-at]}]
(wrk/submit! {::db/conn conn
::wrk/task :delete-object
::wrk/params {:object :file
:deleted-at deleted-at
:id id}})
(conj acc id))
#{}
(db/plan conn [sql:delete-team-files request-at team-id
(db/create-array conn "uuid" ids)])))
;; --- MUTATION COMMAND: restore-files-immediatelly ;; --- MUTATION COMMAND: restore-files-immediatelly
@@ -1204,7 +1268,7 @@
{:keys [files projects]} {:keys [files projects]}
(reduce (fn [result {:keys [id project-id]}] (reduce (fn [result {:keys [id project-id]}]
(let [index (-> result :files count)] (let [index (-> result :files count)]
(events/tap :progress {:file-id id :index (inc index) :total total-files}) (events/tap :progress {:file-id id :index index :total total-files})
(restore-file conn id) (restore-file conn id)
(-> result (-> result
@@ -1227,7 +1291,7 @@
(sv/defmethod ::restore-deleted-team-files (sv/defmethod ::restore-deleted-team-files
"Removes the deletion mark from the specified files (and respective "Removes the deletion mark from the specified files (and respective
projects) on the specified team." projects) on the specified team."
{::doc/added "2.13" {::doc/added "2.12"
::sse/stream? true ::sse/stream? true
::sm/params schema:restore-deleted-team-files} ::sm/params schema:restore-deleted-team-files}
[cfg params] [cfg params]

View File

@@ -199,13 +199,15 @@
[cfg {:keys [::rpc/profile-id file-id strip-frames-with-thumbnails] :as params}] [cfg {:keys [::rpc/profile-id file-id strip-frames-with-thumbnails] :as params}]
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}] (db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
(files/check-read-permissions! conn profile-id file-id) (files/check-read-permissions! conn profile-id file-id)
(let [team (teams/get-team conn (let [team (teams/get-team conn
:profile-id profile-id :profile-id profile-id
:file-id file-id) :file-id file-id)
file (bfc/get-file cfg file-id file (bfc/get-file cfg file-id
:include-deleted? true
:realize? true :realize? true
:read-only? true) :read-only? true)
strip-frames-with-thumbnails strip-frames-with-thumbnails
(or (nil? strip-frames-with-thumbnails) ;; if not present, default to true (or (nil? strip-frames-with-thumbnails) ;; if not present, default to true
(true? strip-frames-with-thumbnails))] (true? strip-frames-with-thumbnails))]
@@ -331,16 +333,12 @@
;; --- MUTATION COMMAND: create-file-thumbnail ;; --- MUTATION COMMAND: create-file-thumbnail
(defn- create-file-thumbnail (defn- create-file-thumbnail!
[{:keys [::db/conn ::sto/storage] :as cfg} {:keys [file-id revn props media] :as params}] [{:keys [::db/conn ::sto/storage]} {:keys [file-id revn props media] :as params}]
(media/validate-media-type! media) (media/validate-media-type! media)
(media/validate-media-size! media) (media/validate-media-size! media)
(let [file (bfc/get-file cfg file-id (let [props (db/tjson (or props {}))
:include-deleted? true
:load-data? false)
props (db/tjson (or props {}))
path (:path media) path (:path media)
mtype (:mtype media) mtype (:mtype media)
hash (sto/calculate-hash path) hash (sto/calculate-hash path)
@@ -369,7 +367,7 @@
(db/update! conn :file-thumbnail (db/update! conn :file-thumbnail
{:media-id (:id media) {:media-id (:id media)
:deleted-at (:deleted-at file) :deleted-at nil
:updated-at tnow :updated-at tnow
:props props} :props props}
{:file-id file-id {:file-id file-id
@@ -380,7 +378,6 @@
:revn revn :revn revn
:created-at tnow :created-at tnow
:updated-at tnow :updated-at tnow
:deleted-at (:deleted-at file)
:props props :props props
:media-id (:id media)})) :media-id (:id media)}))
@@ -405,8 +402,6 @@
::rtry/when rtry/conflict-exception? ::rtry/when rtry/conflict-exception?
::sm/params schema:create-file-thumbnail} ::sm/params schema:create-file-thumbnail}
;; FIXME: do not run the thumbnail upload inside a transaction
[cfg {:keys [::rpc/profile-id file-id] :as params}] [cfg {:keys [::rpc/profile-id file-id] :as params}]
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
;; TODO For now we check read permissions instead of write, ;; TODO For now we check read permissions instead of write,
@@ -414,6 +409,6 @@
;; review this approach on the future. ;; review this approach on the future.
(files/check-read-permissions! conn profile-id file-id) (files/check-read-permissions! conn profile-id file-id)
(when-not (db/read-only? conn) (when-not (db/read-only? conn)
(let [media (create-file-thumbnail cfg params)] (let [media (create-file-thumbnail! cfg params)]
{:uri (files/resolve-public-uri (:id media)) {:uri (files/resolve-public-uri (:id media))
:id (:id media)}))))) :id (:id media)})))))

View File

@@ -6,17 +6,14 @@
(ns app.rpc.commands.fonts (ns app.rpc.commands.fonts
(:require (:require
[app.binfile.common :as bfc]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.media :as cmedia]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.time :as ct] [app.common.time :as ct]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.db :as db] [app.db :as db]
[app.db.sql :as-alias sql] [app.db.sql :as-alias sql]
[app.features.logical-deletion :as ldel] [app.features.logical-deletion :as ldel]
[app.http :as-alias http]
[app.loggers.audit :as-alias audit] [app.loggers.audit :as-alias audit]
[app.loggers.webhooks :as-alias webhooks] [app.loggers.webhooks :as-alias webhooks]
[app.media :as media] [app.media :as media]
@@ -29,19 +26,7 @@
[app.rpc.helpers :as rph] [app.rpc.helpers :as rph]
[app.rpc.quotes :as quotes] [app.rpc.quotes :as quotes]
[app.storage :as sto] [app.storage :as sto]
[app.storage.tmp :as tmp] [app.util.services :as sv]))
[app.util.services :as sv]
[datoteka.io :as io])
(:import
java.io.InputStream
java.io.OutputStream
java.io.SequenceInputStream
java.util.Collections
java.util.zip.ZipEntry
java.util.zip.ZipOutputStream))
(set! *warn-on-reflection* true)
(def valid-weight #{100 200 300 400 500 600 700 800 900 950}) (def valid-weight #{100 200 300 400 500 600 700 800 900 950})
(def valid-style #{"normal" "italic"}) (def valid-style #{"normal" "italic"})
@@ -81,7 +66,7 @@
(uuid? file-id) (uuid? file-id)
(let [file (db/get-by-id conn :file file-id {:columns [:id :project-id]}) (let [file (db/get-by-id conn :file file-id {:columns [:id :project-id]})
project (db/get-by-id conn :project (:project-id file) {:columns [:id :team-id]}) project (db/get-by-id conn :project (:project-id file) {:columns [:id :team-id]})
perms (bfc/get-file-permissions conn profile-id file-id share-id)] perms (files/get-permissions conn profile-id file-id share-id)]
(files/check-read-permissions! perms) (files/check-read-permissions! perms)
(db/query conn :team-font-variant (db/query conn :team-font-variant
{:team-id (:team-id project) {:team-id (:team-id project)
@@ -93,8 +78,7 @@
(def ^:private schema:create-font-variant (def ^:private schema:create-font-variant
[:map {:title "create-font-variant"} [:map {:title "create-font-variant"}
[:team-id ::sm/uuid] [:team-id ::sm/uuid]
[:data [:map-of ::sm/text [:or ::sm/bytes [:data [:map-of ::sm/text ::sm/any]]
[::sm/vec ::sm/bytes]]]]
[:font-id ::sm/uuid] [:font-id ::sm/uuid]
[:font-family ::sm/text] [:font-family ::sm/text]
[:font-weight [::sm/one-of {:format "number"} valid-weight]] [:font-weight [::sm/one-of {:format "number"} valid-weight]]
@@ -120,7 +104,7 @@
(defn create-font-variant (defn create-font-variant
[{:keys [::sto/storage ::db/conn]} {:keys [data] :as params}] [{:keys [::sto/storage ::db/conn]} {:keys [data] :as params}]
(letfn [(generate-missing [data] (letfn [(generate-missing! [data]
(let [data (media/run {:cmd :generate-fonts :input data})] (let [data (media/run {:cmd :generate-fonts :input data})]
(when (and (not (contains? data "font/otf")) (when (and (not (contains? data "font/otf"))
(not (contains? data "font/ttf")) (not (contains? data "font/ttf"))
@@ -131,26 +115,8 @@
:hint "invalid font upload, unable to generate missing font assets")) :hint "invalid font upload, unable to generate missing font assets"))
data)) data))
(process-chunks [chunks]
(let [tmp (tmp/tempfile :prefix "penpot.tempfont." :suffix "")
streams (map io/input-stream chunks)
streams (Collections/enumeration streams)]
(with-open [^OutputStream output (io/output-stream tmp)
^InputStream input (SequenceInputStream. streams)]
(io/copy input output))
tmp))
(join-chunks [data]
(reduce-kv (fn [data mtype content]
(if (vector? content)
(assoc data mtype (process-chunks content))
data))
data
data))
(prepare-font [data mtype] (prepare-font [data mtype]
(when-let [resource (get data mtype)] (when-let [resource (get data mtype)]
(let [hash (sto/calculate-hash resource) (let [hash (sto/calculate-hash resource)
content (-> (sto/content resource) content (-> (sto/content resource)
(sto/wrap-with-hash hash))] (sto/wrap-with-hash hash))]
@@ -189,8 +155,7 @@
:otf-file-id (:id otf) :otf-file-id (:id otf)
:ttf-file-id (:id ttf)}))] :ttf-file-id (:id ttf)}))]
(let [data (join-chunks data) (let [data (generate-missing! data)
data (generate-missing data)
assets (persist-fonts-files! data) assets (persist-fonts-files! data)
result (insert-font-variant! assets)] result (insert-font-variant! assets)]
(vary-meta result assoc ::audit/replace-props (update params :data (comp vec keys)))))) (vary-meta result assoc ::audit/replace-props (update params :data (comp vec keys))))))
@@ -300,98 +265,3 @@
(rph/with-meta (rph/wrap) (rph/with-meta (rph/wrap)
{::audit/props {:font-family (:font-family variant) {::audit/props {:font-family (:font-family variant)
:font-id (:font-id variant)}}))) :font-id (:font-id variant)}})))
;; --- DOWNLOAD FONT
(defn- make-temporal-storage-object
[cfg profile-id content]
(let [storage (sto/resolve cfg)
content (media/check-input content)
hash (sto/calculate-hash (:path content))
data (-> (sto/content (:path content))
(sto/wrap-with-hash hash))
mtype (:mtype content "application/octet-stream")
content {::sto/content data
::sto/deduplicate? true
::sto/touched-at (ct/in-future {:minutes 30})
:profile-id profile-id
:content-type mtype
:bucket "tempfile"}]
(sto/put-object! storage content)))
(defn- make-variant-filename
[v mtype]
(str (:font-family v) "-" (:font-weight v)
(when-not (= "normal" (:font-style v)) (str "-" (:font-style v)))
(cmedia/mtype->extension mtype)))
(def ^:private schema:download-font
[:map {:title "download-font"}
[:id ::sm/uuid]])
(sv/defmethod ::download-font
"Download the font file. Returns a http redirect to the asset resource uri."
{::doc/added "2.15"
::sm/params schema:download-font}
[{:keys [::sto/storage ::db/pool] :as cfg} {:keys [::rpc/profile-id id]}]
(let [variant (db/get pool :team-font-variant {:id id})]
(teams/check-read-permissions! pool profile-id (:team-id variant))
;; Try to get the best available font format (prefer TTF for broader compatibility).
(let [media-id (or (:ttf-file-id variant)
(:otf-file-id variant)
(:woff2-file-id variant)
(:woff1-file-id variant))
sobj (sto/get-object storage media-id)
mtype (-> sobj meta :content-type)]
{:id (:id sobj)
:uri (files/resolve-public-uri (:id sobj))
:name (make-variant-filename variant mtype)})))
(def ^:private schema:download-font-family
[:map {:title "download-font-family"}
[:font-id ::sm/uuid]])
(sv/defmethod ::download-font-family
"Download the entire font family as a zip file. Returns the zip
bytes on the body, without encoding it on transit or json."
{::doc/added "2.15"
::sm/params schema:download-font-family}
[{:keys [::sto/storage ::db/pool] :as cfg} {:keys [::rpc/profile-id font-id]}]
(let [variants (db/query pool :team-font-variant
{:font-id font-id
:deleted-at nil})]
(when-not (seq variants)
(ex/raise :type :not-found
:code :object-not-found))
(teams/check-read-permissions! pool profile-id (:team-id (first variants)))
(let [tempfile (tmp/tempfile :suffix ".zip")
ffamily (-> variants first :font-family)]
(with-open [^OutputStream output (io/output-stream tempfile)
^OutputStream output (ZipOutputStream. output)]
(doseq [v variants]
(let [media-id (or (:ttf-file-id v)
(:otf-file-id v)
(:woff2-file-id v)
(:woff1-file-id v))
sobj (sto/get-object storage media-id)
mtype (-> sobj meta :content-type)
name (make-variant-filename v mtype)]
(with-open [input (sto/get-object-data storage sobj)]
(.putNextEntry ^ZipOutputStream output (ZipEntry. ^String name))
(io/copy input output :size (:size sobj))
(.closeEntry ^ZipOutputStream output)))))
(let [{:keys [id] :as sobj} (make-temporal-storage-object cfg profile-id
{:mtype "application/zip"
:path tempfile})]
{:id id
:uri (files/resolve-public-uri id)
:name (str ffamily ".zip")}))))

View File

@@ -21,7 +21,6 @@
[app.loggers.audit :as audit] [app.loggers.audit :as audit]
[app.main :as-alias main] [app.main :as-alias main]
[app.media :as media] [app.media :as media]
[app.nitrate :as nitrate]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.rpc.climit :as climit] [app.rpc.climit :as climit]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
@@ -48,7 +47,6 @@
(def schema:props (def schema:props
[:map {:title "ProfileProps"} [:map {:title "ProfileProps"}
[:plugins {:optional true} schema:plugin-registry] [:plugins {:optional true} schema:plugin-registry]
[:mcp-status {:optional true} ::sm/boolean]
[:newsletter-updates {:optional true} ::sm/boolean] [:newsletter-updates {:optional true} ::sm/boolean]
[:newsletter-news {:optional true} ::sm/boolean] [:newsletter-news {:optional true} ::sm/boolean]
[:onboarding-team-id {:optional true} ::sm/uuid] [:onboarding-team-id {:optional true} ::sm/uuid]
@@ -90,8 +88,6 @@
;; --- QUERY: Get profile (own) ;; --- QUERY: Get profile (own)
(sv/defmethod ::get-profile (sv/defmethod ::get-profile
{::rpc/auth false {::rpc/auth false
::doc/added "1.18" ::doc/added "1.18"
@@ -102,13 +98,9 @@
;; no profile-id is in session, and when db call raises not found. In all other ;; no profile-id is in session, and when db call raises not found. In all other
;; cases we need to reraise the exception. ;; cases we need to reraise the exception.
(try (try
(let [profile (-> (get-profile pool profile-id) (-> (get-profile pool profile-id)
(strip-private-attrs) (strip-private-attrs)
(update :props filter-props))] (update :props filter-props))
(if (contains? cf/flags :nitrate)
(nitrate/add-nitrate-licence-to-profile cfg profile)
profile))
(catch Throwable _ (catch Throwable _
{:id uuid/zero :fullname "Anonymous User"}))) {:id uuid/zero :fullname "Anonymous User"})))

View File

@@ -19,7 +19,7 @@
inner join team_profile_rel as tpr on (tpr.team_id = p.team_id) inner join team_profile_rel as tpr on (tpr.team_id = p.team_id)
where tpr.profile_id = ? where tpr.profile_id = ?
and p.team_id = ? and p.team_id = ?
and (p.deleted_at is null) and (p.deleted_at is null or p.deleted_at > now())
and (tpr.is_admin = true or and (tpr.is_admin = true or
tpr.is_owner = true or tpr.is_owner = true or
tpr.can_edit = true) tpr.can_edit = true)
@@ -29,7 +29,7 @@
inner join project_profile_rel as ppr on (ppr.project_id = p.id) inner join project_profile_rel as ppr on (ppr.project_id = p.id)
where ppr.profile_id = ? where ppr.profile_id = ?
and p.team_id = ? and p.team_id = ?
and (p.deleted_at is null) and (p.deleted_at is null or p.deleted_at > now())
and (ppr.is_admin = true or and (ppr.is_admin = true or
ppr.is_owner = true or ppr.is_owner = true or
ppr.can_edit = true) ppr.can_edit = true)
@@ -47,7 +47,7 @@
left join file_thumbnail as ft on (ft.file_id = f.id and ft.revn = f.revn) left join file_thumbnail as ft on (ft.file_id = f.id and ft.revn = f.revn)
inner join projects as pr on (f.project_id = pr.id) inner join projects as pr on (f.project_id = pr.id)
where f.name ilike ('%' || ? || '%') where f.name ilike ('%' || ? || '%')
and (f.deleted_at is null) and (f.deleted_at is null or f.deleted_at > now())
order by f.created_at asc") order by f.created_at asc")
(defn search-files (defn search-files

View File

@@ -23,7 +23,6 @@
[app.main :as-alias main] [app.main :as-alias main]
[app.media :as media] [app.media :as media]
[app.msgbus :as mbus] [app.msgbus :as mbus]
[app.nitrate :as nitrate]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.rpc.commands.profile :as profile] [app.rpc.commands.profile :as profile]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
@@ -191,9 +190,7 @@
::sm/params schema:get-teams} ::sm/params schema:get-teams}
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
(dm/with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(cond->> (get-teams conn profile-id) (get-teams conn profile-id)))
(contains? cf/flags :nitrate)
(map #(nitrate/add-org-to-team cfg % params)))))
(def ^:private sql:get-owned-teams (def ^:private sql:get-owned-teams
"SELECT t.id, t.name, "SELECT t.id, t.name,

View File

@@ -248,11 +248,11 @@
invitations (into #{} invitations (into #{}
(comp (comp
;; We don't re-send invitations to ;; We don't re-send invitations to
;; already existing members ;; already existing members
(remove #(contains? team-members (:email %))) (remove #(contains? team-members (:email %)))
;; We don't send invitations to ;; We don't send invitations to
;; join-requested members ;; join-requested members
(remove #(contains? join-requests (:email %))) (remove #(contains? join-requests (:email %)))
(map (fn [{:keys [email role]}] (map (fn [{:keys [email role]}]
(create-invitation cfg (create-invitation cfg

View File

@@ -13,6 +13,7 @@
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.rpc.commands.files :as files]
[app.rpc.commands.teams :as teams] [app.rpc.commands.teams :as teams]
[app.rpc.cond :as-alias cond] [app.rpc.cond :as-alias cond]
[app.rpc.doc :as-alias doc] [app.rpc.doc :as-alias doc]
@@ -120,7 +121,7 @@
[system {:keys [::rpc/profile-id file-id share-id] :as params}] [system {:keys [::rpc/profile-id file-id share-id] :as params}]
(db/run! system (db/run! system
(fn [{:keys [::db/conn] :as system}] (fn [{:keys [::db/conn] :as system}]
(let [perms (bfc/get-file-permissions conn profile-id file-id share-id) (let [perms (files/get-permissions conn profile-id file-id share-id)
params (-> params params (-> params
(assoc ::perms perms) (assoc ::perms perms)
(assoc :profile-id profile-id))] (assoc :profile-id profile-id))]

View File

@@ -1,114 +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.rpc.management.nitrate
"Internal Nitrate HTTP RPC API. Provides authenticated access to
organization management and token validation endpoints."
(:require
[app.common.schema :as sm]
[app.common.types.profile :refer [schema:profile, schema:basic-profile]]
[app.common.types.team :refer [schema:team]]
[app.common.uuid :as uuid]
[app.db :as db]
[app.msgbus :as mbus]
[app.rpc :as-alias rpc]
[app.rpc.commands.files :as files]
[app.rpc.commands.profile :as profile]
[app.rpc.doc :as doc]
[app.util.services :as sv]))
;; ---- API: authenticate
(sv/defmethod ::authenticate
"Authenticate the current user"
{::doc/added "2.14"
::sm/params [:map]
::sm/result schema:profile}
[cfg {:keys [::rpc/profile-id] :as params}]
(let [profile (profile/get-profile cfg profile-id)]
{:id (get profile :id)
:name (get profile :fullname)
:email (get profile :email)
:photo-url (files/resolve-public-uri (get profile :photo-id))}))
;; ---- API: get-teams
(def ^:private sql:get-teams
"SELECT t.*
FROM team AS t
JOIN team_profile_rel AS tpr ON t.id = tpr.team_id
WHERE tpr.profile_id = ?
AND tpr.is_owner IS TRUE
AND t.is_default IS FALSE
AND t.deleted_at IS NULL;")
(def ^:private schema:get-teams-result
[:vector schema:team])
(sv/defmethod ::get-teams
"List teams for which current user is owner"
{::doc/added "2.14"
::sm/params [:map]
::sm/result schema:get-teams-result}
[cfg {:keys [::rpc/profile-id]}]
(let [current-user-id (-> (profile/get-profile cfg profile-id) :id)]
(->> (db/exec! cfg [sql:get-teams current-user-id])
(map #(select-keys % [:id :name])))))
;; ---- API: notify-team-change
(def ^:private schema:notify-team-change
[:map
[:id ::sm/uuid]
[:organization-id ::sm/text]])
(sv/defmethod ::notify-team-change
"Notify to Penpot a team change from nitrate"
{::doc/added "2.14"
::sm/params schema:notify-team-change
::rpc/auth false}
[cfg {:keys [id organization-id organization-name]}]
(let [msgbus (::mbus/msgbus cfg)]
(mbus/pub! msgbus
;;TODO There is a bug on dashboard with teams notifications.
;;For now we send it to uuid/zero instead of team-id
:topic uuid/zero
:message {:type :team-org-change
:team-id id
:organization-id organization-id
:organization-name organization-name})))
;; ---- API: get-managed-profiles
(def ^:private sql:get-managed-profiles
"SELECT DISTINCT p.id, p.fullname as name, p.email
FROM profile p
JOIN team_profile_rel tpr_member
ON tpr_member.profile_id = p.id
WHERE p.id <> ?
AND EXISTS (
SELECT 1
FROM team_profile_rel tpr_owner
JOIN team t
ON t.id = tpr_owner.team_id
WHERE tpr_owner.profile_id = ?
AND tpr_owner.team_id = tpr_member.team_id
AND tpr_owner.is_owner IS TRUE
AND t.is_default IS FALSE
AND t.deleted_at IS NULL);")
(def schema:managed-profile-result
[:vector schema:basic-profile])
(sv/defmethod ::get-managed-profiles
"List profiles that belong to teams for which current user is owner"
{::doc/added "2.14"
::sm/params [:map]
::sm/result schema:managed-profile-result}
[cfg {:keys [::rpc/profile-id]}]
(let [current-user-id (-> (profile/get-profile cfg profile-id) :id)]
(db/exec! cfg [sql:get-managed-profiles current-user-id current-user-id])))

View File

@@ -0,0 +1,183 @@
;; 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.rpc.management.subscription
(:require
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.schema.generators :as sg]
[app.common.time :as ct]
[app.db :as db]
[app.rpc :as-alias rpc]
[app.rpc.commands.profile :as profile]
[app.rpc.doc :as doc]
[app.util.services :as sv]))
;; ---- RPC METHOD: AUTHENTICATE
(def ^:private
schema:authenticate-params
[:map {:title "authenticate-params"}])
(def ^:private
schema:authenticate-result
[:map {:title "authenticate-result"}
[:profile-id ::sm/uuid]])
(sv/defmethod ::auth
{::doc/added "2.12"
::sm/params schema:authenticate-params
::sm/result schema:authenticate-result}
[_ {:keys [::rpc/profile-id]}]
{:profile-id profile-id})
;; ---- RPC METHOD: GET-CUSTOMER
;; FIXME: move to app.common.time
(def ^:private schema:timestamp
(sm/type-schema
{:type ::timestamp
:pred ct/inst?
:type-properties
{:title "inst"
:description "The same as :app.common.time/inst but encodes to epoch"
:error/message "should be an instant"
:gen/gen (->> (sg/small-int)
(sg/fmap (fn [v] (ct/inst v))))
:decode/string #(some-> % ct/inst)
:encode/string #(some-> % inst-ms)
:decode/json #(some-> % ct/inst)
:encode/json #(some-> % inst-ms)}}))
(def ^:private schema:subscription
[:map {:title "Subscription"}
[:id ::sm/text]
[:customer-id ::sm/text]
[:type [:enum
"unlimited"
"professional"
"enterprise"]]
[:status [:enum
"active"
"canceled"
"incomplete"
"incomplete_expired"
"past_due"
"paused"
"trialing"
"unpaid"]]
[:billing-period [:enum
"month"
"day"
"week"
"year"]]
[:quantity :int]
[:description [:maybe ::sm/text]]
[:created-at schema:timestamp]
[:start-date [:maybe schema:timestamp]]
[:ended-at [:maybe schema:timestamp]]
[:trial-end [:maybe schema:timestamp]]
[:trial-start [:maybe schema:timestamp]]
[:cancel-at [:maybe schema:timestamp]]
[:canceled-at [:maybe schema:timestamp]]
[:current-period-end [:maybe schema:timestamp]]
[:current-period-start [:maybe schema:timestamp]]
[:cancel-at-period-end :boolean]
[:cancellation-details
[:map {:title "CancellationDetails"}
[:comment [:maybe ::sm/text]]
[:reason [:maybe ::sm/text]]
[:feedback [:maybe
[:enum
"customer_service"
"low_quality"
"missing_feature"
"other"
"switched_service"
"too_complex"
"too_expensive"
"unused"]]]]]])
(def ^:private sql:get-customer-slots
"WITH teams AS (
SELECT tpr.team_id AS id,
tpr.profile_id AS profile_id
FROM team_profile_rel AS tpr
WHERE tpr.is_owner IS true
AND tpr.profile_id = ?
), teams_with_slots AS (
SELECT tpr.team_id AS id,
count(*) AS total
FROM team_profile_rel AS tpr
WHERE tpr.team_id IN (SELECT id FROM teams)
AND tpr.can_edit IS true
GROUP BY 1
ORDER BY 2
)
SELECT max(total) AS total FROM teams_with_slots;")
(defn- get-customer-slots
[cfg profile-id]
(let [result (db/exec-one! cfg [sql:get-customer-slots profile-id])]
(:total result)))
(def ^:private schema:get-customer-params
[:map])
(def ^:private schema:get-customer-result
[:map
[:id ::sm/uuid]
[:name :string]
[:num-editors ::sm/int]
[:subscription {:optional true} schema:subscription]])
(sv/defmethod ::get-customer
{::doc/added "2.12"
::sm/params schema:get-customer-params
::sm/result schema:get-customer-result}
[cfg {:keys [::rpc/profile-id]}]
(let [profile (profile/get-profile cfg profile-id)]
{:id (get profile :id)
:name (get profile :fullname)
:email (get profile :email)
:num-editors (get-customer-slots cfg profile-id)
:subscription (-> profile :props :subscription)}))
;; ---- RPC METHOD: GET-CUSTOMER
(def ^:private schema:update-customer-params
[:map
[:subscription [:maybe schema:subscription]]])
(def ^:private schema:update-customer-result
[:map])
(sv/defmethod ::update-customer
{::doc/added "2.12"
::sm/params schema:update-customer-params
::sm/result schema:update-customer-result}
[cfg {:keys [::rpc/profile-id subscription]}]
(let [{:keys [props] :as profile}
(profile/get-profile cfg profile-id ::db/for-update true)
props
(assoc props :subscription subscription)]
(l/dbg :hint "update customer"
:profile-id (str profile-id)
:subscription-type (get subscription :type)
:subscription-status (get subscription :status)
:subscription-quantity (get subscription :quantity))
(db/update! cfg :profile
{:props (db/tjson props)}
{:id profile-id}
{::db/return-keys false})
nil))

View File

@@ -52,8 +52,6 @@
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
[app.http :as-alias http] [app.http :as-alias http]
[app.loggers.database :as loggers.db]
[app.loggers.mattermost :as loggers.mm]
[app.redis :as rds] [app.redis :as rds]
[app.redis.script :as-alias rscript] [app.redis.script :as-alias rscript]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
@@ -106,29 +104,28 @@
(def ^:private schema:limit (def ^:private schema:limit
[:and [:and
[:map [:map
[::name :keyword] [::name :any]
[::strategy schema:strategy] [::strategy schema:strategy]
[::key :string] [::key :string]
[::opts :string] [::opts :string]]
[::capacity {:optional true} ::sm/int] [:or
[::rate {:optional true} ::sm/int] [:map
[::interval {:optional true} ::ct/duration] [::capacity ::sm/int]
[::params {:optional true} [::sm/vec :any]] [::rate ::sm/int]
[::permits {:optional true} ::sm/int] [::internal ::ct/duration]
[::unit {:optional true} [:enum :days :hours :minutes :seconds :weeks]]] [::params [::sm/vec :any]]]
[:fn (fn [attrs] [:map
(let [contains-fn (partial contains? attrs)] [::nreq ::sm/int]
(or (every? contains-fn [::capacity ::rate ::interval]) [::unit [:enum :days :hours :minutes :seconds :weeks]]]]])
(every? contains-fn [::permits ::unit]))))]])
(def ^:private schema:limits (def ^:private schema:limits
[:map-of :keyword [::sm/vec schema:limit]]) [:map-of :keyword [::sm/vec schema:limit]])
(def ^:private valid-limit-tuple? (def ^:private valid-limit-tuple?
(sm/validator schema:limit-tuple)) (sm/lazy-validator schema:limit-tuple))
(def ^:private valid-rlimit-instance? (def ^:private valid-rlimit-instance?
(sm/validator ::rpc/rlimit)) (sm/lazy-validator ::rpc/rlimit))
(defmethod parse-limit :window (defmethod parse-limit :window
[[name strategy opts :as vlimit]] [[name strategy opts :as vlimit]]
@@ -137,16 +134,16 @@
(merge (merge
{::name name {::name name
::strategy strategy} ::strategy strategy}
(if-let [[_ permits unit] (re-find window-opts-re opts)] (if-let [[_ nreq unit] (re-find window-opts-re opts)]
(let [permits (parse-long permits)] (let [nreq (parse-long nreq)]
{::permits permits {::nreq nreq
::unit (case unit ::unit (case unit
"d" :days "d" :days
"h" :hours "h" :hours
"m" :minutes "m" :minutes
"s" :seconds "s" :seconds
"w" :weeks) "w" :weeks)
::key (str "penpot.rlimit." (cf/get :tenant) ".window." (d/name name)) ::key (str "ratelimit.window." (d/name name))
::opts opts}) ::opts opts})
(ex/raise :type :validation (ex/raise :type :validation
:code :invalid-window-limit-opts :code :invalid-window-limit-opts
@@ -167,15 +164,15 @@
::interval interval ::interval interval
::opts opts ::opts opts
::params [(->seconds interval) rate capacity] ::params [(->seconds interval) rate capacity]
::key (str "penpot.rlimit." (cf/get :tenant) ".bucket." (d/name name))}) ::key (str "ratelimit.bucket." (d/name name))})
(ex/raise :type :validation (ex/raise :type :validation
:code :invalid-bucket-limit-opts :code :invalid-bucket-limit-opts
:hint (str/ffmt "looks like '%' does not have a valid format" opts)))) :hint (str/ffmt "looks like '%' does not have a valid format" opts))))
(defmethod process-limit :bucket (defmethod process-limit :bucket
[rconn profile-id now {:keys [::key ::params ::method ::capacity ::interval ::rate] :as limit}] [rconn user-id now {:keys [::key ::params ::service ::capacity ::interval ::rate] :as limit}]
(let [script (-> bucket-rate-limit-script (let [script (-> bucket-rate-limit-script
(assoc ::rscript/keys [(str key "." method "." profile-id)]) (assoc ::rscript/keys [(str key "." service "." user-id)])
(assoc ::rscript/vals (conj params (->seconds now)))) (assoc ::rscript/vals (conj params (->seconds now))))
result (rds/eval rconn script) result (rds/eval rconn script)
allowed? (boolean (nth result 0)) allowed? (boolean (nth result 0))
@@ -183,7 +180,7 @@
reset (* (/ (inst-ms interval) rate) reset (* (/ (inst-ms interval) rate)
(- capacity remaining))] (- capacity remaining))]
(l/trace :hint "limit processed" (l/trace :hint "limit processed"
:method method :service service
:limit (name (::name limit)) :limit (name (::name limit))
:strategy (name (::strategy limit)) :strategy (name (::strategy limit))
:opts (::opts limit) :opts (::opts limit)
@@ -195,31 +192,30 @@
(assoc ::lresult/remaining remaining)))) (assoc ::lresult/remaining remaining))))
(defmethod process-limit :window (defmethod process-limit :window
[rconn uid now {:keys [::permits ::unit ::key ::method] :as limit}] [rconn user-id now {:keys [::nreq ::unit ::key ::service] :as limit}]
(let [ts (ct/truncate now unit) (let [ts (ct/truncate now unit)
ttl (ct/diff now (ct/plus ts {unit 1})) ttl (ct/diff now (ct/plus ts {unit 1}))
script (-> window-rate-limit-script script (-> window-rate-limit-script
(assoc ::rscript/keys [(str key "." method "." uid "." (ct/format-inst ts))]) (assoc ::rscript/keys [(str key "." service "." user-id "." (ct/format-inst ts))])
(assoc ::rscript/vals [permits (->seconds ttl)])) (assoc ::rscript/vals [nreq (->seconds ttl)]))
result (rds/eval rconn script) result (rds/eval rconn script)
allowed? (boolean (nth result 0)) allowed? (boolean (nth result 0))
remaining (nth result 1)] remaining (nth result 1)]
(l/trace :hint "limit processed" (l/trace :hint "limit processed"
:method method :service service
:name (name (::name limit)) :limit (name (::name limit))
:strategy (name (::strategy limit)) :strategy (name (::strategy limit))
:opts (::opts limit) :opts (::opts limit)
:allowed allowed? :allowed allowed?
:remaining remaining) :remaining remaining)
(-> limit (-> limit
(assoc ::lresult/allowed allowed?) (assoc ::lresult/allowed allowed?)
(assoc ::lresult/timestamp ts)
(assoc ::lresult/remaining remaining) (assoc ::lresult/remaining remaining)
(assoc ::lresult/reset (ct/plus ts {unit 1}))))) (assoc ::lresult/reset (ct/plus ts {unit 1})))))
(defn- process-limits (defn- process-limits
[{:keys [::rds/conn] :as cfg} uid limits now] [rconn user-id limits now]
(let [results (into [] (map (partial process-limit conn uid now)) limits) (let [results (into [] (map (partial process-limit rconn user-id now)) limits)
remaining (->> results remaining (->> results
(d/index-by ::name ::lresult/remaining) (d/index-by ::name ::lresult/remaining)
(uri/map->query-string)) (uri/map->query-string))
@@ -230,22 +226,11 @@
rejected (d/seek (complement ::lresult/allowed) results)] rejected (d/seek (complement ::lresult/allowed) results)]
(when rejected (when rejected
(let [event {::id (uuid/next) (l/warn :hint "rejected rate limit"
::uid uid :user-id (str user-id)
::method (-> rejected ::method name) :limit-service (-> rejected ::service name)
::name (-> rejected ::name name) :limit-name (-> rejected ::name name)
::strategy (-> rejected ::strategy name) :limit-strategy (-> rejected ::strategy name)))
::results results}]
(l/warn :hint "rejected rate limit"
:method (-> rejected ::method name)
:name (-> rejected ::name name)
:strategy (-> rejected ::strategy name)
:uid (str uid)
:report-id (:id event))
(loggers.mm/emit cfg event)
(loggers.db/emit cfg event)))
{::enabled true {::enabled true
::allowed (not (some? rejected)) ::allowed (not (some? rejected))
@@ -258,7 +243,7 @@
[state skey sname] [state skey sname]
(when-let [limits (or (get-in @state [::limits skey]) (when-let [limits (or (get-in @state [::limits skey])
(get-in @state [::limits :default]))] (get-in @state [::limits :default]))]
(into [] (map #(assoc % ::method sname)) limits))) (into [] (map #(assoc % ::service sname)) limits)))
(defn- get-uid (defn- get-uid
[{:keys [::rpc/profile-id] :as params}] [{:keys [::rpc/profile-id] :as params}]
@@ -268,10 +253,10 @@
uuid/zero))) uuid/zero)))
(defn- process-request' (defn- process-request'
[cfg limits params] [{:keys [::rds/conn] :as cfg} limits params]
(try (try
(let [uid (get-uid params) (let [uid (get-uid params)
result (process-limits cfg uid limits (ct/now))] result (process-limits conn uid limits (ct/now))]
(if (contains? cf/flags :soft-rpc-rlimit) (if (contains? cf/flags :soft-rpc-rlimit)
{::enabled false} {::enabled false}
result)) result))
@@ -289,8 +274,8 @@
(assert (or (nil? rlimit) (valid-rlimit-instance? rlimit)) "expected a valid rlimit instance") (assert (or (nil? rlimit) (valid-rlimit-instance? rlimit)) "expected a valid rlimit instance")
(if rlimit (if rlimit
(let [skey (keyword (::rpc/module cfg) (->> mdata ::sv/spec name)) (let [skey (keyword (::rpc/type cfg) (->> mdata ::sv/spec name))
sname (str (::rpc/module cfg) "." (->> mdata ::sv/spec name)) sname (str (::rpc/type cfg) "." (->> mdata ::sv/spec name))
cfg (-> cfg cfg (-> cfg
(assoc ::skey skey) (assoc ::skey skey)
(assoc ::sname sname))] (assoc ::sname sname))]
@@ -386,9 +371,12 @@
(defn- on-refresh-error (defn- on-refresh-error
[_ cause] [_ cause]
(when-not (instance? java.util.concurrent.RejectedExecutionException cause) (when-not (instance? java.util.concurrent.RejectedExecutionException cause)
(l/warn :hint "unexpected exception on loading config" (if-let [explain (-> cause ex-data ex/explain)]
:cause cause (l/warn ::l/raw (str "unable to refresh config, invalid format:\n" explain)
::l/sync? true))) ::l/sync? true)
(l/warn :hint "unexpected exception on loading config"
:cause cause
::l/sync? true))))
(defn- get-config-path (defn- get-config-path
[] []

View File

@@ -25,9 +25,9 @@ local allowed = filled >= requested
local newTokens = filled local newTokens = filled
if allowed then if allowed then
newTokens = filled - requested newTokens = filled - requested
redis.call("hset", tokensKey, "tokens", newTokens, "timestamp", timestamp)
end end
redis.call("hset", tokensKey, "tokens", newTokens, "timestamp", timestamp)
redis.call("expire", tokensKey, ttl) redis.call("expire", tokensKey, ttl)
return { allowed, newTokens } return { allowed, newTokens }

View File

@@ -17,7 +17,6 @@
[app.setup.templates] [app.setup.templates]
[buddy.core.codecs :as bc] [buddy.core.codecs :as bc]
[buddy.core.nonce :as bn] [buddy.core.nonce :as bn]
[cuerdas.core :as str]
[integrant.core :as ig])) [integrant.core :as ig]))
(defn- generate-random-key (defn- generate-random-key
@@ -89,38 +88,7 @@
(-> (get-all-props conn) (-> (get-all-props conn)
(assoc :secret-key secret) (assoc :secret-key secret)
(assoc :tokens-key (keys/derive secret :salt "tokens")) (assoc :tokens-key (keys/derive secret :salt "tokens"))
(assoc :management-key (keys/derive secret :salt "management"))
(update :instance-id handle-instance-id conn (db/read-only? pool))))))) (update :instance-id handle-instance-id conn (db/read-only? pool)))))))
(sm/register! ::props [:map-of :keyword ::sm/any]) (sm/register! ::props [:map-of :keyword ::sm/any])
(defmethod ig/init-key ::shared-keys
[_ {:keys [::props] :as cfg}]
(let [secret (get props :secret-key)]
(d/without-nils
{:exporter
(let [key (or (get cfg :exporter)
(-> (keys/derive secret :salt "exporter")
(bc/bytes->b64-str true)))]
(if (or (str/empty? key)
(str/blank? key))
(do
(l/wrn :hint "exporter key is disabled because empty string found")
nil)
(do
(l/inf :hint "exporter key initialized" :key (d/obfuscate-string key))
key)))
:nitrate
(let [key (or (get cfg :nitrate)
(-> (keys/derive secret :salt "nitrate")
(bc/bytes->b64-str true)))]
(if (or (str/empty? key)
(str/blank? key))
(do
(l/wrn :hint "nitrate key is disabled because empty string found")
nil)
(do
(l/inf :hint "nitrate key initialized" :key (d/obfuscate-string key))
key)))})))

View File

@@ -9,35 +9,48 @@
modification of time offset (useful for testing and time adjustments)." modification of time offset (useful for testing and time adjustments)."
(:require (:require
[app.common.logging :as l] [app.common.logging :as l]
[app.common.time :as ct])) [app.common.time :as ct]
[app.setup :as-alias setup]
[integrant.core :as ig])
(:import
java.time.Clock
java.time.Duration
java.time.Instant
java.time.ZoneId))
(defonce state (defonce current
(atom {})) (atom {:clock (Clock/systemDefaultZone)
:offset nil}))
(defn assign-offset (defmethod ig/init-key ::setup/clock
"Assign virtual clock offset to a specific user. Is the responsability [_ _]
of RPC module to properly bind the correct clock to the user (add-watch current ::common
request." (fn [_ _ _ {:keys [clock offset]}]
[profile-id duration] (let [clock (if (ct/duration? offset)
(swap! state (fn [state] (Clock/offset ^Clock clock
(if (nil? duration) ^Duration offset)
(dissoc state profile-id) clock)]
(assoc state profile-id duration))))) (l/wrn :hint "altering clock" :clock (str clock))
(alter-var-root #'ct/*clock* (constantly clock))))))
(defn get-offset
[profile-id]
(get @state profile-id))
(defn get-clock (defmethod ig/halt-key! ::setup/clock
[profile-id] [_ _]
(if-let [offset (get-offset profile-id)] (remove-watch current ::common))
(ct/offset-clock offset)
(ct/get-system-clock)))
(defn set-global-clock (defn fixed
"Get fixed clock, mainly used in tests"
[instant]
(Clock/fixed ^Instant (ct/inst instant)
^ZoneId (ZoneId/of "Z")))
(defn set-offset!
[duration]
(swap! current assoc :offset (some-> duration ct/duration)))
(defn set-clock!
([] ([]
(set-global-clock (ct/get-system-clock))) (swap! current assoc :clock (Clock/systemDefaultZone)))
([clock] ([clock]
(assert (ct/clock? clock) "expected valid clock instance") (when (instance? Clock clock)
(l/wrn :hint "altering clock" :clock (str clock)) (swap! current assoc :clock clock))))
(alter-var-root #'ct/*clock* (constantly clock))))

View File

@@ -35,9 +35,6 @@
:assets-s3 :s3 :assets-s3 :s3
nil))) nil)))
(def default-bucket
"file-media-object")
(def valid-buckets (def valid-buckets
#{"file-media-object" #{"file-media-object"
"team-font-variant" "team-font-variant"

View File

@@ -25,7 +25,7 @@
[app.common.time :as ct] [app.common.time :as ct]
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
[app.storage :as sto] [app.storage :as-alias sto]
[app.storage.impl :as impl] [app.storage.impl :as impl]
[integrant.core :as ig])) [integrant.core :as ig]))
@@ -130,7 +130,7 @@
[{:keys [metadata]}] [{:keys [metadata]}]
(or (some-> metadata :bucket) (or (some-> metadata :bucket)
(some-> metadata :reference d/name) (some-> metadata :reference d/name)
sto/default-bucket)) "file-media-object"))
(defn- process-objects! (defn- process-objects!
[conn has-refs? bucket objects] [conn has-refs? bucket objects]

View File

@@ -33,7 +33,6 @@
java.util.Optional java.util.Optional
java.util.concurrent.atomic.AtomicLong java.util.concurrent.atomic.AtomicLong
org.reactivestreams.Subscriber org.reactivestreams.Subscriber
software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider
software.amazon.awssdk.core.ResponseBytes software.amazon.awssdk.core.ResponseBytes
software.amazon.awssdk.core.async.AsyncRequestBody software.amazon.awssdk.core.async.AsyncRequestBody
software.amazon.awssdk.core.async.AsyncResponseTransformer software.amazon.awssdk.core.async.AsyncResponseTransformer
@@ -200,8 +199,7 @@
(defn- build-s3-client (defn- build-s3-client
[{:keys [::region ::endpoint ::wrk/netty-io-executor]}] [{:keys [::region ::endpoint ::wrk/netty-io-executor]}]
(let [creds-provider (DefaultCredentialsProvider/create) (let [aconfig (-> (ClientAsyncConfiguration/builder)
aconfig (-> (ClientAsyncConfiguration/builder)
(.build)) (.build))
sconfig (-> (S3Configuration/builder) sconfig (-> (S3Configuration/builder)
@@ -223,7 +221,6 @@
builder (.asyncConfiguration ^S3AsyncClientBuilder builder ^ClientAsyncConfiguration aconfig) builder (.asyncConfiguration ^S3AsyncClientBuilder builder ^ClientAsyncConfiguration aconfig)
builder (.httpClient ^S3AsyncClientBuilder builder ^NettyNioAsyncHttpClient hclient) builder (.httpClient ^S3AsyncClientBuilder builder ^NettyNioAsyncHttpClient hclient)
builder (.region ^S3AsyncClientBuilder builder (lookup-region region)) builder (.region ^S3AsyncClientBuilder builder (lookup-region region))
builder (.credentialsProvider ^S3AsyncClientBuilder builder creds-provider)
builder (cond-> ^S3AsyncClientBuilder builder builder (cond-> ^S3AsyncClientBuilder builder
(some? endpoint) (some? endpoint)
(.endpointOverride (URI. (str endpoint))))] (.endpointOverride (URI. (str endpoint))))]
@@ -240,8 +237,7 @@
(defn- build-s3-presigner (defn- build-s3-presigner
[{:keys [::region ::endpoint]}] [{:keys [::region ::endpoint]}]
(let [creds-provider (DefaultCredentialsProvider/create) (let [config (-> (S3Configuration/builder)
config (-> (S3Configuration/builder)
(cond-> (some? endpoint) (.pathStyleAccessEnabled true)) (cond-> (some? endpoint) (.pathStyleAccessEnabled true))
(.build))] (.build))]
@@ -249,7 +245,6 @@
(cond-> (some? endpoint) (.endpointOverride (URI. (str endpoint)))) (cond-> (some? endpoint) (.endpointOverride (URI. (str endpoint))))
(.region (lookup-region region)) (.region (lookup-region region))
(.serviceConfiguration ^S3Configuration config) (.serviceConfiguration ^S3Configuration config)
(.credentialsProvider creds-provider)
(.build)))) (.build))))
(defn- write-input-stream (defn- write-input-stream

View File

@@ -45,8 +45,7 @@
:deleted-at (ct/format-inst deleted-at)) :deleted-at (ct/format-inst deleted-at))
(db/update! conn :file (db/update! conn :file
{:deleted-at deleted-at {:deleted-at deleted-at}
:is-shared false}
{:id id} {:id id}
{::db/return-keys false}) {::db/return-keys false})
@@ -54,7 +53,7 @@
(not *team-deletion*)) (not *team-deletion*))
;; NOTE: we don't prevent file deletion on absorb operation failure ;; NOTE: we don't prevent file deletion on absorb operation failure
(try (try
(db/tx-run! cfg files/absorb-library id) (db/tx-run! cfg files/absorb-library! id)
(catch Throwable cause (catch Throwable cause
(l/warn :hint "error on absorbing library" (l/warn :hint "error on absorbing library"
:file-id id :file-id id

View File

@@ -8,7 +8,7 @@
"A generic asynchronous events notifications subsystem; used mainly "A generic asynchronous events notifications subsystem; used mainly
for mark event points in functions and be able to attach listeners for mark event points in functions and be able to attach listeners
to them. Mainly used in http.sse for progress reporting." to them. Mainly used in http.sse for progress reporting."
(:refer-clojure :exclude [run!]) (:refer-clojure :exclude [tap run!])
(:require (:require
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.logging :as l] [app.common.logging :as l]

View File

@@ -7,18 +7,10 @@
(ns app.util.template (ns app.util.template
(:require (:require
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[cuerdas.core :as str]
[selmer.filters :as sf]
[selmer.parser :as sp])) [selmer.parser :as sp]))
;; (sp/cache-off!) ;; (sp/cache-off!)
(sf/add-filter! :abbreviate
(fn [s n]
(let [n (parse-long n)]
(str/abbreviate s n))))
(defn render (defn render
[path context] [path context]
(try (try

View File

@@ -103,14 +103,6 @@
(assoc-in [::db/pool ::db/username] (:database-username config)) (assoc-in [::db/pool ::db/username] (:database-username config))
(assoc-in [::db/pool ::db/password] (:database-password config)) (assoc-in [::db/pool ::db/password] (:database-password config))
(assoc-in [:app.rpc/methods :app.setup/templates] templates) (assoc-in [:app.rpc/methods :app.setup/templates] templates)
(assoc-in [:app.rpc/methods :app.setup/templates] templates)
(update :app.rpc/rlimit assoc
:app.loggers.mattermost/reporter nil
:app.loggers.database/reporter nil)
(update :app.rpc/methods assoc
:app.setup/templates templates
:app.loggers.mattermost/reporter nil
:app.loggers.database/reporter nil)
(dissoc :app.srepl/server (dissoc :app.srepl/server
:app.http/server :app.http/server
:app.http/route :app.http/route
@@ -603,8 +595,8 @@
(px/exec! :virtual #(rcp/write-body-to-stream body nil output)) (px/exec! :virtual #(rcp/write-body-to-stream body nil output))
(into [] (into []
(map (fn [{:keys [event data]}] (map (fn [{:keys [event data]}]
(d/vec2 (keyword event) [(keyword event)
(tr/decode-str data)))) (tr/decode-str data)]))
(parse-sse (slurp' input))) (parse-sse (slurp' input)))
(finally (finally
(.close input))))) (.close input)))))

View File

@@ -86,7 +86,7 @@
(t/deftest shared-key-auth (t/deftest shared-key-auth
(let [handler (#'app.http.middleware/wrap-shared-key-auth (let [handler (#'app.http.middleware/wrap-shared-key-auth
(fn [req] {::yres/status 200}) (fn [req] {::yres/status 200})
{:test1 "secret-key"})] "secret-key")]
(let [response (handler (->DummyRequest {} {}))] (let [response (handler (->DummyRequest {} {}))]
(t/is (= 403 (::yres/status response)))) (t/is (= 403 (::yres/status response))))
@@ -95,14 +95,11 @@
(t/is (= 403 (::yres/status response)))) (t/is (= 403 (::yres/status response))))
(let [response (handler (->DummyRequest {"x-shared-key" "secret-key"} {}))] (let [response (handler (->DummyRequest {"x-shared-key" "secret-key"} {}))]
(t/is (= 403 (::yres/status response))))
(let [response (handler (->DummyRequest {"x-shared-key" "test1 secret-key"} {}))]
(t/is (= 200 (::yres/status response)))))) (t/is (= 200 (::yres/status response))))))
(t/deftest access-token-authz (t/deftest access-token-authz
(let [profile (th/create-profile* 1) (let [profile (th/create-profile* 1)
token (db/tx-run! th/*system* app.rpc.commands.access-token/create-access-token (:id profile) "test" nil nil) token (db/tx-run! th/*system* app.rpc.commands.access-token/create-access-token (:id profile) "test" nil)
handler (#'app.http.access-token/wrap-authz identity th/*system*)] handler (#'app.http.access-token/wrap-authz identity th/*system*)]
(let [response (handler nil)] (let [response (handler nil)]

View File

@@ -107,18 +107,4 @@
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(let [results (:result out)] (let [results (:result out)]
(t/is (= 2 (count results)))))) (t/is (= 2 (count results))))))))
(t/testing "get mcp token"
(let [_ (th/command! {::th/type :create-access-token
::rpc/profile-id (:id prof)
:type "mcp"
:name "token 1"
:perms ["get-profile"]})
{:keys [error result]}
(th/command! {::th/type :get-current-mcp-token
::rpc/profile-id (:id prof)})]
;; (th/print-result! result)
(t/is (nil? error))
(t/is (string? (:token result)))))))

View File

@@ -17,6 +17,7 @@
[app.db.sql :as sql] [app.db.sql :as sql]
[app.http :as http] [app.http :as http]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.setup.clock :as clock]
[app.storage :as sto] [app.storage :as sto]
[backend-tests.helpers :as th] [backend-tests.helpers :as th]
[clojure.test :as t] [clojure.test :as t]
@@ -133,7 +134,7 @@
;; this will run pending task triggered by deleting user snapshot ;; this will run pending task triggered by deleting user snapshot
(th/run-pending-tasks!) (th/run-pending-tasks!)
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))] (binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
(let [res (th/run-task! :objects-gc {})] (let [res (th/run-task! :objects-gc {})]
;; delete 2 snapshots and 2 file data entries ;; delete 2 snapshots and 2 file data entries
(t/is (= 4 (:processed res))))))))) (t/is (= 4 (:processed res)))))))))

View File

@@ -19,6 +19,7 @@
[app.http :as http] [app.http :as http]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.rpc.commands.files :as files] [app.rpc.commands.files :as files]
[app.setup.clock :as clock]
[app.storage :as sto] [app.storage :as sto]
[backend-tests.helpers :as th] [backend-tests.helpers :as th]
[clojure.test :as t] [clojure.test :as t]
@@ -841,7 +842,7 @@
out (th/command! data) out (th/command! data)
error (:error out)] error (:error out)]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (th/ex-info? error)) (t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :not-found)))) (t/is (th/ex-of-type? error :not-found))))
@@ -863,7 +864,7 @@
out (th/command! data) out (th/command! data)
error (:error out)] error (:error out)]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (th/ex-info? error)) (t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :not-found)))) (t/is (th/ex-of-type? error :not-found))))
@@ -921,7 +922,7 @@
(t/is (= 0 (:processed result)))) (t/is (= 0 (:processed result))))
;; run permanent deletion ;; run permanent deletion
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))] (binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
(let [result (th/run-task! :objects-gc {})] (let [result (th/run-task! :objects-gc {})]
(t/is (= 3 (:processed result))))) (t/is (= 3 (:processed result)))))
@@ -1261,7 +1262,7 @@
(t/is (= 1 (count rows))) (t/is (= 1 (count rows)))
(t/is (every? #(some? (:data %)) rows))) (t/is (every? #(some? (:data %)) rows)))
;; Mark the file ellegible again for GC ;; Mark the file ellegible again for GC
(th/db-update! :file (th/db-update! :file
{:has-media-trimmed false} {:has-media-trimmed false}
{:id (:id file)}) {:id (:id file)})
@@ -1318,7 +1319,7 @@
{:file-id (:id file) {:file-id (:id file)
:type "fragment"} :type "fragment"}
{:order-by [:created-at]})] {:order-by [:created-at]})]
;; (pp/pprint rows) ;; (pp/pprint rows)
(t/is (= 2 (count rows))) (t/is (= 2 (count rows)))
(t/is (nil? (:data row1))) (t/is (nil? (:data row1)))
(t/is (= "storage" (:backend row1))) (t/is (= "storage" (:backend row1)))
@@ -1874,7 +1875,7 @@
file-id (uuid/next) file-id (uuid/next)
now (ct/inst "2025-10-31T00:00:00Z")] now (ct/inst "2025-10-31T00:00:00Z")]
(binding [ct/*clock* (ct/fixed-clock now)] (binding [ct/*clock* (clock/fixed now)]
(let [data {::th/type :create-file (let [data {::th/type :create-file
::rpc/profile-id (:id prof) ::rpc/profile-id (:id prof)
:project-id proj-id :project-id proj-id
@@ -1920,11 +1921,7 @@
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(let [result (:result out)] (let [result (:result out)]
(t/is (fn? result)) (t/is (= (:ids data) result)))
(let [[ev1 ev2 :as events] (th/consume-sse result)]
(t/is (= 2 (count events)))
(t/is (= (:ids data) (val ev2)))))
(let [row (th/db-exec-one! ["select * from file where id = ?" file-id])] (let [row (th/db-exec-one! ["select * from file where id = ?" file-id])]
(t/is (= (:deleted-at row) now))))))) (t/is (= (:deleted-at row) now)))))))
@@ -1936,7 +1933,7 @@
file-id (uuid/next) file-id (uuid/next)
now (ct/inst "2025-10-31T00:00:00Z")] now (ct/inst "2025-10-31T00:00:00Z")]
(binding [ct/*clock* (ct/fixed-clock now)] (binding [ct/*clock* (clock/fixed now)]
(let [data {::th/type :create-file (let [data {::th/type :create-file
::rpc/profile-id (:id prof) ::rpc/profile-id (:id prof)
:project-id proj-id :project-id proj-id
@@ -1999,7 +1996,7 @@
team-id (:default-team-id profile) team-id (:default-team-id profile)
now (ct/inst "2025-10-31T00:00:00Z")] now (ct/inst "2025-10-31T00:00:00Z")]
(binding [ct/*clock* (ct/fixed-clock now)] (binding [ct/*clock* (clock/fixed now)]
(let [project (th/create-project* 1 {:profile-id (:id profile) (let [project (th/create-project* 1 {:profile-id (:id profile)
:team-id team-id}) :team-id team-id})
file (th/create-file* 1 {:profile-id (:id profile) file (th/create-file* 1 {:profile-id (:id profile)

View File

@@ -85,7 +85,7 @@
(t/is (map? (:result out)))) (t/is (map? (:result out))))
;; run the task again ;; run the task again
(let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:minutes 31}))] (let [res (binding [ct/*clock* (clock/fixed (ct/in-future {:minutes 31}))]
(th/run-task! "storage-gc-touched" {}))] (th/run-task! "storage-gc-touched" {}))]
(t/is (= 2 (:freeze res)))) (t/is (= 2 (:freeze res))))
@@ -136,7 +136,7 @@
(t/is (some? (sto/get-object storage (:media-id row2)))) (t/is (some? (sto/get-object storage (:media-id row2))))
;; run the task again ;; run the task again
(let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:minutes 31}))] (let [res (binding [ct/*clock* (clock/fixed (ct/in-future {:minutes 31}))]
(th/run-task! :storage-gc-touched {}))] (th/run-task! :storage-gc-touched {}))]
(t/is (= 1 (:delete res))) (t/is (= 1 (:delete res)))
(t/is (= 0 (:freeze res)))) (t/is (= 0 (:freeze res))))
@@ -147,7 +147,7 @@
;; Run the storage gc deleted task, it should permanently delete ;; Run the storage gc deleted task, it should permanently delete
;; all storage objects related to the deleted thumbnails ;; all storage objects related to the deleted thumbnails
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))] (binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
(let [res (th/run-task! :storage-gc-deleted {})] (let [res (th/run-task! :storage-gc-deleted {})]
(t/is (= 1 (:deleted res))))) (t/is (= 1 (:deleted res)))))
@@ -247,7 +247,7 @@
;; Run the storage gc deleted task, it should permanently delete ;; Run the storage gc deleted task, it should permanently delete
;; all storage objects related to the deleted thumbnails ;; all storage objects related to the deleted thumbnails
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))] (binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
(let [result (th/run-task! :storage-gc-deleted {})] (let [result (th/run-task! :storage-gc-deleted {})]
(t/is (= 1 (:deleted result))))) (t/is (= 1 (:deleted result)))))

View File

@@ -12,6 +12,7 @@
[app.db :as db] [app.db :as db]
[app.http :as http] [app.http :as http]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.setup.clock :as clock]
[app.storage :as sto] [app.storage :as sto]
[backend-tests.helpers :as th] [backend-tests.helpers :as th]
[clojure.test :as t] [clojure.test :as t]
@@ -93,41 +94,6 @@
:font-weight :font-weight
:font-style)))) :font-style))))
(t/deftest woff2-font-upload-1
(let [prof (th/create-profile* 1 {:is-active true})
team-id (:default-team-id prof)
proj-id (:default-project-id prof)
font-id (uuid/custom 10 1)
data (-> (io/resource "backend_tests/test_files/font-1.woff2")
(io/read*))
params {::th/type :create-font-variant
::rpc/profile-id (:id prof)
:team-id team-id
:font-id font-id
:font-family "somefont"
:font-weight 400
:font-style "normal"
:data {"font/woff2" data}}
out (th/command! params)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (uuid? (:id result)))
(t/is (uuid? (:ttf-file-id result)))
(t/is (uuid? (:otf-file-id result)))
(t/is (uuid? (:woff1-file-id result)))
(t/is (uuid? (:woff2-file-id result)))
(t/are [k] (= (get params k)
(get result k))
:team-id
:font-id
:font-family
:font-weight
:font-style))))
(t/deftest font-deletion-1 (t/deftest font-deletion-1
(let [prof (th/create-profile* 1 {:is-active true}) (let [prof (th/create-profile* 1 {:is-active true})
team-id (:default-team-id prof) team-id (:default-team-id prof)
@@ -181,7 +147,7 @@
(t/is (= 0 (:freeze res))) (t/is (= 0 (:freeze res)))
(t/is (= 0 (:delete res)))) (t/is (= 0 (:delete res))))
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))] (binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
(let [res (th/run-task! :objects-gc {})] (let [res (th/run-task! :objects-gc {})]
(t/is (= 2 (:processed res)))) (t/is (= 2 (:processed res))))
@@ -242,7 +208,7 @@
(t/is (= 0 (:freeze res))) (t/is (= 0 (:freeze res)))
(t/is (= 0 (:delete res)))) (t/is (= 0 (:delete res))))
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))] (binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
(let [res (th/run-task! :objects-gc {})] (let [res (th/run-task! :objects-gc {})]
(t/is (= 1 (:processed res)))) (t/is (= 1 (:processed res))))
@@ -302,37 +268,10 @@
(t/is (= 0 (:freeze res))) (t/is (= 0 (:freeze res)))
(t/is (= 0 (:delete res)))) (t/is (= 0 (:delete res))))
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))] (binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
(let [res (th/run-task! :objects-gc {})] (let [res (th/run-task! :objects-gc {})]
(t/is (= 1 (:processed res)))) (t/is (= 1 (:processed res))))
(let [res (th/run-task! :storage-gc-touched {})] (let [res (th/run-task! :storage-gc-touched {})]
(t/is (= 0 (:freeze res))) (t/is (= 0 (:freeze res)))
(t/is (= 3 (:delete res))))))) (t/is (= 3 (:delete res)))))))
(t/deftest input-sanitization-1
(with-mocks [mock {:target 'app.rpc.quotes/check! :return nil}]
(let [prof (th/create-profile* 1 {:is-active true})
team-id (:default-team-id prof)
proj-id (:default-project-id prof)
font-id (uuid/custom 10 1)
ttfdata (-> (io/resource "backend_tests/test_files/font-1.ttf")
(io/read*))
params {::th/type :create-font-variant
::rpc/profile-id (:id prof)
:team-id team-id
:font-id font-id
:font-family "somefont"
:font-weight 400
:font-style "normal"
:data {"font/ttf" "/etc/passwd"}}
out (th/command! params)]
(t/is (= 0 (:call-count @mock)))
;; (th/print-result! out)
(let [error (:error out)
error-data (ex-data error)]
(t/is (th/ex-info? error))))))

View File

@@ -536,7 +536,7 @@
:token rtoken} :token rtoken}
{:keys [result error] :as out} (th/command! data)] {:keys [result error] :as out} (th/command! data)]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? error)) (t/is (nil? error))
(t/is (map? result)) (t/is (map? result))
(t/is (string? (:invitation-token result)))))) (t/is (string? (:invitation-token result))))))

View File

@@ -12,6 +12,7 @@
[app.db :as db] [app.db :as db]
[app.http :as http] [app.http :as http]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.setup.clock :as clock]
[backend-tests.helpers :as th] [backend-tests.helpers :as th]
[clojure.test :as t])) [clojure.test :as t]))
@@ -30,7 +31,7 @@
:team-id (:id team) :team-id (:id team)
:name "test project"} :name "test project"}
out (th/command! data)] out (th/command! data)]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(let [result (:result out)] (let [result (:result out)]
@@ -93,7 +94,7 @@
:id project-id} :id project-id}
out (th/command! data)] out (th/command! data)]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(t/is (nil? (:result out)))) (t/is (nil? (:result out))))
@@ -227,7 +228,7 @@
(t/is (= 0 (count result))))) (t/is (= 0 (count result)))))
;; run permanent deletion ;; run permanent deletion
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))] (binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
(let [result (th/run-task! :objects-gc {})] (let [result (th/run-task! :objects-gc {})]
(t/is (= 1 (:processed result))))) (t/is (= 1 (:processed result)))))

View File

@@ -13,6 +13,7 @@
[app.db :as db] [app.db :as db]
[app.http :as http] [app.http :as http]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
[app.setup.clock :as clock]
[app.storage :as sto] [app.storage :as sto]
[app.tokens :as tokens] [app.tokens :as tokens]
[backend-tests.helpers :as th] [backend-tests.helpers :as th]
@@ -525,7 +526,7 @@
(t/is (= :not-found (:type edata))))) (t/is (= :not-found (:type edata)))))
;; run permanent deletion ;; run permanent deletion
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))] (binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
(let [result (th/run-task! :objects-gc {})] (let [result (th/run-task! :objects-gc {})]
(t/is (= 2 (:processed result))))) (t/is (= 2 (:processed result)))))
@@ -582,7 +583,7 @@
(t/is (= 1 (count rows))) (t/is (= 1 (count rows)))
(t/is (ct/inst? (:deleted-at (first rows))))) (t/is (ct/inst? (:deleted-at (first rows)))))
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))] (binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
(let [result (th/run-task! :objects-gc {})] (let [result (th/run-task! :objects-gc {})]
(t/is (= 7 (:processed result))))))) (t/is (= 7 (:processed result)))))))

Some files were not shown because too many files have changed in this diff Show More