mirror of
https://github.com/penpot/penpot.git
synced 2026-04-26 03:38:18 +00:00
Compare commits
No commits in common. "develop" and "2.12.0" have entirely different histories.
305
.circleci/config.yml
Normal file
305
.circleci/config.yml
Normal 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
|
||||||
@ -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}
|
||||||
|
|
||||||
|
|||||||
@ -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]]
|
||||||
|
|||||||
38
.github/ISSUE_TEMPLATE/new-render-bug-report.md
vendored
38
.github/ISSUE_TEMPLATE/new-render-bug-report.md
vendored
@ -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.
|
|
||||||
4
.github/workflows/build-bundle.yml
vendored
4
.github/workflows/build-bundle.yml
vendored
@ -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 }}
|
||||||
@ -48,7 +48,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
ref: ${{ inputs.gh_ref }}
|
ref: ${{ inputs.gh_ref }}
|
||||||
|
|||||||
1
.github/workflows/build-develop.yml
vendored
1
.github/workflows/build-develop.yml
vendored
@ -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'
|
||||||
|
|
||||||
|
|||||||
15
.github/workflows/build-docker-devenv.yml
vendored
15
.github/workflows/build-docker-devenv.yml
vendored
@ -7,28 +7,23 @@ 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@v6
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to Docker Registry
|
- name: Login to Docker Registry
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.PUB_DOCKER_USERNAME }}
|
username: ${{ secrets.PUB_DOCKER_USERNAME }}
|
||||||
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
|
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
|
||||||
|
|
||||||
- name: Build and push DevEnv Docker image
|
- name: Build and push DevEnv Docker image
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v6
|
||||||
env:
|
env:
|
||||||
DOCKER_IMAGE: 'penpotapp/devenv'
|
DOCKER_IMAGE: 'penpotapp/devenv'
|
||||||
with:
|
with:
|
||||||
|
|||||||
49
.github/workflows/build-docker.yml
vendored
49
.github/workflows/build-docker.yml
vendored
@ -19,16 +19,11 @@ 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@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
ref: ${{ inputs.gh_ref }}
|
ref: ${{ inputs.gh_ref }}
|
||||||
@ -59,43 +54,32 @@ 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
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to Docker Registry
|
- name: Login to Docker Registry
|
||||||
uses: docker/login-action@v4
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ secrets.DOCKER_REGISTRY }}
|
registry: ${{ secrets.DOCKER_REGISTRY }}
|
||||||
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@v4
|
|
||||||
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@v6
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images:
|
images:
|
||||||
frontend
|
frontend
|
||||||
backend
|
backend
|
||||||
exporter
|
exporter
|
||||||
storybook
|
storybook
|
||||||
mcp
|
|
||||||
labels: |
|
labels: |
|
||||||
bundle_version=${{ steps.bundles.outputs.bundle_version }}
|
bundle_version=${{ steps.bundles.outputs.bundle_version }}
|
||||||
|
|
||||||
- name: Build and push Backend Docker image
|
- name: Build and push Backend Docker image
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v6
|
||||||
env:
|
env:
|
||||||
DOCKER_IMAGE: 'backend'
|
DOCKER_IMAGE: 'backend'
|
||||||
BUNDLE_PATH: './bundle-backend'
|
BUNDLE_PATH: './bundle-backend'
|
||||||
@ -110,7 +94,7 @@ jobs:
|
|||||||
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 Frontend Docker image
|
- name: Build and push Frontend Docker image
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v6
|
||||||
env:
|
env:
|
||||||
DOCKER_IMAGE: 'frontend'
|
DOCKER_IMAGE: 'frontend'
|
||||||
BUNDLE_PATH: './bundle-frontend'
|
BUNDLE_PATH: './bundle-frontend'
|
||||||
@ -125,7 +109,7 @@ jobs:
|
|||||||
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 Exporter Docker image
|
- name: Build and push Exporter Docker image
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v6
|
||||||
env:
|
env:
|
||||||
DOCKER_IMAGE: 'exporter'
|
DOCKER_IMAGE: 'exporter'
|
||||||
BUNDLE_PATH: './bundle-exporter'
|
BUNDLE_PATH: './bundle-exporter'
|
||||||
@ -140,7 +124,7 @@ jobs:
|
|||||||
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 Storybook Docker image
|
- name: Build and push Storybook Docker image
|
||||||
uses: docker/build-push-action@v7
|
uses: docker/build-push-action@v6
|
||||||
env:
|
env:
|
||||||
DOCKER_IMAGE: 'storybook'
|
DOCKER_IMAGE: 'storybook'
|
||||||
BUNDLE_PATH: './bundle-storybook'
|
BUNDLE_PATH: './bundle-storybook'
|
||||||
@ -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@v7
|
|
||||||
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
|
||||||
|
|||||||
22
.github/workflows/build-main-staging.yml
vendored
22
.github/workflows/build-main-staging.yml
vendored
@ -1,22 +0,0 @@
|
|||||||
name: _MAIN-STAGING
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
schedule:
|
|
||||||
- cron: '26 5-20 * * 1-5'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-bundle:
|
|
||||||
uses: ./.github/workflows/build-bundle.yml
|
|
||||||
secrets: inherit
|
|
||||||
with:
|
|
||||||
gh_ref: "main-staging"
|
|
||||||
build_wasm: "yes"
|
|
||||||
build_storybook: "yes"
|
|
||||||
|
|
||||||
build-docker:
|
|
||||||
needs: build-bundle
|
|
||||||
uses: ./.github/workflows/build-docker.yml
|
|
||||||
secrets: inherit
|
|
||||||
with:
|
|
||||||
gh_ref: "main-staging"
|
|
||||||
1
.github/workflows/build-staging.yml
vendored
1
.github/workflows/build-staging.yml
vendored
@ -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'
|
||||||
|
|
||||||
|
|||||||
3
.github/workflows/build-tag.yml
vendored
3
.github/workflows/build-tag.yml
vendored
@ -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
|
||||||
|
|
||||||
|
|||||||
5
.github/workflows/commit-checker.yml
vendored
5
.github/workflows/commit-checker.yml
vendored
@ -6,14 +6,12 @@ on:
|
|||||||
- edited
|
- edited
|
||||||
- reopened
|
- reopened
|
||||||
- synchronize
|
- synchronize
|
||||||
- ready_for_review
|
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
types:
|
types:
|
||||||
- opened
|
- opened
|
||||||
- edited
|
- edited
|
||||||
- reopened
|
- reopened
|
||||||
- synchronize
|
- synchronize
|
||||||
- ready_for_review
|
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
@ -22,14 +20,13 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-commit-message:
|
check-commit-message:
|
||||||
if: ${{ !github.event.pull_request.draft }}
|
|
||||||
name: Check Commit Message
|
name: Check Commit Message
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- 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
|
||||||
|
|||||||
142
.github/workflows/plugins-deploy-api-doc.yml
vendored
142
.github/workflows/plugins-deploy-api-doc.yml
vendored
@ -1,142 +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@v6
|
|
||||||
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@v5
|
|
||||||
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: Add noindex header and robots.txt files for non-production environments
|
|
||||||
if: ${{ steps.vars.outputs.gh_ref != 'main' }}
|
|
||||||
working-directory: plugins
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
ASSETS_DIR="dist/doc"
|
|
||||||
|
|
||||||
cat > "${ASSETS_DIR}/_headers" << 'EOF'
|
|
||||||
/*
|
|
||||||
X-Robots-Tag: noindex, nofollow
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > "${ASSETS_DIR}/robots.txt" << 'EOF'
|
|
||||||
User-agent: *
|
|
||||||
Disallow: /
|
|
||||||
EOF
|
|
||||||
|
|
||||||
- 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
|
|
||||||
127
.github/workflows/plugins-deploy-package.yml
vendored
127
.github/workflows/plugins-deploy-package.yml
vendored
@ -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@v6
|
|
||||||
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@v5
|
|
||||||
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
|
|
||||||
143
.github/workflows/plugins-deploy-packages.yml
vendored
143
.github/workflows/plugins-deploy-packages.yml
vendored
@ -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@v6
|
|
||||||
- id: filter
|
|
||||||
uses: dorny/paths-filter@v4
|
|
||||||
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
|
|
||||||
140
.github/workflows/plugins-deploy-styles-doc.yml
vendored
140
.github/workflows/plugins-deploy-styles-doc.yml
vendored
@ -1,140 +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@v6
|
|
||||||
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@v5
|
|
||||||
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: Add noindex header and robots.txt files for non-production environments
|
|
||||||
if: ${{ steps.vars.outputs.gh_ref != 'main' }}
|
|
||||||
working-directory: plugins
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
ASSETS_DIR="dist/apps/example-styles"
|
|
||||||
|
|
||||||
cat > "${ASSETS_DIR}/_headers" << 'EOF'
|
|
||||||
/*
|
|
||||||
X-Robots-Tag: noindex, nofollow
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat > "${ASSETS_DIR}/robots.txt" << 'EOF'
|
|
||||||
User-agent: *
|
|
||||||
Disallow: /
|
|
||||||
EOF
|
|
||||||
|
|
||||||
- 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
|
|
||||||
7
.github/workflows/release.yml
vendored
7
.github/workflows/release.yml
vendored
@ -31,7 +31,7 @@ jobs:
|
|||||||
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
|
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
ref: ${{ steps.vars.outputs.gh_ref }}
|
ref: ${{ steps.vars.outputs.gh_ref }}
|
||||||
@ -64,14 +64,13 @@ jobs:
|
|||||||
echo "$PUB_DOCKER_PASSWORD" | skopeo login --username "$PUB_DOCKER_USERNAME" --password-stdin docker.io
|
echo "$PUB_DOCKER_PASSWORD" | skopeo login --username "$PUB_DOCKER_USERNAME" --password-stdin docker.io
|
||||||
|
|
||||||
IMAGES=("frontend" "backend" "exporter" "storybook")
|
IMAGES=("frontend" "backend" "exporter" "storybook")
|
||||||
SHORT_TAG=${TAG%.*}
|
|
||||||
|
|
||||||
for image in "${IMAGES[@]}"; do
|
for image in "${IMAGES[@]}"; do
|
||||||
skopeo copy --all \
|
skopeo copy --all \
|
||||||
docker://$DOCKER_REGISTRY/$image:$TAG \
|
docker://$DOCKER_REGISTRY/$image:$TAG \
|
||||||
docker://docker.io/penpotapp/$image:$TAG
|
docker://docker.io/penpotapp/$image:$TAG
|
||||||
|
|
||||||
for alias in main latest "$SHORT_TAG"; do
|
for alias in main latest; do
|
||||||
skopeo copy --all \
|
skopeo copy --all \
|
||||||
docker://$DOCKER_REGISTRY/$image:$TAG \
|
docker://$DOCKER_REGISTRY/$image:$TAG \
|
||||||
docker://docker.io/penpotapp/$image:$alias
|
docker://docker.io/penpotapp/$image:$alias
|
||||||
@ -94,7 +93,7 @@ jobs:
|
|||||||
|
|
||||||
# --- Create GitHub release ---
|
# --- Create GitHub release ---
|
||||||
- name: Create GitHub release
|
- name: Create GitHub release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
|
|||||||
47
.github/workflows/tests-mcp.yml
vendored
47
.github/workflows/tests-mcp.yml
vendored
@ -1,47 +0,0 @@
|
|||||||
name: "MCP CI"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
- staging
|
|
||||||
- main
|
|
||||||
|
|
||||||
types:
|
|
||||||
- opened
|
|
||||||
- synchronize
|
|
||||||
- ready_for_review
|
|
||||||
|
|
||||||
paths:
|
|
||||||
- 'mcp/**'
|
|
||||||
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
- staging
|
|
||||||
- main
|
|
||||||
|
|
||||||
paths:
|
|
||||||
- 'mcp/**'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test-mcp:
|
|
||||||
if: ${{ !github.event.pull_request.draft }}
|
|
||||||
name: "Test MCP"
|
|
||||||
runs-on: penpot-runner-02
|
|
||||||
container: penpotapp/devenv:latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- 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;
|
|
||||||
229
.github/workflows/tests.yml
vendored
229
.github/workflows/tests.yml
vendored
@ -9,7 +9,6 @@ on:
|
|||||||
types:
|
types:
|
||||||
- opened
|
- opened
|
||||||
- synchronize
|
- synchronize
|
||||||
- ready_for_review
|
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- develop
|
- develop
|
||||||
@ -21,140 +20,45 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
if: ${{ !github.event.pull_request.draft }}
|
|
||||||
name: "Linter"
|
name: "Linter"
|
||||||
runs-on: penpot-runner-02
|
runs-on: ubuntu-24.04
|
||||||
container: penpotapp/devenv:latest
|
container: penpotapp/devenv:latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Lint Common
|
- name: Check clojure code format
|
||||||
working-directory: ./common
|
|
||||||
run: |
|
run: |
|
||||||
corepack enable;
|
./scripts/lint
|
||||||
corepack install;
|
|
||||||
pnpm install;
|
|
||||||
pnpm run check-fmt:clj
|
|
||||||
pnpm run check-fmt:js
|
|
||||||
pnpm run lint:clj
|
|
||||||
|
|
||||||
- name: Lint Frontend
|
|
||||||
working-directory: ./frontend
|
|
||||||
run: |
|
|
||||||
corepack enable;
|
|
||||||
corepack install;
|
|
||||||
pnpm install;
|
|
||||||
pnpm run check-fmt:js
|
|
||||||
pnpm run check-fmt:clj
|
|
||||||
pnpm run check-fmt:scss
|
|
||||||
pnpm run lint:clj
|
|
||||||
pnpm run lint:js
|
|
||||||
pnpm run lint:scss
|
|
||||||
|
|
||||||
- name: Lint Backend
|
|
||||||
working-directory: ./backend
|
|
||||||
run: |
|
|
||||||
corepack enable;
|
|
||||||
corepack install;
|
|
||||||
pnpm install;
|
|
||||||
pnpm run check-fmt
|
|
||||||
pnpm run lint
|
|
||||||
|
|
||||||
- name: Lint Exporter
|
|
||||||
working-directory: ./exporter
|
|
||||||
run: |
|
|
||||||
corepack enable;
|
|
||||||
corepack install;
|
|
||||||
pnpm install;
|
|
||||||
pnpm run check-fmt
|
|
||||||
pnpm run lint
|
|
||||||
|
|
||||||
- name: Lint Library
|
|
||||||
working-directory: ./library
|
|
||||||
run: |
|
|
||||||
corepack enable;
|
|
||||||
corepack install;
|
|
||||||
pnpm install;
|
|
||||||
pnpm run check-fmt
|
|
||||||
pnpm run lint
|
|
||||||
|
|
||||||
test-common:
|
test-common:
|
||||||
if: ${{ !github.event.pull_request.draft }}
|
|
||||||
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:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests on JVM
|
||||||
|
working-directory: ./common
|
||||||
|
run: |
|
||||||
|
clojure -M:dev:test
|
||||||
|
|
||||||
|
- name: Run tests on NODE
|
||||||
working-directory: ./common
|
working-directory: ./common
|
||||||
run: |
|
run: |
|
||||||
./scripts/test
|
./scripts/test
|
||||||
|
|
||||||
test-plugins:
|
|
||||||
if: ${{ !github.event.pull_request.draft }}
|
|
||||||
name: Plugins Runtime Linter & Tests
|
|
||||||
runs-on: penpot-runner-02
|
|
||||||
container: penpotapp/devenv:latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- 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:
|
||||||
if: ${{ !github.event.pull_request.draft }}
|
|
||||||
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:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Unit Tests
|
- name: Unit Tests
|
||||||
working-directory: ./frontend
|
working-directory: ./frontend
|
||||||
@ -163,20 +67,17 @@ 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:
|
||||||
if: ${{ !github.event.pull_request.draft }}
|
|
||||||
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:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Format
|
- name: Format
|
||||||
working-directory: ./render-wasm
|
working-directory: ./render-wasm
|
||||||
@ -194,9 +95,8 @@ jobs:
|
|||||||
./test
|
./test
|
||||||
|
|
||||||
test-backend:
|
test-backend:
|
||||||
if: ${{ !github.event.pull_request.draft }}
|
|
||||||
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:
|
||||||
@ -220,7 +120,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
working-directory: ./backend
|
working-directory: ./backend
|
||||||
@ -234,14 +134,13 @@ jobs:
|
|||||||
clojure -M:dev:test --reporter kaocha.report/documentation
|
clojure -M:dev:test --reporter kaocha.report/documentation
|
||||||
|
|
||||||
test-library:
|
test-library:
|
||||||
if: ${{ !github.event.pull_request.draft }}
|
|
||||||
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:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
working-directory: ./library
|
working-directory: ./library
|
||||||
@ -249,39 +148,47 @@ jobs:
|
|||||||
./scripts/test
|
./scripts/test
|
||||||
|
|
||||||
build-integration:
|
build-integration:
|
||||||
if: ${{ !github.event.pull_request.draft }}
|
|
||||||
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:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Build Bundle
|
- name: Build Bundle
|
||||||
working-directory: ./frontend
|
working-directory: ./frontend
|
||||||
run: |
|
run: |
|
||||||
./scripts/build
|
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@v5
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
key: "integration-bundle-${{ github.sha }}"
|
key: "integration-bundle-${{ github.sha }}"
|
||||||
path: frontend/resources/public
|
path: frontend/resources/public
|
||||||
|
|
||||||
test-integration-1:
|
test-integration-1:
|
||||||
if: ${{ !github.event.pull_request.draft }}
|
name: "Integration Tests 1/4"
|
||||||
name: "Integration Tests 1/3"
|
runs-on: ubuntu-24.04
|
||||||
runs-on: penpot-runner-02
|
|
||||||
container: penpotapp/devenv:latest
|
container: penpotapp/devenv:latest
|
||||||
needs: build-integration
|
needs: build-integration
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Restore Cache
|
- name: Restore Cache
|
||||||
uses: actions/cache/restore@v5
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
key: "integration-bundle-${{ github.sha }}"
|
key: "integration-bundle-${{ github.sha }}"
|
||||||
path: frontend/resources/public
|
path: frontend/resources/public
|
||||||
@ -289,10 +196,10 @@ jobs:
|
|||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
working-directory: ./frontend
|
working-directory: ./frontend
|
||||||
run: |
|
run: |
|
||||||
./scripts/test-e2e --shard="1/3";
|
./scripts/test-e2e --shard="1/4";
|
||||||
|
|
||||||
- name: Upload test result
|
- name: Upload test result
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v4
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: integration-tests-result-1
|
name: integration-tests-result-1
|
||||||
@ -301,18 +208,17 @@ jobs:
|
|||||||
retention-days: 3
|
retention-days: 3
|
||||||
|
|
||||||
test-integration-2:
|
test-integration-2:
|
||||||
if: ${{ !github.event.pull_request.draft }}
|
name: "Integration Tests 2/4"
|
||||||
name: "Integration Tests 2/3"
|
runs-on: ubuntu-24.04
|
||||||
runs-on: penpot-runner-02
|
|
||||||
container: penpotapp/devenv:latest
|
container: penpotapp/devenv:latest
|
||||||
needs: build-integration
|
needs: build-integration
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Restore Cache
|
- name: Restore Cache
|
||||||
uses: actions/cache/restore@v5
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
key: "integration-bundle-${{ github.sha }}"
|
key: "integration-bundle-${{ github.sha }}"
|
||||||
path: frontend/resources/public
|
path: frontend/resources/public
|
||||||
@ -320,10 +226,10 @@ jobs:
|
|||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
working-directory: ./frontend
|
working-directory: ./frontend
|
||||||
run: |
|
run: |
|
||||||
./scripts/test-e2e --shard="2/3";
|
./scripts/test-e2e --shard="2/4";
|
||||||
|
|
||||||
- name: Upload test result
|
- name: Upload test result
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v4
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: integration-tests-result-2
|
name: integration-tests-result-2
|
||||||
@ -332,18 +238,17 @@ jobs:
|
|||||||
retention-days: 3
|
retention-days: 3
|
||||||
|
|
||||||
test-integration-3:
|
test-integration-3:
|
||||||
if: ${{ !github.event.pull_request.draft }}
|
name: "Integration Tests 3/4"
|
||||||
name: "Integration Tests 3/3"
|
runs-on: ubuntu-24.04
|
||||||
runs-on: penpot-runner-02
|
|
||||||
container: penpotapp/devenv:latest
|
container: penpotapp/devenv:latest
|
||||||
needs: build-integration
|
needs: build-integration
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Restore Cache
|
- name: Restore Cache
|
||||||
uses: actions/cache/restore@v5
|
uses: actions/cache/restore@v4
|
||||||
with:
|
with:
|
||||||
key: "integration-bundle-${{ github.sha }}"
|
key: "integration-bundle-${{ github.sha }}"
|
||||||
path: frontend/resources/public
|
path: frontend/resources/public
|
||||||
@ -351,13 +256,43 @@ jobs:
|
|||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
working-directory: ./frontend
|
working-directory: ./frontend
|
||||||
run: |
|
run: |
|
||||||
./scripts/test-e2e --shard="3/3";
|
./scripts/test-e2e --shard="3/4";
|
||||||
|
|
||||||
- name: Upload test result
|
- name: Upload test result
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v4
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: integration-tests-result-3
|
name: integration-tests-result-3
|
||||||
path: frontend/test-results/
|
path: frontend/test-results/
|
||||||
overwrite: true
|
overwrite: true
|
||||||
retention-days: 3
|
retention-days: 3
|
||||||
|
|
||||||
|
test-integration-4:
|
||||||
|
name: "Integration Tests 4/4"
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
container: penpotapp/devenv:latest
|
||||||
|
needs: build-integration
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Restore Cache
|
||||||
|
uses: actions/cache/restore@v4
|
||||||
|
with:
|
||||||
|
key: "integration-bundle-${{ github.sha }}"
|
||||||
|
path: frontend/resources/public
|
||||||
|
|
||||||
|
- name: Run Tests
|
||||||
|
working-directory: ./frontend
|
||||||
|
run: |
|
||||||
|
./scripts/test-e2e --shard="4/4";
|
||||||
|
|
||||||
|
- name: Upload test result
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: integration-tests-result-4
|
||||||
|
path: frontend/test-results/
|
||||||
|
overwrite: true
|
||||||
|
retention-days: 3
|
||||||
|
|||||||
28
.gitignore
vendored
28
.gitignore
vendored
@ -1,4 +1,10 @@
|
|||||||
.pnp.*
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/sdks
|
||||||
|
!.yarn/versions
|
||||||
*-init.clj
|
*-init.clj
|
||||||
*.css.json
|
*.css.json
|
||||||
*.jar
|
*.jar
|
||||||
@ -13,6 +19,7 @@
|
|||||||
.nyc_output
|
.nyc_output
|
||||||
.rebel_readline_history
|
.rebel_readline_history
|
||||||
.repl
|
.repl
|
||||||
|
.shadow-cljs
|
||||||
/*.jpg
|
/*.jpg
|
||||||
/*.md
|
/*.md
|
||||||
/*.png
|
/*.png
|
||||||
@ -26,8 +33,6 @@
|
|||||||
/notes
|
/notes
|
||||||
/playground/
|
/playground/
|
||||||
/backend/*.md
|
/backend/*.md
|
||||||
!/backend/AGENTS.md
|
|
||||||
/backend/.shadow-cljs
|
|
||||||
/backend/*.sql
|
/backend/*.sql
|
||||||
/backend/*.txt
|
/backend/*.txt
|
||||||
/backend/assets/
|
/backend/assets/
|
||||||
@ -38,34 +43,27 @@
|
|||||||
/backend/resources/public/media
|
/backend/resources/public/media
|
||||||
/backend/target/
|
/backend/target/
|
||||||
/backend/experiments
|
/backend/experiments
|
||||||
/backend/scripts/_env.local
|
|
||||||
/bundle*
|
/bundle*
|
||||||
|
/cd.md
|
||||||
/clj-profiler/
|
/clj-profiler/
|
||||||
/common/coverage
|
/common/coverage
|
||||||
/common/target
|
/common/target
|
||||||
/common/.shadow-cljs
|
/deploy
|
||||||
/docker/images/bundle*
|
/docker/images/bundle*
|
||||||
/exporter/target
|
/exporter/target
|
||||||
/exporter/.shadow-cljs
|
|
||||||
/frontend/.storybook/preview-body.html
|
/frontend/.storybook/preview-body.html
|
||||||
/frontend/.storybook/preview-head.html
|
/frontend/.storybook/preview-head.html
|
||||||
/frontend/playwright-report/
|
|
||||||
/frontend/playwright/ui/visual-specs/
|
|
||||||
/frontend/text-editor/src/wasm/
|
|
||||||
/frontend/dist/
|
/frontend/dist/
|
||||||
/frontend/npm-debug.log
|
/frontend/npm-debug.log
|
||||||
/frontend/out/
|
/frontend/out/
|
||||||
/frontend/package-lock.json
|
/frontend/package-lock.json
|
||||||
/frontend/resources/fonts/experiments
|
/frontend/resources/fonts/experiments
|
||||||
/frontend/resources/public/*
|
/frontend/resources/public/*
|
||||||
/frontend/src/app/render_wasm/api/shared.js
|
|
||||||
/frontend/storybook-static/
|
/frontend/storybook-static/
|
||||||
/frontend/target/
|
/frontend/target/
|
||||||
/frontend/test-results/
|
|
||||||
/frontend/.shadow-cljs
|
|
||||||
/other/
|
/other/
|
||||||
/scripts/
|
/scripts/
|
||||||
/nexus/
|
/telemetry/
|
||||||
/tmp/
|
/tmp/
|
||||||
/vendor/**/target
|
/vendor/**/target
|
||||||
/vendor/svgclean/bundle*.js
|
/vendor/svgclean/bundle*.js
|
||||||
@ -73,13 +71,13 @@
|
|||||||
/library/target/
|
/library/target/
|
||||||
/library/*.zip
|
/library/*.zip
|
||||||
/external
|
/external
|
||||||
/penpot-nitrate
|
|
||||||
|
clj-profiler/
|
||||||
|
node_modules
|
||||||
/test-results/
|
/test-results/
|
||||||
/playwright-report/
|
/playwright-report/
|
||||||
/blob-report/
|
/blob-report/
|
||||||
/playwright/.cache/
|
/playwright/.cache/
|
||||||
/render-wasm/target/
|
/render-wasm/target/
|
||||||
/**/node_modules
|
|
||||||
/**/.yarn/*
|
/**/.yarn/*
|
||||||
/.pnpm-store
|
/.pnpm-store
|
||||||
/.vscode
|
|
||||||
|
|||||||
105
.gitpod.yml
Normal file
105
.gitpod.yml
Normal 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
|
||||||
@ -1,27 +0,0 @@
|
|||||||
---
|
|
||||||
name: commiter
|
|
||||||
description: Git commit assistant following CONTRIBUTING.md commit rules
|
|
||||||
mode: primary
|
|
||||||
---
|
|
||||||
|
|
||||||
Role: You are responsible for creating git commits for Penpot and must follow
|
|
||||||
the repository commit-format rules exactly.
|
|
||||||
|
|
||||||
Requirements:
|
|
||||||
|
|
||||||
* Read `CONTRIBUTING.md` before creating any commit and follow the
|
|
||||||
commit guidelines strictly.
|
|
||||||
* Use commit messages in the form `:emoji: <imperative subject>`.
|
|
||||||
* Keep the subject capitalized, concise, 70 characters or fewer, and
|
|
||||||
without a trailing period.
|
|
||||||
* Keep the description (commit body) with maximum line length of 80
|
|
||||||
characters. Use manual line breaks to wrap text before it exceeds
|
|
||||||
this limit.
|
|
||||||
* Separate the subject from the body with a blank line.
|
|
||||||
* Write a clear and concise body when needed.
|
|
||||||
* Use `git commit -s` so the commit includes the required
|
|
||||||
`Signed-off-by` line.
|
|
||||||
* Do not guess or hallucinate git author information (Name or
|
|
||||||
Email). Never include the `--author` flag in git commands unless
|
|
||||||
specifically instructed by the user for a unique case; assume the
|
|
||||||
local environment is already configured.
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
---
|
|
||||||
name: engineer
|
|
||||||
description: Senior Full-Stack Software Engineer
|
|
||||||
mode: primary
|
|
||||||
---
|
|
||||||
|
|
||||||
Role: You are a high-autonomy Senior Full-Stack Software Engineer working on
|
|
||||||
Penpot, an open-source design tool. You have full permission to navigate the
|
|
||||||
codebase, modify files, and execute commands to fulfill your tasks. Your goal is
|
|
||||||
to solve complex technical tasks with high precision while maintaining a strong
|
|
||||||
focus on maintainability and performance.
|
|
||||||
|
|
||||||
Tech stack: Clojure (backend), ClojureScript (frontend/exporter), Rust/WASM
|
|
||||||
(render-wasm), TypeScript (plugins/mcp), SCSS.
|
|
||||||
|
|
||||||
Requirements:
|
|
||||||
|
|
||||||
* Read the root `AGENTS.md` to understand the repository and application
|
|
||||||
architecture. Then read the `AGENTS.md` **only** for each affected module.
|
|
||||||
Not all modules have one — verify before reading.
|
|
||||||
* Before writing code, analyze the task in depth and describe your plan. If the
|
|
||||||
task is complex, break it down into atomic steps.
|
|
||||||
* When searching code, prefer `ripgrep` (`rg`) over `grep` — it respects
|
|
||||||
`.gitignore` by default.
|
|
||||||
* Do **not** touch unrelated modules unless the task explicitly requires it.
|
|
||||||
* Only reference functions, namespaces, or APIs that actually exist in the
|
|
||||||
codebase. Verify their existence before citing them. If unsure, search first.
|
|
||||||
* Be concise and autonomous — avoid unnecessary explanations.
|
|
||||||
* After making changes, run the applicable lint and format checks for the
|
|
||||||
affected module before considering the work done (see module `AGENTS.md` for
|
|
||||||
exact commands).
|
|
||||||
* Make small and logical commits following the commit guideline described in
|
|
||||||
`CONTRIBUTING.md`. Commit only when explicitly asked.
|
|
||||||
- Do not guess or hallucinate git author information (Name or Email). Never include the
|
|
||||||
`--author` flag in git commands unless specifically instructed by the user for a unique
|
|
||||||
case; assume the local environment is already configured. Allow git commit to
|
|
||||||
automatically pull the identity from the local git config `user.name` and `user.email`.
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
---
|
|
||||||
name: testing
|
|
||||||
description: Senior Software Engineer specialized on testing
|
|
||||||
mode: primary
|
|
||||||
---
|
|
||||||
|
|
||||||
Role: You are a Senior Software Engineer specialized in testing Clojure and
|
|
||||||
ClojureScript codebases. You work on Penpot, an open-source design tool.
|
|
||||||
|
|
||||||
Tech stack: Clojure (backend/JVM), ClojureScript (frontend/Node.js), shared
|
|
||||||
Cljc (common module), Rust (render-wasm).
|
|
||||||
|
|
||||||
Requirements:
|
|
||||||
|
|
||||||
* Read the root `AGENTS.md` to understand the repository and application
|
|
||||||
architecture. Then read the `AGENTS.md` **only** for each affected module. Not all
|
|
||||||
modules have one — verify before reading.
|
|
||||||
* Before writing code, describe your plan. If the task is complex, break it down into
|
|
||||||
atomic steps.
|
|
||||||
* Tests should be exhaustive and include edge cases relevant to Penpot's domain:
|
|
||||||
nil/missing fields, empty collections, invalid UUIDs, boundary geometries, Malli schema
|
|
||||||
violations, concurrent state mutations, and timeouts.
|
|
||||||
* Tests must be deterministic — do not use `setTimeout`, real network calls, or rely on
|
|
||||||
execution order. Use synchronous mocks for asynchronous workflows.
|
|
||||||
* Use `with-redefs` or equivalent mocking utilities to isolate the logic under test. Avoid
|
|
||||||
testing through the UI (DOM); e2e tests cover that.
|
|
||||||
* Only reference functions, namespaces, or test utilities that actually exist in the
|
|
||||||
codebase. Verify their existence before citing them.
|
|
||||||
* After adding or modifying tests, run the applicable lint and format checks for the
|
|
||||||
affected module before considering the work done (see module `AGENTS.md` for exact
|
|
||||||
commands).
|
|
||||||
* Make small and logical commits following the commit guideline described in
|
|
||||||
`CONTRIBUTING.md`. Commit only when explicitly asked.
|
|
||||||
- Do not guess or hallucinate git author information (Name or Email). Never include the
|
|
||||||
`--author` flag in git commands unless specifically instructed by the user for a unique
|
|
||||||
case; assume the local environment is already configured. Allow git commit to
|
|
||||||
automatically pull the identity from the local git config `user.name` and `user.email`.
|
|
||||||
40
.travis.yml
Normal file
40
.travis.yml
Normal 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
|
||||||
9
.vscode/settings.json
vendored
Normal file
9
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"files.exclude": {
|
||||||
|
"**/.clj-kondo": true,
|
||||||
|
"**/.cpcache": true,
|
||||||
|
"**/.lsp": true,
|
||||||
|
"**/.shadow-cljs": true,
|
||||||
|
"**/node_modules": true
|
||||||
|
}
|
||||||
|
}
|
||||||
11
.yarnrc.yml
Normal file
11
.yarnrc.yml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
enableGlobalCache: true
|
||||||
|
|
||||||
|
enableImmutableCache: false
|
||||||
|
|
||||||
|
enableImmutableInstalls: false
|
||||||
|
|
||||||
|
enableTelemetry: false
|
||||||
|
|
||||||
|
httpTimeout: 600000
|
||||||
|
|
||||||
|
nodeLinker: node-modules
|
||||||
93
AGENTS.md
93
AGENTS.md
@ -1,93 +0,0 @@
|
|||||||
# AI Agent Guide
|
|
||||||
|
|
||||||
This document provides the core context and operating guidelines for AI agents
|
|
||||||
working in this repository.
|
|
||||||
|
|
||||||
## Before You Start
|
|
||||||
|
|
||||||
Before responding to any user request, you must:
|
|
||||||
|
|
||||||
1. Read this file completely.
|
|
||||||
2. Identify which modules are affected by the task.
|
|
||||||
3. Load the `AGENTS.md` file **only** for each affected module (see the
|
|
||||||
architecture table below). Not all modules have an `AGENTS.md` — verify the
|
|
||||||
file exists before attempting to read it.
|
|
||||||
4. Do **not** load `AGENTS.md` files for unrelated modules.
|
|
||||||
|
|
||||||
## Role: Senior Software Engineer
|
|
||||||
|
|
||||||
You are a high-autonomy Senior Full-Stack Software Engineer. You have full
|
|
||||||
permission to navigate the codebase, modify files, and execute commands to
|
|
||||||
fulfill your tasks. Your goal is to solve complex technical tasks with high
|
|
||||||
precision while maintaining a strong focus on maintainability and performance.
|
|
||||||
|
|
||||||
### Operational Guidelines
|
|
||||||
|
|
||||||
1. Before writing code, describe your plan. If the task is complex, break it
|
|
||||||
down into atomic steps.
|
|
||||||
2. Be concise and autonomous.
|
|
||||||
3. Do **not** touch unrelated modules unless the task explicitly requires it.
|
|
||||||
4. Commit only when explicitly asked. Follow the commit format rules in
|
|
||||||
`CONTRIBUTING.md`.
|
|
||||||
5. When searching code, prefer `ripgrep` (`rg`) over `grep` — it respects
|
|
||||||
`.gitignore` by default.
|
|
||||||
|
|
||||||
## GitHub Operations
|
|
||||||
|
|
||||||
To obtain the list of repository members/collaborators:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gh api repos/:owner/:repo/collaborators --paginate --jq '.[].login'
|
|
||||||
```
|
|
||||||
|
|
||||||
To obtain the list of open PRs authored by members:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
MEMBERS=$(gh api repos/:owner/:repo/collaborators --paginate --jq '.[].login' | tr '\n' '|' | sed 's/|$//')
|
|
||||||
gh pr list --state open --limit 200 --json author,title,number | jq -r --arg members "$MEMBERS" '
|
|
||||||
($members | split("|")) as $m |
|
|
||||||
.[] | select(.author.login as $a | $m | index($a)) |
|
|
||||||
"\(.number)\t\(.author.login)\t\(.title)"
|
|
||||||
'
|
|
||||||
```
|
|
||||||
|
|
||||||
To obtain the list of open PRs from external contributors (non-members):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
MEMBERS=$(gh api repos/:owner/:repo/collaborators --paginate --jq '.[].login' | tr '\n' '|' | sed 's/|$//')
|
|
||||||
gh pr list --state open --limit 200 --json author,title,number | jq -r --arg members "$MEMBERS" '
|
|
||||||
($members | split("|")) as $m |
|
|
||||||
.[] | select(.author.login as $a | $m | index($a) | not) |
|
|
||||||
"\(.number)\t\(.author.login)\t\(.title)"
|
|
||||||
'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture Overview
|
|
||||||
|
|
||||||
Penpot is an open-source design tool composed of several modules:
|
|
||||||
|
|
||||||
| Directory | Language | Purpose | Has `AGENTS.md` |
|
|
||||||
|-----------|----------|---------|:----------------:|
|
|
||||||
| `frontend/` | ClojureScript + SCSS | Single-page React app (design editor) | Yes |
|
|
||||||
| `backend/` | Clojure (JVM) | HTTP/RPC server, PostgreSQL, Redis | Yes |
|
|
||||||
| `common/` | Cljc (shared Clojure/ClojureScript) | Data types, geometry, schemas, utilities | Yes |
|
|
||||||
| `render-wasm/` | Rust -> WebAssembly | High-performance canvas renderer (Skia) | Yes |
|
|
||||||
| `exporter/` | ClojureScript (Node.js) | Headless Playwright-based export (SVG/PDF) | No |
|
|
||||||
| `mcp/` | TypeScript | Model Context Protocol integration | No |
|
|
||||||
| `plugins/` | TypeScript | Plugin runtime and example plugins | No |
|
|
||||||
|
|
||||||
Some submodules use `pnpm` workspaces. The root `package.json` and
|
|
||||||
`pnpm-lock.yaml` manage shared dependencies. Helper scripts live in `scripts/`.
|
|
||||||
|
|
||||||
### Module Dependency Graph
|
|
||||||
|
|
||||||
```
|
|
||||||
frontend ──> common
|
|
||||||
backend ──> common
|
|
||||||
exporter ──> common
|
|
||||||
frontend ──> render-wasm (loads compiled WASM)
|
|
||||||
```
|
|
||||||
|
|
||||||
`common` is referenced as a local dependency (`{:local/root "../common"}`) by
|
|
||||||
both `frontend` and `backend`. Changes to `common` can therefore affect multiple
|
|
||||||
modules — test across consumers when modifying shared code.
|
|
||||||
328
CHANGES.md
328
CHANGES.md
@ -1,330 +1,5 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
## 2.17.0 (Unreleased)
|
|
||||||
|
|
||||||
### :boom: Breaking changes & Deprecations
|
|
||||||
|
|
||||||
### :rocket: Epics and highlights
|
|
||||||
|
|
||||||
- Add MCP server integration [Taiga #13112](https://tree.taiga.io/project/penpot/us/13112)
|
|
||||||
|
|
||||||
### :sparkles: New features & Enhancements
|
|
||||||
|
|
||||||
- Show alpha percentage next to library color values to distinguish colors that differ only in opacity (by @rockchris099) [Github #6328](https://github.com/penpot/penpot/issues/6328)
|
|
||||||
- Add "Clear artboard guides" option to right-click context menu for frames (by @eureka0928) [Github #6987](https://github.com/penpot/penpot/issues/6987)
|
|
||||||
- Add loader feedback while importing and exporting files [Github #9020](https://github.com/penpot/penpot/issues/9020)
|
|
||||||
- Allow duplicating color and typography styles (by @MkDev11) [Github #2912](https://github.com/penpot/penpot/issues/2912)
|
|
||||||
- Add woff2 support on user uploaded fonts (by @Nivl) [Github #8248](https://github.com/penpot/penpot/pull/8248)
|
|
||||||
- Import Tokens from linked library (by @dfelinto) [Github #8391](https://github.com/penpot/penpot/pull/8391)
|
|
||||||
- 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)
|
|
||||||
- Add Tab/Shift+Tab navigation to rename layers sequentially (by @bittoby) [Github #8474](https://github.com/penpot/penpot/pull/8474)
|
|
||||||
- Copy and paste entire rows in existing table (by @bittoby) [Github #8498](https://github.com/penpot/penpot/pull/8498)
|
|
||||||
- Rename token group [Taiga #13137](https://tree.taiga.io/project/penpot/us/13137)
|
|
||||||
- Duplicate token group [Taiga #10653](https://tree.taiga.io/project/penpot/us/10653)
|
|
||||||
- Copy token name from contextual menu [Taiga #13568](https://tree.taiga.io/project/penpot/issue/13568)
|
|
||||||
- Add natural sorting on token names [Taiga #13713](https://tree.taiga.io/project/penpot/issue/13713)
|
|
||||||
- Add drag-to-change for numeric inputs in workspace sidebar [Github #2466](https://github.com/penpot/penpot/issues/2466)
|
|
||||||
- Add CSS linter [Taiga #13790](https://tree.taiga.io/project/penpot/us/13790)
|
|
||||||
- Save and restore selection state in undo/redo (by @eureka0928) [Github #6007](https://github.com/penpot/penpot/issues/6007)
|
|
||||||
- Fix warnings for unsupported token $type (by @Dexterity104) [Github #8790](https://github.com/penpot/penpot/issues/8790)
|
|
||||||
- Add per-group add button for typographies (by @eureka0928) [Github #5275](https://github.com/penpot/penpot/issues/5275)
|
|
||||||
- Add Find & Replace for text content and layer names (by @statxc) [Github #7108](https://github.com/penpot/penpot/issues/7108)
|
|
||||||
- Use page name for multi-export ZIP/PDF downloads (by @Dexterity104) [Github #8773](https://github.com/penpot/penpot/issues/8773)
|
|
||||||
- Make links in comments clickable (by @eureka0928) [Github #1602](https://github.com/penpot/penpot/issues/1602)
|
|
||||||
- Add visibility toggle for strokes (by @eureka0928) [Github #7438](https://github.com/penpot/penpot/issues/7438)
|
|
||||||
- Sort asset library subfolders alphabetically at every nesting level (by @eureka0928) [Github #2572](https://github.com/penpot/penpot/issues/2572)
|
|
||||||
- Add Paste to replace (Cmd+Shift+V) to replace the selected shape with clipboard contents (by @eureka0928) [Github #4240](https://github.com/penpot/penpot/issues/4240)
|
|
||||||
- Differentiate incoming and outgoing interaction link colors (by @claytonlin1110) [Github #7794](https://github.com/penpot/penpot/issues/7794)
|
|
||||||
- Add guide locking and fix locked elements not selectable in viewer (by @Dexterity104) [Github #8358](https://github.com/penpot/penpot/issues/8358)
|
|
||||||
- Apply styles to selection (by @AzazelN28) [Taiga #13647](https://tree.taiga.io/project/penpot/task/13647)
|
|
||||||
- Reorder prototyping overlay options to show Position before Relative to (by @rockchris099) [Github #2910](https://github.com/penpot/penpot/issues/2910)
|
|
||||||
- Add customizable colors for ruler guides (by @Dexterity104) [Github #5199](https://github.com/penpot/penpot/issues/5199)
|
|
||||||
- Persist asset search query and section filter when switching sidebar tabs (by @eureka0928) [Github #2913](https://github.com/penpot/penpot/issues/2913)
|
|
||||||
- Add delete and duplicate buttons to typography dialog (by @eureka0928) [Github #5270](https://github.com/penpot/penpot/issues/5270)
|
|
||||||
- Edit ruler guide position by double-clicking the guide pill (by @eureka0928) [Github #2311](https://github.com/penpot/penpot/issues/2311)
|
|
||||||
- Add a search bar to filter colors in the color palette toolbar (by @eureka0928) [Github #7653](https://github.com/penpot/penpot/issues/7653)
|
|
||||||
- Allow customising the OIDC login button label (by @wdeveloper16) [Github #7027](https://github.com/penpot/penpot/issues/7027)
|
|
||||||
- Add page separators in Workspace [Taiga #13611](https://tree.taiga.io/project/penpot/us/13611?milestone=262806)
|
|
||||||
- Add Shift+Numpad0/1/2 as aliases to Shift+0/1/2 for zoom shortcuts [Github #2457](https://github.com/penpot/penpot/issues/2457)
|
|
||||||
|
|
||||||
### :bug: Bugs fixed
|
|
||||||
|
|
||||||
- Fix `PENPOT_OIDC_USER_INFO_SOURCE` flag being silently ignored (`userinfo` / `token`) in the OIDC callback, causing "incomplete user info" failures during registration [Github #9108](https://github.com/penpot/penpot/issues/9108)
|
|
||||||
- Fix `get-view-only-bundle` crashing when a share-link viewer encounters a team member whose email lacks `@` (NullPointerException in `obfuscate-email`) or whose domain has no `.` (previously produced a dangling-dot `****@****.`); now the viewer-side obfuscation is nil-safe and omits the trailing dot when the domain has no TLD
|
|
||||||
- Remove `corepack` from the MCP local launcher so it runs on Node.js 25+, where corepack is no longer bundled [Github #8877](https://github.com/penpot/penpot/issues/8877)
|
|
||||||
- Fix Copy as SVG: emit a single valid SVG document when multiple shapes are selected, and publish `image/svg+xml` to the clipboard so the paste target works in Inkscape and other SVG-native tools [Github #838](https://github.com/penpot/penpot/issues/838)
|
|
||||||
- Reset profile submenu state when the account menu closes (by @eureka0928) [Github #8947](https://github.com/penpot/penpot/issues/8947)
|
|
||||||
- Add export panel to inspect styles tab [Taiga #13582](https://tree.taiga.io/project/penpot/issue/13582)
|
|
||||||
- Fix styles between grid layout inputs [Taiga #13526](https://tree.taiga.io/project/penpot/issue/13526)
|
|
||||||
- Fix id prop on switch component [Taiga #13534](https://tree.taiga.io/project/penpot/issue/13534)
|
|
||||||
- Update copy on penpot update message [Taiga #12924](https://tree.taiga.io/project/penpot/issue/12924)
|
|
||||||
- Fix scroll on library modal [Taiga #13639](https://tree.taiga.io/project/penpot/issue/13639)
|
|
||||||
- Fix dates to avoid show them in english when browser is in auto [Taiga #13786](https://tree.taiga.io/project/penpot/issue/13786)
|
|
||||||
- Fix focus radio button [Taiga #13841](https://tree.taiga.io/project/penpot/issue/13841)
|
|
||||||
- Token tree should be expanded by default [Taiga #13631](https://tree.taiga.io/project/penpot/issue/13631)
|
|
||||||
- Fix opacity incorrectly disabled for visible shapes [Taiga #13906](https://tree.taiga.io/project/penpot/issue/13906)
|
|
||||||
- Update onboarding image [Taiga #13864](https://tree.taiga.io/project/penpot/issue/13864)
|
|
||||||
- Fix plugin modal drag interactions over iframe and close-button behavior (by @marekhrabe) [Github #8871](https://github.com/penpot/penpot/pull/8871)
|
|
||||||
- Fix hot update on color-row on texts [Taiga #13923](https://tree.taiga.io/project/penpot/issue/13923)
|
|
||||||
- Fix selected color tokens [Taiga #13930](https://tree.taiga.io/project/penpot/issue/13930)
|
|
||||||
- Fix dashboard Recent/Deleted titles overlapped by scrolling content (by @rockchris099) [Github #8577](https://github.com/penpot/penpot/issues/8577)
|
|
||||||
- Display resolved values of inactive tokens [Taiga #13628](https://tree.taiga.io/project/penpot/issue/13628)
|
|
||||||
- Fix hyphens stripped from export filenames (by @jamesrayammons) [Github #8901](https://github.com/penpot/penpot/issues/8901)
|
|
||||||
- Fix app crash when selecting shapes with one hidden [Taiga #13959](https://tree.taiga.io/project/penpot/issue/13959)
|
|
||||||
- Fix opacity mixed value [Taiga #13960](https://tree.taiga.io/project/penpot/issue/13960)
|
|
||||||
- Fix gap input throwing an error [Github #8984](https://github.com/penpot/penpot/pull/8984)
|
|
||||||
- Fix non-functional clear icon in change email modal inputs (by @Dexterity104) [Github #8977](https://github.com/penpot/penpot/issues/8977)
|
|
||||||
- Disable save button after saving account profile settings (by @Dexterity104) [Github #8979](https://github.com/penpot/penpot/issues/8979)
|
|
||||||
- Fix copy to be more specific [Taiga #13990](https://tree.taiga.io/project/penpot/issue/13990)
|
|
||||||
- Allow deleting the profile avatar after uploading [Github #9067](https://github.com/penpot/penpot/issues/9067)
|
|
||||||
- Fix incorrect rendering when exporting text as SVG, PNG and JPG (by @edwin-rivera-dev) [Github #8516](https://github.com/penpot/penpot/issues/8516)
|
|
||||||
- Fix Settings and Notifications "Update Settings" button enabled state when form has no changes (by @moorsecopers99) [Github #9090](https://github.com/penpot/penpot/issues/9090)
|
|
||||||
- Fix "Help & Learning" submenu vertical alignment in account menu (by @juan-flores077) [Github #9137](https://github.com/penpot/penpot/issues/9137)
|
|
||||||
- Fix plugin `addInteraction` silently rejecting `open-overlay` actions with `manualPositionLocation` [Github #8409](https://github.com/penpot/penpot/issues/8409)
|
|
||||||
- Fix typography style creation with tokenized line-height (by @juan-flores077) [Github #8479](https://github.com/penpot/penpot/issues/8479)
|
|
||||||
- Fix colorpicker layout so the eyedropper button is visible again [Taiga #14057](https://tree.taiga.io/project/penpot/issue/14057)
|
|
||||||
|
|
||||||
|
|
||||||
## 2.16.0 (Unreleased)
|
|
||||||
|
|
||||||
### :boom: Breaking changes & Deprecations
|
|
||||||
|
|
||||||
### :rocket: Epics and highlights
|
|
||||||
|
|
||||||
### :sparkles: New features & Enhancements
|
|
||||||
|
|
||||||
- Access Tokens look & feel refinement [Taiga #13114](https://tree.taiga.io/project/penpot/us/13114)
|
|
||||||
- Enhance readability of applied tokens in plugins API [Taiga #13714](https://tree.taiga.io/project/penpot/issue/13714)
|
|
||||||
|
|
||||||
### :bug: Bugs fixed
|
|
||||||
|
|
||||||
- Fix Alt/Option to draw shapes from center point (by @offreal) [Github #8361](https://github.com/penpot/penpot/pull/8361)
|
|
||||||
- Add token name on broken token pill on sidebar [Taiga #13527](https://tree.taiga.io/project/penpot/issue/13527)
|
|
||||||
- Fix tooltip activated when tab change [Taiga #13627](https://tree.taiga.io/project/penpot/issue/13627)
|
|
||||||
- Fix title on shared button [Taiga #13730](https://tree.taiga.io/project/penpot/issue/13730)
|
|
||||||
- Fix hover on layers [Taiga #13799](https://tree.taiga.io/project/penpot/issue/13799)
|
|
||||||
- Fix highlight after name edition [Taiga #13783](https://tree.taiga.io/project/penpot/issue/13783)
|
|
||||||
- Fix id prop on switch component [Taiga #13534](https://tree.taiga.io/project/penpot/issue/13534)
|
|
||||||
- Fix dashboard navigation tabs overlap with projects content when scrolling [Taiga #13962](https://tree.taiga.io/project/penpot/issue/13962)
|
|
||||||
- Fix text editor v1 focus [Taiga #13961](https://tree.taiga.io/project/penpot/issue/13961)
|
|
||||||
- Fix color dropdown option update [Taiga #14035](https://tree.taiga.io/project/penpot/issue/14035)
|
|
||||||
- Fix themes modal height [Taiga #14046](https://tree.taiga.io/project/penpot/issue/14046)
|
|
||||||
|
|
||||||
|
|
||||||
## 2.15.0 (Unreleased)
|
|
||||||
|
|
||||||
### :sparkles: New features & Enhancements
|
|
||||||
|
|
||||||
- Add MCP server integration [Taiga #13112](https://tree.taiga.io/project/penpot/us/13112)
|
|
||||||
- Add chunked upload API for large media and binary files (removes previous upload size limits) [Github #8909](https://github.com/penpot/penpot/pull/8909)
|
|
||||||
|
|
||||||
### :bug: Bugs fixed
|
|
||||||
|
|
||||||
- Fix incorrect handling of version restore operation [Github #9041](https://github.com/penpot/penpot/pull/9041)
|
|
||||||
|
|
||||||
|
|
||||||
## 2.14.4
|
|
||||||
|
|
||||||
### :bug: Bugs fixed
|
|
||||||
|
|
||||||
- Fix email validation [Taiga #14006](https://tree.taiga.io/project/penpot/issue/14006)
|
|
||||||
- Fix email blacklisting [Github #9122](https://github.com/penpot/penpot/pull/9122)
|
|
||||||
- Fix removeChild errors from unmount race conditions [Github #8927](https://github.com/penpot/penpot/pull/8927)
|
|
||||||
|
|
||||||
|
|
||||||
## 2.14.3
|
|
||||||
|
|
||||||
### :sparkles: New features & Enhancements
|
|
||||||
|
|
||||||
- Add webp export format to plugin types [Github #8870](https://github.com/penpot/penpot/pull/8870)
|
|
||||||
- Use shared singleton containers for React portals to reduce DOM growth [Github #8957](https://github.com/penpot/penpot/pull/8957)
|
|
||||||
|
|
||||||
### :bug: Bugs fixed
|
|
||||||
|
|
||||||
- Fix component "broken" after switch variant [Taiga #12984](https://tree.taiga.io/project/penpot/issue/12984)
|
|
||||||
- Fix variants corner cases with selrect and points [Github #8882](https://github.com/penpot/penpot/pull/8882)
|
|
||||||
- Fix dashboard navigation tabs overlap with projects content when scrolling [Taiga #13962](https://tree.taiga.io/project/penpot/issue/13962)
|
|
||||||
- Fix text editor v1 focus [Taiga #13961](https://tree.taiga.io/project/penpot/issue/13961)
|
|
||||||
- Fix highlight on frames after rename [Github #8938](https://github.com/penpot/penpot/pull/8938)
|
|
||||||
- Fix TypeError in sd-token-uuid when resolving tokens interactively [Github #8929](https://github.com/penpot/penpot/pull/8929)
|
|
||||||
- Fix path drawing preview passing shape instead of content to next-node
|
|
||||||
- Fix swapped arguments in CLJS PathData `-nth` with default
|
|
||||||
- Normalize PathData coordinates to safe integer bounds on read
|
|
||||||
- Fix RangeError from re-entrant error handling causing stack overflow [Github #8962](https://github.com/penpot/penpot/pull/8962)
|
|
||||||
- Fix builder bool styles and media validation [Github #8963](https://github.com/penpot/penpot/pull/8963)
|
|
||||||
- Fix "Move to" menu allowing same project as target when multiple files are selected
|
|
||||||
- Fix crash when index query param is duplicated in URL
|
|
||||||
- Fix wrong extremity point in path `calculate-extremities` for line-to segments
|
|
||||||
- Fix reversed args in DTCG shadow composite token conversion
|
|
||||||
- Fix `inside-layout?` passing shape id instead of shape to `frame-shape?`
|
|
||||||
- Fix wrong `mapcat` call in `collect-main-shapes`
|
|
||||||
- Fix stale accumulator in `get-children-in-instance` recursion
|
|
||||||
- Fix typo `:podition` in swap-shapes grid cell
|
|
||||||
- Fix multiple selection on shapes with token applied to stroke color
|
|
||||||
|
|
||||||
|
|
||||||
## 2.14.2
|
|
||||||
|
|
||||||
### :sparkles: New features & Enhancements
|
|
||||||
|
|
||||||
- Add protection for stale JS asset cache to force reload on version mismatch [Github #8638](https://github.com/penpot/penpot/pull/8638)
|
|
||||||
- Normalize newsletter opt-in checkbox across different register flows [Github #8839](https://github.com/penpot/penpot/pull/8839)
|
|
||||||
|
|
||||||
### :bug: Bugs fixed
|
|
||||||
|
|
||||||
- Fix PathData corruption root causes across WASM and CLJS (unsafe transmute and byteOffset handling)
|
|
||||||
- Handle corrupted PathData segments gracefully instead of crashing
|
|
||||||
- Fix swapped move-to/line-to type codes in PathData binary readers
|
|
||||||
- Fix non-integer row/column values in grid cell position inputs [Github #8869](https://github.com/penpot/penpot/pull/8869)
|
|
||||||
- Fix nil path content crash by exposing safe public API [Github #8806](https://github.com/penpot/penpot/pull/8806)
|
|
||||||
- Fix infinite recursion in get-frame-ids for thumbnail extraction [Github #8807](https://github.com/penpot/penpot/pull/8807)
|
|
||||||
- Fix stale-asset detector missing protocol-dispatch errors
|
|
||||||
- Ignore Zone.js toString TypeError in uncaught error handler [Github #8804](https://github.com/penpot/penpot/pull/8804)
|
|
||||||
- Prevent thumbnail frame recursion overflow [Github #8763](https://github.com/penpot/penpot/pull/8763)
|
|
||||||
- Fix vector index out of bounds in viewer zoom-to-fit/fill [Github #8834](https://github.com/penpot/penpot/pull/8834)
|
|
||||||
- Guard delete undo against missing sibling order [Github #8858](https://github.com/penpot/penpot/pull/8858)
|
|
||||||
- Fix ICounted error on numeric-input token dropdown keyboard nav [Github #8803](https://github.com/penpot/penpot/pull/8803)
|
|
||||||
|
|
||||||
## 2.14.1
|
|
||||||
|
|
||||||
### :sparkles: New features & Enhancements
|
|
||||||
|
|
||||||
- Add automatic retry with backoff for idempotent RPC requests on network failures [Github #8792](https://github.com/penpot/penpot/pull/8792)
|
|
||||||
- Add scroll and zoom throttling to one state update per animation frame [Github #8812](https://github.com/penpot/penpot/pull/8812)
|
|
||||||
- Improve error handling and exception formatting [Github #8757](https://github.com/penpot/penpot/pull/8757)
|
|
||||||
|
|
||||||
### :bug: Bugs fixed
|
|
||||||
|
|
||||||
- Fix crash in apply-text-modifier with nil selrect or modifier [Github #8762](https://github.com/penpot/penpot/pull/8762)
|
|
||||||
- Fix incorrect attrs references on generate-sync-shape [Github #8776](https://github.com/penpot/penpot/pull/8776)
|
|
||||||
- Fix regression on subpath support [Github #8793](https://github.com/penpot/penpot/pull/8793)
|
|
||||||
- Improve error reporting on request parsing failures [Github #8805](https://github.com/penpot/penpot/pull/8805)
|
|
||||||
- Fix fetch abort errors escaping the unhandled exception handler [Github #8801](https://github.com/penpot/penpot/pull/8801)
|
|
||||||
- Fix nil deref on missing bounds in layout modifier propagation [Github #8735](https://github.com/penpot/penpot/pull/8735)
|
|
||||||
- Fix TypeError when token error map lacks :error/fn key [Github #8767](https://github.com/penpot/penpot/pull/8767)
|
|
||||||
- Fix dissoc error when detaching stroke color from library [Github #8738](https://github.com/penpot/penpot/pull/8738)
|
|
||||||
- Fix crash when pasting image into text editor
|
|
||||||
- Fix null text crash on paste in text editor
|
|
||||||
- Ensure path content is always PathData when saving
|
|
||||||
- Fix error when get-parent-with-data encounters non-Element nodes
|
|
||||||
|
|
||||||
## 2.14.0
|
|
||||||
|
|
||||||
### :boom: Breaking changes & Deprecations
|
|
||||||
|
|
||||||
- Deprecate `PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE` in favour of `PENPOT_HTTP_SERVER_MAX_BODY_SIZE`.
|
|
||||||
|
|
||||||
### :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)
|
|
||||||
- [MCP server] Integrations section [Taiga #13112](https://tree.taiga.io/project/penpot/us/13112)
|
|
||||||
- [Access Tokens] Look & feel refinement [Taiga #13114](https://tree.taiga.io/project/penpot/us/13114)
|
|
||||||
|
|
||||||
### :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)
|
|
||||||
- Fix 45 rotated board titles rendered incorrectly [Taiga #13306](https://tree.taiga.io/project/penpot/issue/13306)
|
|
||||||
- Fix cannot apply second token after creation while shape is selected [Taiga #13513](https://tree.taiga.io/project/penpot/issue/13513)
|
|
||||||
- Fix error activating a set with invalid shadow token applied [Taiga #13528](https://tree.taiga.io/project/penpot/issue/13528)
|
|
||||||
- Fix component "broken" after variant switch [Taiga #12984](https://tree.taiga.io/project/penpot/issue/12984)
|
|
||||||
- Fix incorrect query for file versions [Github #8463](https://github.com/penpot/penpot/pull/8463)
|
|
||||||
- Fix warning when clicking on number token pills [Taiga #13661](https://tree.taiga.io/project/penpot/issue/13661)
|
|
||||||
- Fix 'not ISeqable' error when entering float values in layout item and opacity inputs [Github #8569](https://github.com/penpot/penpot/pull/8569)
|
|
||||||
- Fix crash in select component when options vector is empty [Github #8578](https://github.com/penpot/penpot/pull/8578)
|
|
||||||
- Fix scroll on colorpicker [Taiga #13623](https://tree.taiga.io/project/penpot/issue/13623)
|
|
||||||
- Fix crash when pasting non-map transit clipboard data [Github #8580](https://github.com/penpot/penpot/pull/8580)
|
|
||||||
- Fix `penpot.openPage()` plugin API not navigating in the same tab; change default to same-tab navigation and allow passing a UUID string instead of a Page object [Github #8520](https://github.com/penpot/penpot/issues/8520)
|
|
||||||
|
|
||||||
## 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!)
|
|
||||||
|
|
||||||
- Add 'page' special shapeId to MCP export_shape tool for full-page snapshots [Github #8689](https://github.com/penpot/penpot/issues/8689)
|
|
||||||
|
|
||||||
- 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
|
## 2.12.0
|
||||||
|
|
||||||
### :boom: Breaking changes & Deprecations
|
### :boom: Breaking changes & Deprecations
|
||||||
@ -336,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
|
||||||
@ -368,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
|
||||||
@ -431,6 +108,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`
|
||||||
|
|||||||
301
CONTRIBUTING.md
301
CONTRIBUTING.md
@ -1,196 +1,213 @@
|
|||||||
# Contributing Guide
|
# Contributing Guide #
|
||||||
|
|
||||||
Thank you for your interest in contributing to Penpot. This guide covers
|
Thank you for your interest in contributing to Penpot. This is a
|
||||||
how to propose changes, submit fixes, and follow project conventions.
|
generic guide that details how to contribute to the project in a way that
|
||||||
|
is efficient for everyone. If you are looking for specific documentation on
|
||||||
|
different parts of the platform, please refer to the `docs/` directory,
|
||||||
|
or the rendered version at the [Help Center](https://help.penpot.app/).
|
||||||
|
|
||||||
For architecture details, module-specific guidelines, and AI-agent
|
## Reporting Bugs ##
|
||||||
instructions, see [AGENTS.md](AGENTS.md). For final user technical
|
|
||||||
documentation, see the `docs/` directory or the rendered [Help
|
|
||||||
Center](https://help.penpot.app/).
|
|
||||||
|
|
||||||
## Table of Contents
|
We are using [GitHub Issues](https://github.com/penpot/penpot/issues)
|
||||||
|
for our public bugs. We keep a close eye on them and try to make it
|
||||||
|
clear when we have an internal fix in progress. Before filing a new
|
||||||
|
task, try to make sure your problem doesn't already exist.
|
||||||
|
|
||||||
- [Prerequisites](#prerequisites)
|
If you found a bug, please report it, as far as possible, with:
|
||||||
- [Reporting Bugs](#reporting-bugs)
|
|
||||||
- [Pull Requests](#pull-requests)
|
|
||||||
- [Commit Guidelines](#commit-guidelines)
|
|
||||||
- [Formatting and Linting](#formatting-and-linting)
|
|
||||||
- [Changelog](#changelog)
|
|
||||||
- [Code of Conduct](#code-of-conduct)
|
|
||||||
- [Developer's Certificate of Origin (DCO)](#developers-certificate-of-origin-dco)
|
|
||||||
|
|
||||||
## Prerequisites
|
- a detailed explanation of steps to reproduce the error
|
||||||
|
- the browser and browser version used
|
||||||
|
- a dev tools console exception stack trace (if available)
|
||||||
|
|
||||||
- **Language**: Penpot is written primarily in Clojure (backend), ClojureScript
|
If you found a bug which you think is better to discuss in private (for
|
||||||
(frontend/exporter), and Rust (render-wasm). Familiarity with the Clojure
|
example, security bugs), consider first sending an email to
|
||||||
ecosystem is expected for most contributions.
|
`support@penpot.app`.
|
||||||
- **Issue tracker**: We use [GitHub Issues](https://github.com/penpot/penpot/issues)
|
|
||||||
for public bugs and [Taiga](https://tree.taiga.io/project/penpot/) for
|
|
||||||
internal project management. Changelog entries reference both.
|
|
||||||
|
|
||||||
## Reporting Bugs
|
**We don't have a formal bug bounty program for security reports; this
|
||||||
|
is an open source application, and your contribution will be recognized
|
||||||
|
in the changelog.**
|
||||||
|
|
||||||
Report bugs via [GitHub Issues](https://github.com/penpot/penpot/issues).
|
|
||||||
Before filing, search existing issues to avoid duplicates.
|
|
||||||
|
|
||||||
Include the following when possible:
|
## Pull Requests ##
|
||||||
|
|
||||||
1. Steps to reproduce the error.
|
If you want to propose a change or bug fix via a pull request (PR),
|
||||||
2. Browser and browser version used.
|
you should first carefully read the section **Developer's Certificate of
|
||||||
3. DevTools console exception stack trace (if available).
|
Origin**. You must also format your code and commits according to the
|
||||||
|
instructions below.
|
||||||
|
|
||||||
For security bugs or issues better discussed in private, email
|
If you intend to fix a bug, it's fine to submit a pull request right
|
||||||
`support@penpot.app` or report them on [Github Security
|
away, but we still recommend filing an issue detailing what you're
|
||||||
Advisories](https://github.com/penpot/penpot/security/advisories)
|
fixing. This is helpful in case we don't accept that specific fix but
|
||||||
|
want to keep track of the issue.
|
||||||
|
|
||||||
> **Note:** We do not have a formal bug bounty program. Security
|
If you want to implement or start working on a new feature, please
|
||||||
> contributions are recognized in the changelog.
|
open a **question*- / **discussion*- issue for it. No PR
|
||||||
|
will be accepted without a prior discussion about the changes,
|
||||||
|
whether it is a new feature, an already planned one, or a quick win.
|
||||||
|
|
||||||
## Pull Requests
|
If it is your first PR, you can learn how to proceed from
|
||||||
|
[this free video
|
||||||
|
series](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github)
|
||||||
|
|
||||||
### Workflow
|
We use the `easy fix` tag to indicate issues that are appropriate for beginners.
|
||||||
|
|
||||||
1. **Read the DCO** — see [Developer's Certificate of Origin](#developers-certificate-of-origin-dco)
|
## Commit Guidelines ##
|
||||||
below. All code patches must include a `Signed-off-by` line.
|
|
||||||
2. **Discuss before building** — open a question/discussion issue before
|
|
||||||
starting work on a new feature or significant change. No PR will be
|
|
||||||
accepted without prior discussion, whether it is a new feature, a planned
|
|
||||||
one, or a quick win.
|
|
||||||
3. **Bug fixes** — you may submit a PR directly, but we still recommend
|
|
||||||
filing an issue first so we can track it independently of your fix.
|
|
||||||
4. **Format and lint** — run the checks described in
|
|
||||||
[Formatting and Linting](#formatting-and-linting) before submitting.
|
|
||||||
|
|
||||||
### Good first issues
|
We have very precise rules on how our git commit messages must be formatted.
|
||||||
|
|
||||||
We use the `easy fix` label to mark issues appropriate for newcomers.
|
The commit message format is:
|
||||||
|
|
||||||
## Commit Guidelines
|
|
||||||
|
|
||||||
Commit messages must follow this format:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
:emoji: <subject>
|
<type> <subject>
|
||||||
|
|
||||||
[body]
|
[body]
|
||||||
|
|
||||||
[footer]
|
[footer]
|
||||||
```
|
```
|
||||||
|
|
||||||
### Commit types
|
Where type is:
|
||||||
|
|
||||||
| Emoji | Description |
|
- :bug: `:bug:` a commit that fixes a bug
|
||||||
|-------|-------------|
|
- :sparkles: `:sparkles:` a commit that adds an improvement
|
||||||
| :bug: | Bug fix |
|
- :tada: `:tada:` a commit with a new feature
|
||||||
| :sparkles: | Improvement or enhancement |
|
- :recycle: `:recycle:` a commit that introduces a refactor
|
||||||
| :tada: | New feature |
|
- :lipstick: `:lipstick:` a commit with cosmetic changes
|
||||||
| :recycle: | Refactor |
|
- :ambulance: `:ambulance:` a commit that fixes a critical bug
|
||||||
| :lipstick: | Cosmetic changes |
|
- :books: `:books:` a commit that improves or adds documentation
|
||||||
| :ambulance: | Critical bug fix |
|
- :construction: `:construction:` a WIP commit
|
||||||
| :books: | Documentation |
|
- :boom: `:boom:` a commit with breaking changes
|
||||||
| :construction: | Work in progress |
|
- :wrench: `:wrench:` a commit for config updates
|
||||||
| :boom: | Breaking change |
|
- :zap: `:zap:` a commit with performance improvements
|
||||||
| :wrench: | Configuration update |
|
- :whale: `:whale:` a commit for Docker-related stuff
|
||||||
| :zap: | Performance improvement |
|
- :paperclip: `:paperclip:` a commit with other non-relevant changes
|
||||||
| :whale: | Docker-related change |
|
- :arrow_up: `:arrow_up:` a commit with dependency updates
|
||||||
| :paperclip: | Other non-relevant changes |
|
- :arrow_down: `:arrow_down:` a commit with dependency downgrades
|
||||||
| :arrow_up: | Dependency update |
|
- :fire: `:fire:` a commit that removes files or code
|
||||||
| :arrow_down: | Dependency downgrade |
|
- :globe_with_meridians: `:globe_with_meridians:` a commit that adds or updates
|
||||||
| :fire: | Removal of code or files |
|
translations
|
||||||
| :globe_with_meridians: | Add or update translations |
|
|
||||||
| :rocket: | Epic or highlight |
|
|
||||||
|
|
||||||
### Rules
|
More info:
|
||||||
|
|
||||||
- Use the **imperative mood** in the subject (e.g. "Fix", not "Fixed")
|
- https://gist.github.com/parmentf/035de27d6ed1dce0b36a
|
||||||
- Capitalize the first letter of the subject
|
- https://gist.github.com/rxaviers/7360908
|
||||||
- Add clear and concise description on the body
|
|
||||||
- Do not end the subject with a period
|
|
||||||
- Keep the subject to **70 characters** or fewer
|
|
||||||
- Separate the subject from the body with a **blank line**
|
|
||||||
|
|
||||||
### Examples
|
Each commit should have:
|
||||||
|
|
||||||
```
|
- A concise subject using the imperative mood.
|
||||||
:bug: Fix unexpected error on launching modal
|
- The subject should capitalize the first letter, omit the period
|
||||||
:sparkles: Enable new modal for profile
|
at the end, and be no longer than 65 characters.
|
||||||
:zap: Improve performance of dashboard navigation
|
- A blank line between the subject line and the body.
|
||||||
:ambulance: Fix critical bug on user registration process
|
- An entry in the CHANGES.md file if applicable, referencing the
|
||||||
:tada: Add new approach for user registration
|
GitHub or Taiga issue/user story using these same rules.
|
||||||
```
|
|
||||||
|
|
||||||
## Formatting and Linting
|
Examples of good commit messages:
|
||||||
|
|
||||||
We use [cljfmt](https://github.com/weavejester/cljfmt) for formatting and
|
- `:bug: Fix unexpected error on launching modal`
|
||||||
[clj-kondo](https://github.com/clj-kondo/clj-kondo) for linting.
|
- `:bug: Set proper error message on generic error`
|
||||||
|
- `:sparkles: Enable new modal for profile`
|
||||||
|
- `:zap: Improve performance of dashboard navigation`
|
||||||
|
- `:wrench: Update default backend configuration`
|
||||||
|
- `:books: Add more documentation for authentication process`
|
||||||
|
- `:ambulance: Fix critical bug on user registration process`
|
||||||
|
- `:tada: Add new approach for user registration`
|
||||||
|
|
||||||
|
## Formatting and Linting ##
|
||||||
|
|
||||||
|
You will want to make sure your code is formatted and linted before submitting
|
||||||
|
a PR. We use [cljfmt](https://github.com/weavejester/cljfmt) and
|
||||||
|
[clj-kondo](https://github.com/clj-kondo/clj-kondo) for this. After installing
|
||||||
|
them on your system, you can run them with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Check formatting (does not modify files)
|
# Check formatting
|
||||||
./scripts/check-fmt
|
yarn fmt:clj:check
|
||||||
|
|
||||||
# Fix formatting (modifies files in place)
|
# Check and fix formatting
|
||||||
./scripts/fmt
|
yarn fmt:clj
|
||||||
|
|
||||||
# Lint
|
# Run the linter
|
||||||
./scripts/lint
|
yarn lint:clj
|
||||||
```
|
```
|
||||||
|
|
||||||
Ideally, run these as git pre-commit hooks.
|
There are more choices in `package.json`.
|
||||||
[Husky](https://typicode.github.io/husky/#/) is a convenient option for
|
|
||||||
setting this up.
|
|
||||||
|
|
||||||
## Changelog
|
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/#/).
|
||||||
|
|
||||||
When your change is user-facing or otherwise notable, add an entry to
|
## Code of Conduct ##
|
||||||
[CHANGES.md](CHANGES.md) following the same commit-type conventions. Reference
|
|
||||||
the relevant GitHub issue or Taiga user story.
|
|
||||||
|
|
||||||
## Code of Conduct
|
As contributors and maintainers of this project, we pledge to respect
|
||||||
|
all people who contribute through reporting issues, posting feature
|
||||||
|
requests, updating documentation, submitting pull requests or patches,
|
||||||
|
and other activities.
|
||||||
|
|
||||||
This project follows the [Contributor Covenant](https://www.contributor-covenant.org/).
|
We are committed to making participation in this project a
|
||||||
The full Code of Conduct is available at
|
harassment-free experience for everyone, regardless of level of
|
||||||
[help.penpot.app/contributing-guide/coc](https://help.penpot.app/contributing-guide/coc/)
|
experience, gender, gender identity and expression, sexual
|
||||||
and in the repository's [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).
|
orientation, disability, personal appearance, body size, race,
|
||||||
|
ethnicity, age, or religion.
|
||||||
|
|
||||||
To report unacceptable behavior, open an issue or contact a project maintainer
|
Examples of unacceptable behavior by participants include the use of
|
||||||
directly.
|
sexual language or imagery, derogatory comments or personal attacks,
|
||||||
|
trolling, public or private harassment, insults, or other
|
||||||
|
unprofessional conduct.
|
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit,
|
||||||
|
or reject comments, commits, code, wiki edits, issues, and other
|
||||||
|
contributions that are not aligned with this Code of Conduct. Project
|
||||||
|
maintainers who do not follow the Code of Conduct may be removed from
|
||||||
|
the project team.
|
||||||
|
|
||||||
|
This Code of Conduct applies both within project spaces and in public
|
||||||
|
spaces when an individual is representing the project or its
|
||||||
|
community.
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior
|
||||||
|
may be reported by opening an issue or contacting one or more of the
|
||||||
|
project maintainers.
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the Contributor Covenant, version
|
||||||
|
1.1.0, available from [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/)
|
||||||
|
|
||||||
## Developer's Certificate of Origin (DCO)
|
## Developer's Certificate of Origin (DCO)
|
||||||
|
|
||||||
By submitting code you agree to and can certify the following:
|
By submitting code you agree to and can certify the following:
|
||||||
|
|
||||||
> **Developer's Certificate of Origin 1.1**
|
Developer's Certificate of Origin 1.1
|
||||||
>
|
|
||||||
> By making a contribution to this project, I certify that:
|
|
||||||
>
|
|
||||||
> (a) The contribution was created in whole or in part by me and I have the
|
|
||||||
> right to submit it under the open source license indicated in the file; or
|
|
||||||
>
|
|
||||||
> (b) The contribution is based upon previous work that, to the best of my
|
|
||||||
> knowledge, is covered under an appropriate open source license and I have
|
|
||||||
> the right under that license to submit that work with modifications,
|
|
||||||
> whether created in whole or in part by me, under the same open source
|
|
||||||
> license (unless I am permitted to submit under a different license), as
|
|
||||||
> indicated in the file; or
|
|
||||||
>
|
|
||||||
> (c) The contribution was provided directly to me by some other person who
|
|
||||||
> certified (a), (b) or (c) and I have not modified it.
|
|
||||||
>
|
|
||||||
> (d) I understand and agree that this project and the contribution are public
|
|
||||||
> and that a record of the contribution (including all personal information
|
|
||||||
> I submit with it, including my sign-off) is maintained indefinitely and
|
|
||||||
> may be redistributed consistent with this project or the open source
|
|
||||||
> license(s) involved.
|
|
||||||
|
|
||||||
### Signed-off-by
|
By making a contribution to this project, I certify that:
|
||||||
|
|
||||||
All code patches (**documentation is excluded**) must contain a sign-off line
|
(a) The contribution was created in whole or in part by me and I
|
||||||
at the end of the commit body. Add it automatically with `git commit -s`.
|
have the right to submit it under the open source license
|
||||||
|
indicated in the file; or
|
||||||
|
|
||||||
|
(b) The contribution is based upon previous work that, to the best
|
||||||
|
of my knowledge, is covered under an appropriate open source
|
||||||
|
license and I have the right under that license to submit that
|
||||||
|
work with modifications, whether created in whole or in part
|
||||||
|
by me, under the same open source license (unless I am
|
||||||
|
permitted to submit under a different license), as indicated
|
||||||
|
in the file; or
|
||||||
|
|
||||||
|
(c) The contribution was provided directly to me by some other
|
||||||
|
person who certified (a), (b) or (c) and I have not modified
|
||||||
|
it.
|
||||||
|
|
||||||
|
(d) I understand and agree that this project and the contribution
|
||||||
|
are public and that a record of the contribution (including all
|
||||||
|
personal information I submit with it, including my sign-off) is
|
||||||
|
maintained indefinitely and may be redistributed consistent with
|
||||||
|
this project or the open source license(s) involved.
|
||||||
|
|
||||||
|
Then, all your code patches (**documentation is excluded**) should
|
||||||
|
contain a sign-off at the end of the patch/commit description body. It
|
||||||
|
can be automatically added by adding the `-s` parameter to `git commit`.
|
||||||
|
|
||||||
|
This is an example of what the line should look like:
|
||||||
|
|
||||||
```
|
```
|
||||||
Signed-off-by: Your Real Name <your.email@example.com>
|
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
|
||||||
```
|
```
|
||||||
|
|
||||||
- Use your **real name** — pseudonyms and anonymous contributions are not
|
Please, use your real name (sorry, no pseudonyms or anonymous
|
||||||
allowed.
|
contributions are allowed).
|
||||||
- The `Signed-off-by` line is **mandatory** and must match the commit author.
|
|
||||||
|
|||||||
57
README.md
57
README.md
@ -29,19 +29,25 @@
|
|||||||
<a href="https://fosstodon.org/@penpot/"><b>Mastodon</b></a> •
|
<a href="https://fosstodon.org/@penpot/"><b>Mastodon</b></a> •
|
||||||
<a href="https://bsky.app/profile/penpot.app"><b>Bluesky</b></a> •
|
<a href="https://bsky.app/profile/penpot.app"><b>Bluesky</b></a> •
|
||||||
<a href="https://twitter.com/penpotapp"><b>X</b></a>
|
<a href="https://twitter.com/penpotapp"><b>X</b></a>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[Penpot video](https://github.com/user-attachments/assets/7c67fd7c-04d3-4c9b-88ec-b6f5e23f8332)
|
<br />
|
||||||
|
|
||||||
|
[Penpot video](https://github.com/user-attachments/assets/7c67fd7c-04d3-4c9b-88ec-b6f5e23f8332
|
||||||
|
)
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
Penpot is the first **open-source** design tool for design and code collaboration. Designers can create stunning designs, interactive prototypes, design systems at scale, while developers enjoy ready-to-use code and make their workflow easy and fast. And all of this with no handoff drama.
|
Penpot is the first **open-source** design tool for design and code collaboration. Designers can create stunning designs, interactive prototypes, design systems at scale, while developers enjoy ready-to-use code and make their workflow easy and fast. And all of this with no handoff drama.
|
||||||
|
|
||||||
Available on browser or self-hosted, Penpot works with open standards like SVG, CSS, HTML and JSON, and it’s free!
|
Available on browser or self-hosted, Penpot works with open standards like SVG, CSS, HTML and JSON, and it’s free!
|
||||||
|
|
||||||
The latest updates take Penpot even further. It’s the first design tool to integrate native [design tokens](https://penpot.dev/collaboration/design-tokens)—a single source of truth to improve efficiency and collaboration between product design and development.
|
The latest updates take Penpot even further. It’s the first design tool to integrate native [design tokens](https://penpot.dev/collaboration/design-tokens)—a single source of truth to improve efficiency and collaboration between product design and development.
|
||||||
|
|
||||||
With the [huge 2.0 release](https://penpot.app/dev-diaries), Penpot took the platform to a whole new level. This update introduces the ground-breaking [CSS Grid Layout feature](https://penpot.app/penpot-2.0), a complete UI redesign, a new Components system, and much more.
|
With the [huge 2.0 release](https://penpot.app/dev-diaries), Penpot took the platform to a whole new level. This update introduces the ground-breaking [CSS Grid Layout feature](https://penpot.app/penpot-2.0), a complete UI redesign, a new Components system, and much more.
|
||||||
|
For organizations that need extra service for its teams, [get in touch](https://cal.com/team/penpot/talk-to-us)
|
||||||
|
|
||||||
For organizations that need extra service for its teams, [get in touch](https://cal.com/team/penpot/talk-to-us).
|
🎇 Design, code, and Open Source meet at [Penpot Fest](https://penpot.app/penpotfest)! Be part of the 2025 edition in Madrid, Spain, on October 9-10.
|
||||||
|
|
||||||
## Table of contents ##
|
## Table of contents ##
|
||||||
|
|
||||||
@ -57,42 +63,43 @@ For organizations that need extra service for its teams, [get in touch](https://
|
|||||||
Penpot expresses designs as code. Designers can do their best work and see it will be beautifully implemented by developers in a two-way collaboration.
|
Penpot expresses designs as code. Designers can do their best work and see it will be beautifully implemented by developers in a two-way collaboration.
|
||||||
|
|
||||||
### Plugin system ###
|
### Plugin system ###
|
||||||
|
|
||||||
[Penpot plugins](https://penpot.app/penpothub/plugins) let you expand the platform's capabilities, give you the flexibility to integrate it with other apps, and design custom solutions.
|
[Penpot plugins](https://penpot.app/penpothub/plugins) let you expand the platform's capabilities, give you the flexibility to integrate it with other apps, and design custom solutions.
|
||||||
|
|
||||||
### Designed for developers ###
|
### Designed for developers ###
|
||||||
|
|
||||||
Penpot was built to serve both designers and developers and create a fluid design-code process. You have the choice to enjoy real-time collaboration or play "solo".
|
Penpot was built to serve both designers and developers and create a fluid design-code process. You have the choice to enjoy real-time collaboration or play "solo".
|
||||||
|
|
||||||
### Inspect mode ###
|
### Inspect mode ###
|
||||||
|
|
||||||
Work with ready-to-use code and make your workflow easy and fast. The inspect tab gives instant access to SVG, CSS and HTML code.
|
Work with ready-to-use code and make your workflow easy and fast. The inspect tab gives instant access to SVG, CSS and HTML code.
|
||||||
|
|
||||||
### Self host your own instance ###
|
### Self host your own instance ###
|
||||||
|
|
||||||
Provide your team or organization with a completely owned collaborative design tool. Use Penpot's cloud service or deploy your own Penpot server.
|
Provide your team or organization with a completely owned collaborative design tool. Use Penpot's cloud service or deploy your own Penpot server.
|
||||||
|
|
||||||
### Integrations ###
|
### Integrations ###
|
||||||
|
|
||||||
Penpot offers integration into the development toolchain, thanks to its support for webhooks and an API accessible through access tokens.
|
Penpot offers integration into the development toolchain, thanks to its support for webhooks and an API accessible through access tokens.
|
||||||
|
|
||||||
### Building Design Systems: design tokens, components and variants ###
|
### Building Design Systems: design tokens, components and variants ###
|
||||||
|
|
||||||
Penpot brings design systems to code-minded teams: a single source of truth with native Design Tokens, Components, and Variants for scalable, reusable, and consistent UI across projects and platforms.
|
Penpot brings design systems to code-minded teams: a single source of truth with native Design Tokens, Components, and Variants for scalable, reusable, and consistent UI across projects and platforms.
|
||||||
|
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://github.com/user-attachments/assets/cce75ad6-f783-473f-8803-da9eb8255fef">
|
<img src="https://github.com/user-attachments/assets/cce75ad6-f783-473f-8803-da9eb8255fef">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
## Getting started ##
|
## Getting started ##
|
||||||
|
|
||||||
Penpot is the only design & prototype platform that is deployment agnostic. You can use it in our [SAAS](https://design.penpot.app) or deploy it anywhere.
|
Penpot is the only design & prototype platform that is deployment agnostic. You can use it in our [SAAS](https://design.penpot.app) or deploy it anywhere.
|
||||||
|
|
||||||
Learn how to install it with Docker, Kubernetes, Elestio or other options on [our website](https://penpot.app/self-host).
|
Learn how to install it with Docker, Kubernetes, Elestio or other options on [our website](https://penpot.app/self-host).
|
||||||
|
<br />
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://github.com/user-attachments/assets/93578500-2dbd-4045-a180-e640ea5b3bd5" style="width: 65%;">
|
<img src="https://site-assets.plasmic.app/2168cf524dd543caeff32384eb9ea0a1.svg" alt="Open Source" style="width: 65%;">
|
||||||
</p>
|
</p>
|
||||||
|
<br />
|
||||||
|
|
||||||
## Community ##
|
## Community ##
|
||||||
|
|
||||||
@ -101,7 +108,6 @@ We love the Open Source software community. Contributing is our passion and if i
|
|||||||
If you need help or have any questions; if you’d like to share your experience using Penpot or get inspired; if you’d rather meet our community of developers and designers, [join our Community](https://community.penpot.app/)!
|
If you need help or have any questions; if you’d like to share your experience using Penpot or get inspired; if you’d rather meet our community of developers and designers, [join our Community](https://community.penpot.app/)!
|
||||||
|
|
||||||
You will find the following categories:
|
You will find the following categories:
|
||||||
|
|
||||||
- [Ask the Community](https://community.penpot.app/c/ask-for-help-using-penpot/6)
|
- [Ask the Community](https://community.penpot.app/c/ask-for-help-using-penpot/6)
|
||||||
- [Troubleshooting](https://community.penpot.app/c/technical/8)
|
- [Troubleshooting](https://community.penpot.app/c/technical/8)
|
||||||
- [Help us Improve Penpot](https://community.penpot.app/c/help-us-improve-penpot/7)
|
- [Help us Improve Penpot](https://community.penpot.app/c/help-us-improve-penpot/7)
|
||||||
@ -111,36 +117,45 @@ You will find the following categories:
|
|||||||
- [Penpot in your language](https://community.penpot.app/c/penpot-in-your-language/12)
|
- [Penpot in your language](https://community.penpot.app/c/penpot-in-your-language/12)
|
||||||
- [Design and Code Essentials](https://community.penpot.app/c/design-and-code-essentials/22)
|
- [Design and Code Essentials](https://community.penpot.app/c/design-and-code-essentials/22)
|
||||||
|
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://github.com/user-attachments/assets/7b7d0f6b-a579-4822-a9ae-d3d5a9fc9d19" alt="Community" style="width: 65%;">
|
<img src="https://github.com/penpot/penpot/assets/5446186/6ac62220-a16c-46c9-ab21-d24ae357ed03" alt="Community" style="width: 65%;">
|
||||||
</p>
|
</p>
|
||||||
|
<br />
|
||||||
|
|
||||||
### Code of Conduct ###
|
### Code of Conduct ###
|
||||||
|
|
||||||
Anyone who contributes to Penpot, whether through code, in the community, or at an event, must adhere to the
|
Anyone who contributes to Penpot, whether through code, in the community, or at an event, must adhere to the
|
||||||
[code of conduct](https://help.penpot.app/contributing-guide/coc/) and foster a positive and safe environment.
|
[code of conduct](https://help.penpot.app/contributing-guide/coc/) and foster a positive and safe environment.
|
||||||
|
|
||||||
|
|
||||||
## Contributing ##
|
## Contributing ##
|
||||||
|
|
||||||
Any contribution will make a difference to improve Penpot. How can you get involved?
|
Any contribution will make a difference to improve Penpot. How can you get involved?
|
||||||
|
|
||||||
Choose your way:
|
Choose your way:
|
||||||
|
|
||||||
- Create and [share Libraries & Templates](https://penpot.app/libraries-templates.html) that will be helpful for the community.
|
- Create and [share Libraries & Templates](https://penpot.app/libraries-templates.html) that will be helpful for the community
|
||||||
- Invite your [team to join](https://design.penpot.app/#/auth/register).
|
- Invite your [team to join](https://design.penpot.app/#/auth/register)
|
||||||
- Give this repo a star and follow us on Social Media: [Mastodon](https://fosstodon.org/@penpot/), [Youtube](https://www.youtube.com/c/Penpot), [Instagram](https://instagram.com/penpot.app), [Linkedin](https://www.linkedin.com/company/penpotdesign), [Peertube](https://peertube.kaleidos.net/a/penpot_app), [X](https://twitter.com/penpotapp) and [BlueSky](https://bsky.app/profile/penpot.app).
|
- Give this repo a star and follow us on Social Media: [Mastodon](https://fosstodon.org/@penpot/), [Youtube](https://www.youtube.com/c/Penpot), [Instagram](https://instagram.com/penpot.app), [Linkedin](https://www.linkedin.com/company/penpotdesign), [Peertube](https://peertube.kaleidos.net/a/penpot_app), [X](https://twitter.com/penpotapp) and [BlueSky](https://bsky.app/profile/penpot.app)
|
||||||
- Participate in the [Community](https://community.penpot.app/) space by asking and answering questions; reacting to others’ articles; opening your own conversations and following along on decisions affecting the project.
|
- Participate in the [Community](https://community.penpot.app/) space by asking and answering questions; reacting to others’ articles; opening your own conversations and following along on decisions affecting the project.
|
||||||
- Report bugs with our easy [guide for bugs hunting](https://help.penpot.app/contributing-guide/reporting-bugs/) or [GitHub issues](https://github.com/penpot/penpot/issues).
|
- Report bugs with our easy [guide for bugs hunting](https://help.penpot.app/contributing-guide/reporting-bugs/) or [GitHub issues](https://github.com/penpot/penpot/issues)
|
||||||
- Become a [translator](https://help.penpot.app/contributing-guide/translations).
|
- Become a [translator](https://help.penpot.app/contributing-guide/translations)
|
||||||
- Give feedback: [Email us](mailto:support@penpot.app).
|
- Give feedback: [Email us](mailto:support@penpot.app)
|
||||||
- **Contribute to Penpot's code:** [Watch this video](https://www.youtube.com/watch?v=TpN0osiY-8k) by Alejandro Alonso, CIO and developer at Penpot, where he gives us a hands-on demo of how to use Penpot’s repository and make changes in both front and back end.
|
- **Contribute to Penpot's code:** [Watch this video](https://www.youtube.com/watch?v=TpN0osiY-8k) by Alejandro Alonso, CIO and developer at Penpot, where he gives us a hands-on demo of how to use Penpot’s repository and make changes in both front and back end
|
||||||
|
|
||||||
To find (almost) everything you need to know on how to contribute to Penpot, refer to the [contributing guide](https://help.penpot.app/contributing-guide/).
|
To find (almost) everything you need to know on how to contribute to Penpot, refer to the [contributing guide](https://help.penpot.app/contributing-guide/).
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://github.com/penpot/penpot/assets/5446186/fea18923-dc06-49be-86ad-c3496a7956e6" alt="Libraries and templates" style="width: 65%;">
|
<img src="https://github.com/penpot/penpot/assets/5446186/fea18923-dc06-49be-86ad-c3496a7956e6" alt="Libraries and templates" style="width: 65%;">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
## Resources ##
|
## Resources ##
|
||||||
|
|
||||||
You can ask and answer questions, have open-ended conversations, and follow along on decisions affecting the project.
|
You can ask and answer questions, have open-ended conversations, and follow along on decisions affecting the project.
|
||||||
@ -155,14 +170,14 @@ You can ask and answer questions, have open-ended conversations, and follow alon
|
|||||||
|
|
||||||
📚 [Dev Diaries](https://penpot.app/dev-diaries.html)
|
📚 [Dev Diaries](https://penpot.app/dev-diaries.html)
|
||||||
|
|
||||||
|
|
||||||
## License ##
|
## License ##
|
||||||
|
|
||||||
```text
|
```
|
||||||
This Source Code Form is subject to the terms of the Mozilla Public
|
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
|
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/.
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
Copyright (c) KALEIDOS INC
|
Copyright (c) KALEIDOS INC
|
||||||
```
|
```
|
||||||
|
|
||||||
Penpot is a Kaleidos’ [open source project](https://kaleidos.net/)
|
Penpot is a Kaleidos’ [open source project](https://kaleidos.net/)
|
||||||
|
|||||||
28
SECURITY.md
28
SECURITY.md
@ -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
7
backend/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/sdks
|
||||||
|
!.yarn/versions
|
||||||
@ -1,259 +0,0 @@
|
|||||||
# Penpot Backend – Agent Instructions
|
|
||||||
|
|
||||||
Clojure backend (RPC) service running on the JVM.
|
|
||||||
|
|
||||||
Uses Integrant for dependency injection, PostgreSQL for storage, and
|
|
||||||
Redis for messaging/caching.
|
|
||||||
|
|
||||||
## General Guidelines
|
|
||||||
|
|
||||||
To ensure consistency across the Penpot JVM stack, all contributions must adhere
|
|
||||||
to these criteria:
|
|
||||||
|
|
||||||
### 1. Testing & Validation
|
|
||||||
|
|
||||||
* **Coverage:** If code is added or modified in `src/`, corresponding
|
|
||||||
tests in `test/backend_tests/` must be added or updated.
|
|
||||||
|
|
||||||
* **Execution:**
|
|
||||||
* **Isolated:** Run `clojure -M:dev:test --focus backend-tests.my-ns-test` for the specific test namespace.
|
|
||||||
* **Regression:** Run `clojure -M:dev:test` to ensure the suite passes without regressions in related functional areas.
|
|
||||||
|
|
||||||
### 2. Code Quality & Formatting
|
|
||||||
|
|
||||||
* **Linting:** All code must pass `clj-kondo` checks (run `pnpm run lint:clj`)
|
|
||||||
* **Formatting:** All the code must pass the formatting check (run `pnpm run
|
|
||||||
check-fmt`). Use `pnpm run fmt` to fix formatting issues. Avoid "dirty"
|
|
||||||
diffs caused by unrelated whitespace changes.
|
|
||||||
* **Type Hinting:** Use explicit JVM type hints (e.g., `^String`, `^long`) in
|
|
||||||
performance-critical paths to avoid reflection overhead.
|
|
||||||
|
|
||||||
## Code Conventions
|
|
||||||
|
|
||||||
### Namespace Overview
|
|
||||||
|
|
||||||
The source is located under `src` directory and this is a general overview of
|
|
||||||
namespaces structure:
|
|
||||||
|
|
||||||
- `app.rpc.commands.*` – RPC command implementations (`auth`, `files`, `teams`, etc.)
|
|
||||||
- `app.http.*` – HTTP routes and middleware
|
|
||||||
- `app.db.*` – Database layer
|
|
||||||
- `app.tasks.*` – Background job tasks
|
|
||||||
- `app.main` – Integrant system setup and entrypoint
|
|
||||||
- `app.loggers` – Internal loggers (auditlog, mattermost, etc.) (not to be confused with `app.common.logging`)
|
|
||||||
|
|
||||||
### RPC
|
|
||||||
|
|
||||||
The RPC methods are implemented using a multimethod-like structure via the
|
|
||||||
`app.util.services` namespace. The main RPC methods are collected under
|
|
||||||
`app.rpc.commands` namespace and exposed under `/api/rpc/command/<cmd-name>`.
|
|
||||||
|
|
||||||
The RPC method accepts POST and GET requests indistinctly and uses the `Accept`
|
|
||||||
header to negotiate the response encoding (which can be Transit — the default —
|
|
||||||
or plain JSON). It also accepts Transit (default) or JSON as input, which should
|
|
||||||
be indicated using the `Content-Type` header.
|
|
||||||
|
|
||||||
The main convention is: use `get-` prefix on RPC name when we want READ
|
|
||||||
operation.
|
|
||||||
|
|
||||||
Example of RPC method definition:
|
|
||||||
|
|
||||||
```clojure
|
|
||||||
(sv/defmethod ::my-command
|
|
||||||
{::rpc/auth true ;; requires auth
|
|
||||||
::doc/added "1.18"
|
|
||||||
::sm/params [:map ...] ;; malli input schema
|
|
||||||
::sm/result [:map ...]} ;; malli output schema
|
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
|
||||||
;; return a plain map or throw
|
|
||||||
{:id (uuid/next)})
|
|
||||||
```
|
|
||||||
|
|
||||||
Look under `src/app/rpc/commands/*.clj` to see more examples.
|
|
||||||
|
|
||||||
### Tests
|
|
||||||
|
|
||||||
Test namespaces match `.*-test$` under `test/`. Config is in `tests.edn`.
|
|
||||||
|
|
||||||
|
|
||||||
### Integrant System
|
|
||||||
|
|
||||||
The `src/app/main.clj` declares the system map. Each key is a component; values
|
|
||||||
are config maps with `::ig/ref` for dependencies. Components implement
|
|
||||||
`ig/init-key` / `ig/halt-key!`.
|
|
||||||
|
|
||||||
|
|
||||||
### Connecting to the Database
|
|
||||||
|
|
||||||
Two PostgreSQL databases are used in this environment:
|
|
||||||
|
|
||||||
| Database | Purpose | Connection string |
|
|
||||||
|---------------|--------------------|----------------------------------------------------|
|
|
||||||
| `penpot` | Development / app | `postgresql://penpot:penpot@postgres/penpot` |
|
|
||||||
| `penpot_test` | Test suite | `postgresql://penpot:penpot@postgres/penpot_test` |
|
|
||||||
|
|
||||||
**Interactive psql session:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# development DB
|
|
||||||
psql "postgresql://penpot:penpot@postgres/penpot"
|
|
||||||
|
|
||||||
# test DB
|
|
||||||
psql "postgresql://penpot:penpot@postgres/penpot_test"
|
|
||||||
```
|
|
||||||
|
|
||||||
**One-shot query (non-interactive):**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
psql "postgresql://penpot:penpot@postgres/penpot" -c "SELECT id, name FROM team LIMIT 5;"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Useful psql meta-commands:**
|
|
||||||
|
|
||||||
```
|
|
||||||
\dt -- list all tables
|
|
||||||
\d <table> -- describe a table (columns, types, constraints)
|
|
||||||
\di -- list indexes
|
|
||||||
\q -- quit
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Migrations table:** Applied migrations are tracked in the `migrations` table
|
|
||||||
> with columns `module`, `step`, and `created_at`. When renaming a migration
|
|
||||||
> logical name, update this table in both databases to match the new name;
|
|
||||||
> otherwise the runner will attempt to re-apply the migration on next startup.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Example: fix a renamed migration entry in the test DB
|
|
||||||
psql "postgresql://penpot:penpot@postgres/penpot_test" \
|
|
||||||
-c "UPDATE migrations SET step = 'new-name' WHERE step = 'old-name';"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database Access (Clojure)
|
|
||||||
|
|
||||||
`app.db` wraps next.jdbc. Queries use a SQL builder that auto-converts kebab-case ↔ snake_case.
|
|
||||||
|
|
||||||
```clojure
|
|
||||||
;; Query helpers
|
|
||||||
(db/get cfg-or-pool :table {:id id}) ; fetch one row (throws if missing)
|
|
||||||
(db/get* cfg-or-pool :table {:id id}) ; fetch one row (returns nil)
|
|
||||||
(db/query cfg-or-pool :table {:team-id team-id}) ; fetch multiple rows
|
|
||||||
(db/insert! cfg-or-pool :table {:name "x" :team-id id}) ; insert
|
|
||||||
(db/update! cfg-or-pool :table {:name "y"} {:id id}) ; update
|
|
||||||
(db/delete! cfg-or-pool :table {:id id}) ; delete
|
|
||||||
|
|
||||||
;; Run multiple statements/queries on single connection
|
|
||||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
|
||||||
(db/insert! conn :table row1)
|
|
||||||
(db/insert! conn :table row2))
|
|
||||||
|
|
||||||
|
|
||||||
;; Transactions
|
|
||||||
(db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
|
||||||
(db/insert! conn :table row)))
|
|
||||||
```
|
|
||||||
|
|
||||||
Almost all methods in the `app.db` namespace accept `pool`, `conn`, or
|
|
||||||
`cfg` as params.
|
|
||||||
|
|
||||||
Migrations live in `src/app/migrations/` as numbered SQL files. They run automatically on startup.
|
|
||||||
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
|
|
||||||
The exception helpers are defined on Common module, and are available under
|
|
||||||
`app.common.exceptions` namespace.
|
|
||||||
|
|
||||||
Example of raising an exception:
|
|
||||||
|
|
||||||
```clojure
|
|
||||||
(ex/raise :type :not-found
|
|
||||||
:code :object-not-found
|
|
||||||
:hint "File does not exist"
|
|
||||||
:file-id id)
|
|
||||||
```
|
|
||||||
|
|
||||||
Common types: `:not-found`, `:validation`, `:authorization`, `:conflict`, `:internal`.
|
|
||||||
|
|
||||||
|
|
||||||
### Performance Macros (`app.common.data.macros`)
|
|
||||||
|
|
||||||
Always prefer these macros over their `clojure.core` equivalents — they provide
|
|
||||||
optimized implementations:
|
|
||||||
|
|
||||||
```clojure
|
|
||||||
(dm/select-keys m [:a :b]) ;; faster than core/select-keys
|
|
||||||
(dm/get-in obj [:a :b :c]) ;; faster than core/get-in
|
|
||||||
(dm/str "a" "b" "c") ;; string concatenation
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
|
|
||||||
`src/app/config.clj` reads `PENPOT_*` environment variables, validated with
|
|
||||||
Malli. Access anywhere via `(cf/get :smtp-host)`. Feature flags: `(cf/flags
|
|
||||||
:enable-smtp)`.
|
|
||||||
|
|
||||||
|
|
||||||
### Background Tasks
|
|
||||||
|
|
||||||
Background tasks live in `src/app/tasks/`. Each task is an Integrant component
|
|
||||||
that exposes a `::handler` key and follows this three-method pattern:
|
|
||||||
|
|
||||||
```clojure
|
|
||||||
(defmethod ig/assert-key ::handler ;; validate config at startup
|
|
||||||
[_ params]
|
|
||||||
(assert (db/pool? (::db/pool params)) "expected a valid database pool"))
|
|
||||||
|
|
||||||
(defmethod ig/expand-key ::handler ;; inject defaults before init
|
|
||||||
[k v]
|
|
||||||
{k (assoc v ::my-option default-value)})
|
|
||||||
|
|
||||||
(defmethod ig/init-key ::handler ;; return the task fn
|
|
||||||
[_ cfg]
|
|
||||||
(fn [_task] ;; receives the task row from the worker
|
|
||||||
(db/tx-run! cfg (fn [{:keys [::db/conn]}]
|
|
||||||
;; … do work …
|
|
||||||
))))
|
|
||||||
```
|
|
||||||
|
|
||||||
**Wiring a new task** requires two changes in `src/app/main.clj`:
|
|
||||||
|
|
||||||
1. **Handler config** – add an entry in `system-config` with the dependencies:
|
|
||||||
|
|
||||||
```clojure
|
|
||||||
:app.tasks.my-task/handler
|
|
||||||
{::db/pool (ig/ref ::db/pool)}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Registry + cron** – register the handler name and schedule it:
|
|
||||||
|
|
||||||
```clojure
|
|
||||||
;; in ::wrk/registry ::wrk/tasks map:
|
|
||||||
:my-task (ig/ref :app.tasks.my-task/handler)
|
|
||||||
|
|
||||||
;; in worker-config ::wrk/cron ::wrk/entries vector:
|
|
||||||
{:cron #penpot/cron "0 0 0 * * ?" ;; daily at midnight
|
|
||||||
:task :my-task}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Useful cron patterns** (Quartz format — six fields: s m h dom mon dow):
|
|
||||||
|
|
||||||
| Expression | Meaning |
|
|
||||||
|------------------------------|--------------------|
|
|
||||||
| `"0 0 0 * * ?"` | Daily at midnight |
|
|
||||||
| `"0 0 */6 * * ?"` | Every 6 hours |
|
|
||||||
| `"0 */5 * * * ?"` | Every 5 minutes |
|
|
||||||
|
|
||||||
**Time helpers** (`app.common.time`):
|
|
||||||
|
|
||||||
```clojure
|
|
||||||
(ct/now) ;; current instant
|
|
||||||
(ct/duration {:hours 1}) ;; java.time.Duration
|
|
||||||
(ct/minus (ct/now) some-duration) ;; subtract duration from instant
|
|
||||||
```
|
|
||||||
|
|
||||||
`db/interval` converts a `Duration` (or millis / string) to a PostgreSQL
|
|
||||||
interval object suitable for use in SQL queries:
|
|
||||||
|
|
||||||
```clojure
|
|
||||||
(db/interval (ct/duration {:hours 1})) ;; → PGInterval "3600.0 seconds"
|
|
||||||
```
|
|
||||||
@ -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"}
|
||||||
@ -28,8 +28,8 @@
|
|||||||
com.google.guava/guava {:mvn/version "33.4.8-jre"}
|
com.google.guava/guava {:mvn/version "33.4.8-jre"}
|
||||||
|
|
||||||
funcool/yetti
|
funcool/yetti
|
||||||
{:git/tag "v11.9"
|
{:git/tag "v11.8"
|
||||||
:git/sha "5fad7a9"
|
:git/sha "1d1b33f"
|
||||||
:git/url "https://github.com/funcool/yetti.git"
|
:git/url "https://github.com/funcool/yetti.git"
|
||||||
:exclusions [org.slf4j/slf4j-api]}
|
:exclusions [org.slf4j/slf4j-api]}
|
||||||
|
|
||||||
@ -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"
|
||||||
|
|||||||
@ -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.31.0+sha512.e3927388bfaa8078ceb79b748ffc1e8274e84d75163e67bc22e06c0d3aed43dd153151cbf11d7f8301ff4acb98c68bdc5cadf6989532801ffafe3b3e4a63c268",
|
"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"
|
||||||
@ -19,9 +19,8 @@
|
|||||||
"ws": "^8.17.0"
|
"ws": "^8.17.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "clj-kondo --parallel --lint ../common/src src/",
|
"fmt:clj:check": "cljfmt check --parallel=false src/ test/",
|
||||||
"check-fmt": "cljfmt check --parallel=true src/ test/",
|
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
|
||||||
"fmt": "cljfmt fix --parallel=true src/ test/",
|
"lint:clj": "clj-kondo --parallel --lint src/"
|
||||||
"test": "clojure -M:dev:test"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
306
backend/pnpm-lock.yaml
generated
306
backend/pnpm-lock.yaml
generated
@ -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: {}
|
|
||||||
@ -1,264 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
|
|
||||||
xmlns:o="urn:schemas-microsoft-com:office:office">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<title>
|
|
||||||
</title>
|
|
||||||
<!--[if !mso]><!-- -->
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<!--<![endif]-->
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<style type="text/css">
|
|
||||||
#outlook a {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
-webkit-text-size-adjust: 100%;
|
|
||||||
-ms-text-size-adjust: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
table,
|
|
||||||
td {
|
|
||||||
border-collapse: collapse;
|
|
||||||
mso-table-lspace: 0pt;
|
|
||||||
mso-table-rspace: 0pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
img {
|
|
||||||
border: 0;
|
|
||||||
height: auto;
|
|
||||||
line-height: 100%;
|
|
||||||
outline: none;
|
|
||||||
text-decoration: none;
|
|
||||||
-ms-interpolation-mode: bicubic;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
display: block;
|
|
||||||
margin: 13px 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<!--[if mso]>
|
|
||||||
<xml>
|
|
||||||
<o:OfficeDocumentSettings>
|
|
||||||
<o:AllowPNG/>
|
|
||||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
|
||||||
</o:OfficeDocumentSettings>
|
|
||||||
</xml>
|
|
||||||
<![endif]-->
|
|
||||||
<!--[if lte mso 11]>
|
|
||||||
<style type="text/css">
|
|
||||||
.mj-outlook-group-fix { width:100% !important; }
|
|
||||||
</style>
|
|
||||||
<![endif]-->
|
|
||||||
<!--[if !mso]><!-->
|
|
||||||
<link href="https://fonts.googleapis.com/css?family=Source%20Sans%20Pro" rel="stylesheet" type="text/css">
|
|
||||||
<style type="text/css">
|
|
||||||
@import url(https://fonts.googleapis.com/css?family=Source%20Sans%20Pro);
|
|
||||||
</style>
|
|
||||||
<!--<![endif]-->
|
|
||||||
<style type="text/css">
|
|
||||||
@media only screen and (min-width:480px) {
|
|
||||||
.mj-column-per-100 {
|
|
||||||
width: 100% !important;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mj-column-px-425 {
|
|
||||||
width: 425px !important;
|
|
||||||
max-width: 425px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<style type="text/css">
|
|
||||||
@media only screen and (max-width:480px) {
|
|
||||||
table.mj-full-width-mobile {
|
|
||||||
width: 100% !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
td.mj-full-width-mobile {
|
|
||||||
width: auto !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body style="background-color:#E5E5E5;">
|
|
||||||
<div style="background-color:#E5E5E5;">
|
|
||||||
<!--[if mso | IE]>
|
|
||||||
<table
|
|
||||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
|
||||||
>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
|
||||||
<![endif]-->
|
|
||||||
<div style="margin:0px auto;max-width:600px;">
|
|
||||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="direction:ltr;font-size:0px;padding:0;text-align:center;">
|
|
||||||
<!--[if mso | IE]>
|
|
||||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
|
|
||||||
<td
|
|
||||||
class="" style="vertical-align:top;width:600px;"
|
|
||||||
>
|
|
||||||
<![endif]-->
|
|
||||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
|
||||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
|
||||||
width="100%">
|
|
||||||
<tr>
|
|
||||||
<td align="left" style="font-size:0px;padding:16px;word-break:break-word;">
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
|
||||||
style="border-collapse:collapse;border-spacing:0px;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="width:97px;">
|
|
||||||
<img height="32" src="{{ public-uri }}/images/email/uxbox-title.png"
|
|
||||||
style="border:0;display:block;outline:none;text-decoration:none;height:32px;width:100%;font-size:13px;"
|
|
||||||
width="97" />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<!--[if mso | IE]>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</table>
|
|
||||||
<![endif]-->
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<!--[if mso | IE]>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<table
|
|
||||||
align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600"
|
|
||||||
>
|
|
||||||
<tr>
|
|
||||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
|
||||||
<![endif]-->
|
|
||||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
|
||||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
|
|
||||||
style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="direction:ltr;font-size:0px;padding:20px 0;text-align:center;">
|
|
||||||
<!--[if mso | IE]>
|
|
||||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
|
||||||
|
|
||||||
<tr>
|
|
||||||
|
|
||||||
<td
|
|
||||||
class="" style="vertical-align:top;width:600px;"
|
|
||||||
>
|
|
||||||
<![endif]-->
|
|
||||||
<div class="mj-column-per-100 mj-outlook-group-fix"
|
|
||||||
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
|
|
||||||
width="100%">
|
|
||||||
<tr>
|
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
|
||||||
<div
|
|
||||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
|
||||||
Hi{% if user-name %} {{ user-name|abbreviate:25 }}{% endif %},
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
|
||||||
<div
|
|
||||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
|
||||||
<b>{{invited-by|abbreviate:25}}</b> sent you an invitation to join the organization:
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
|
||||||
<div
|
|
||||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
|
||||||
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="20" height="20" style="display:inline-block;vertical-align:middle;">
|
|
||||||
<tr>
|
|
||||||
<td width="20" height="20" align="center" valign="middle"
|
|
||||||
background="{{org-logo}}"
|
|
||||||
style="width:20px;height:20px;text-align:center;font-weight:bold;font-size:9px;line-height:20px;color:#ffffff;background-size:cover;background-position:center;background-repeat:no-repeat;border-radius: 50%;color:black">
|
|
||||||
{{org-initials}}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<span style="display:inline-block; vertical-align: middle;padding-left:5px;height:20px;line-height: 20px;">
|
|
||||||
“{{ organization-name|abbreviate:25 }}”
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="center" vertical-align="middle"
|
|
||||||
style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
|
||||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
|
|
||||||
style="border-collapse:separate;line-height:100%;">
|
|
||||||
<tr>
|
|
||||||
<td align="center" bgcolor="#6911d4" role="presentation"
|
|
||||||
style="border:none;border-radius:8px;cursor:auto;mso-padding-alt:10px 25px;background:#6911d4;"
|
|
||||||
valign="middle">
|
|
||||||
<a href="{{ public-uri }}/#/auth/verify-token?token={{token}}"
|
|
||||||
style="display:inline-block;background:#6911d4;color:#FFFFFF;font-family:Source Sans Pro, sans-serif;font-size:16px;font-weight:normal;line-height:120%;margin:0;text-decoration:none;text-transform:none;padding:10px 25px;mso-padding-alt:0px;border-radius:8px;"
|
|
||||||
target="_blank"> ACCEPT INVITE </a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
|
||||||
<div
|
|
||||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
|
||||||
Enjoy!</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
|
||||||
<div
|
|
||||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
|
||||||
The Penpot team.</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<!--[if mso | IE]>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
</table>
|
|
||||||
<![endif]-->
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% include "app/email/includes/footer.html" %}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{{invited-by|abbreviate:25}} has invited you to join the organization “{{ organization-name|abbreviate:25 }}”
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
Hello!
|
|
||||||
|
|
||||||
{{invited-by|abbreviate:25}} has invited you to join the organization “{{ organization-name|abbreviate:25 }}”.
|
|
||||||
|
|
||||||
Accept invitation using this link:
|
|
||||||
|
|
||||||
{{ public-uri }}/#/auth/verify-token?token={{token}}
|
|
||||||
|
|
||||||
Enjoy!
|
|
||||||
The Penpot team.
|
|
||||||
@ -186,8 +186,7 @@
|
|||||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||||
<div
|
<div
|
||||||
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
style="font-family:Source Sans Pro, sans-serif;font-size:16px;line-height:150%;text-align:left;color:#000000;">
|
||||||
{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}”{% if organization %}
|
{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}”.</div>
|
||||||
part of the organization “{{ organization|abbreviate:25 }}”{% endif %}.</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
Hello!
|
Hello!
|
||||||
|
|
||||||
{{invited-by|abbreviate:25}} has invited you to join the team "{{ team|abbreviate:25 }}"{% if organization %}, part of the organization "{{ organization|abbreviate:25 }}"{% endif %}.
|
{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}”.
|
||||||
|
|
||||||
Accept invitation using this link:
|
Accept invitation using this link:
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 %}
|
||||||
|
|||||||
@ -7,12 +7,9 @@ penpot - error list
|
|||||||
{% 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>
|
|
||||||
<a class="{% if version = 4 %}strong{% endif %}" href="?version=4">[FRONTEND ERRORS]</a>
|
|
||||||
<a class="{% if version = 5 %}strong{% endif %}" href="?version=5">[RLIMIT REPORTS]</a>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<main class="horizontal-list">
|
<main class="horizontal-list">
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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="#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">Origin:</span> <br/> {{origin}}</h2>
|
|
||||||
<h2><span class="not-important">HREF:</span> <br/> {{href}}</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 %}
|
|
||||||
@ -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 %}
|
|
||||||
@ -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 {
|
||||||
|
|||||||
@ -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"]]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +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_NEXUS_SHARED_KEY=super-secret-nexus-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
|
||||||
|
|
||||||
@ -19,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 \
|
||||||
@ -45,10 +38,6 @@ export PENPOT_FLAGS="\
|
|||||||
enable-redis-cache \
|
enable-redis-cache \
|
||||||
enable-subscriptions";
|
enable-subscriptions";
|
||||||
|
|
||||||
# Uncomment for nexus integration testing
|
|
||||||
# export PENPOT_FLAGS="$PENPOT_FLAGS enable-audit-log-archive";
|
|
||||||
# export PENPOT_AUDIT_LOG_ARCHIVE_URI="http://localhost:6070/api/audit";
|
|
||||||
|
|
||||||
# Default deletion delay for devenv
|
# Default deletion delay for devenv
|
||||||
export PENPOT_DELETION_DELAY="24h"
|
export PENPOT_DELETION_DELAY="24h"
|
||||||
|
|
||||||
@ -66,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 \
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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};
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
@ -401,9 +412,8 @@
|
|||||||
|
|
||||||
(defn- parse-attr-path
|
(defn- parse-attr-path
|
||||||
[provider path]
|
[provider path]
|
||||||
(let [separator (if (str/includes? path "__") "__" ".")
|
(let [[fitem & items] (str/split path "__")]
|
||||||
[fitem & items] (str/split path separator)]
|
(into [(keyword (:type provider) fitem)] (map keyword) items)))
|
||||||
(into [(keyword (:type provider) (str/kebab fitem))] (map keyword) items)))
|
|
||||||
|
|
||||||
(defn- build-redirect-uri
|
(defn- build-redirect-uri
|
||||||
[]
|
[]
|
||||||
@ -424,7 +434,7 @@
|
|||||||
|
|
||||||
(defn- qualify-prop-key
|
(defn- qualify-prop-key
|
||||||
[provider k]
|
[provider k]
|
||||||
(keyword (:type provider) (-> k name str/kebab)))
|
(keyword (:type provider) (name k)))
|
||||||
|
|
||||||
(defn- qualify-props
|
(defn- qualify-props
|
||||||
[provider props]
|
[provider props]
|
||||||
@ -449,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))
|
||||||
|
|
||||||
@ -502,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))}
|
||||||
@ -548,28 +558,15 @@
|
|||||||
(def ^:private valid-info?
|
(def ^:private valid-info?
|
||||||
(sm/validator schema:info))
|
(sm/validator schema:info))
|
||||||
|
|
||||||
(defn- select-user-info-source
|
|
||||||
"Normalise the provider's configured user-info source into a keyword the
|
|
||||||
dispatch below can match. The raw value comes from config as a string
|
|
||||||
per the malli schema in `app.config` (`\"token\"`, `\"userinfo\"`, or
|
|
||||||
`\"auto\"`) and from hard-coded per-provider maps as strings as well;
|
|
||||||
any unrecognised or missing value falls back to `:auto` (prefer claims,
|
|
||||||
use userinfo as fallback)."
|
|
||||||
[source]
|
|
||||||
(case source
|
|
||||||
"token" :token
|
|
||||||
"userinfo" :userinfo
|
|
||||||
:auto))
|
|
||||||
|
|
||||||
(defn- get-info
|
(defn- get-info
|
||||||
[cfg provider state code]
|
[cfg provider state code]
|
||||||
(let [tdata (fetch-access-token cfg provider code)
|
(let [tdata (fetch-access-token cfg provider code)
|
||||||
claims (get-id-token-claims provider tdata)
|
claims (get-id-token-claims provider tdata)
|
||||||
|
|
||||||
info (case (select-user-info-source (get provider :user-info-source))
|
info (case (get provider :user-info-source)
|
||||||
:token (dissoc claims :exp :iss :iat :aud :sid)
|
:token (dissoc claims :exp :iss :iat :aud :sub :sid)
|
||||||
:userinfo (fetch-user-info cfg provider tdata)
|
:userinfo (fetch-user-info cfg provider tdata)
|
||||||
:auto (or (some-> claims (dissoc :exp :iss :iat :aud :sid))
|
(or (some-> claims (dissoc :exp :iss :iat :aud :sub :sid))
|
||||||
(fetch-user-info cfg provider tdata)))
|
(fetch-user-info cfg provider tdata)))
|
||||||
|
|
||||||
info (process-user-info provider tdata info)]
|
info (process-user-info provider tdata info)]
|
||||||
|
|||||||
@ -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}))
|
||||||
|
|||||||
@ -40,8 +40,8 @@
|
|||||||
[promesa.util :as pu]
|
[promesa.util :as pu]
|
||||||
[yetti.adapter :as yt])
|
[yetti.adapter :as yt])
|
||||||
(:import
|
(:import
|
||||||
com.github.luben.zstd.ZstdInputStream
|
|
||||||
com.github.luben.zstd.ZstdIOException
|
com.github.luben.zstd.ZstdIOException
|
||||||
|
com.github.luben.zstd.ZstdInputStream
|
||||||
com.github.luben.zstd.ZstdOutputStream
|
com.github.luben.zstd.ZstdOutputStream
|
||||||
java.io.DataInputStream
|
java.io.DataInputStream
|
||||||
java.io.DataOutputStream
|
java.io.DataOutputStream
|
||||||
|
|||||||
@ -873,8 +873,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))))
|
||||||
[]
|
[]
|
||||||
|
|||||||
@ -82,10 +82,7 @@
|
|||||||
:initial-project-skey "initial-project"
|
:initial-project-skey "initial-project"
|
||||||
|
|
||||||
;; time to avoid email sending after profile modification
|
;; time to avoid email sending after profile modification
|
||||||
:email-verify-threshold "15m"
|
:email-verify-threshold "15m"})
|
||||||
|
|
||||||
:quotes-upload-sessions-per-profile 5
|
|
||||||
:quotes-upload-chunks-per-session 20})
|
|
||||||
|
|
||||||
(def schema:config
|
(def schema:config
|
||||||
(do #_sm/optional-keys
|
(do #_sm/optional-keys
|
||||||
@ -101,12 +98,10 @@
|
|||||||
[:http-server-port {:optional true} ::sm/int]
|
[:http-server-port {:optional true} ::sm/int]
|
||||||
[:http-server-host {:optional true} :string]
|
[:http-server-host {:optional true} :string]
|
||||||
[:http-server-max-body-size {:optional true} ::sm/int]
|
[:http-server-max-body-size {:optional true} ::sm/int]
|
||||||
|
[:http-server-max-multipart-body-size {:optional true} ::sm/int]
|
||||||
[: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]
|
|
||||||
[:nexus-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]
|
||||||
@ -157,8 +152,6 @@
|
|||||||
[:quotes-snapshots-per-team {:optional true} ::sm/int]
|
[:quotes-snapshots-per-team {:optional true} ::sm/int]
|
||||||
[:quotes-team-access-requests-per-team {:optional true} ::sm/int]
|
[:quotes-team-access-requests-per-team {:optional true} ::sm/int]
|
||||||
[:quotes-team-access-requests-per-requester {:optional true} ::sm/int]
|
[:quotes-team-access-requests-per-requester {:optional true} ::sm/int]
|
||||||
[:quotes-upload-sessions-per-profile {:optional true} ::sm/int]
|
|
||||||
[:quotes-upload-chunks-per-session {:optional true} ::sm/int]
|
|
||||||
|
|
||||||
[:auth-token-cookie-name {:optional true} :string]
|
[:auth-token-cookie-name {:optional true} :string]
|
||||||
[:auth-token-cookie-max-age {:optional true} ::ct/duration]
|
[:auth-token-cookie-max-age {:optional true} ::ct/duration]
|
||||||
@ -232,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]
|
||||||
@ -332,7 +323,7 @@
|
|||||||
|
|
||||||
(defn logging-context
|
(defn logging-context
|
||||||
[]
|
[]
|
||||||
{:backend/version (:full version)})
|
{:version/backend (:full version)})
|
||||||
|
|
||||||
;; Set value for all new threads bindings.
|
;; Set value for all new threads bindings.
|
||||||
(alter-var-root #'*assert* (constantly (contains? flags :backend-asserts)))
|
(alter-var-root #'*assert* (constantly (contains? flags :backend-asserts)))
|
||||||
|
|||||||
@ -36,11 +36,11 @@
|
|||||||
java.sql.Connection
|
java.sql.Connection
|
||||||
java.sql.PreparedStatement
|
java.sql.PreparedStatement
|
||||||
java.sql.Savepoint
|
java.sql.Savepoint
|
||||||
|
org.postgresql.PGConnection
|
||||||
org.postgresql.geometric.PGpoint
|
org.postgresql.geometric.PGpoint
|
||||||
org.postgresql.jdbc.PgArray
|
org.postgresql.jdbc.PgArray
|
||||||
org.postgresql.largeobject.LargeObject
|
org.postgresql.largeobject.LargeObject
|
||||||
org.postgresql.largeobject.LargeObjectManager
|
org.postgresql.largeobject.LargeObjectManager
|
||||||
org.postgresql.PGConnection
|
|
||||||
org.postgresql.util.PGInterval
|
org.postgresql.util.PGInterval
|
||||||
org.postgresql.util.PGobject))
|
org.postgresql.util.PGobject))
|
||||||
|
|
||||||
|
|||||||
@ -22,13 +22,13 @@
|
|||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[integrant.core :as ig])
|
[integrant.core :as ig])
|
||||||
(:import
|
(:import
|
||||||
|
jakarta.mail.Message$RecipientType
|
||||||
|
jakarta.mail.Session
|
||||||
|
jakarta.mail.Transport
|
||||||
jakarta.mail.internet.InternetAddress
|
jakarta.mail.internet.InternetAddress
|
||||||
jakarta.mail.internet.MimeBodyPart
|
jakarta.mail.internet.MimeBodyPart
|
||||||
jakarta.mail.internet.MimeMessage
|
jakarta.mail.internet.MimeMessage
|
||||||
jakarta.mail.internet.MimeMultipart
|
jakarta.mail.internet.MimeMultipart
|
||||||
jakarta.mail.Message$RecipientType
|
|
||||||
jakarta.mail.Session
|
|
||||||
jakarta.mail.Transport
|
|
||||||
java.util.Properties))
|
java.util.Properties))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
@ -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))
|
||||||
@ -412,21 +414,6 @@
|
|||||||
:id ::invite-to-team
|
:id ::invite-to-team
|
||||||
:schema schema:invite-to-team))
|
:schema schema:invite-to-team))
|
||||||
|
|
||||||
(def ^:private schema:invite-to-org
|
|
||||||
[:map
|
|
||||||
[:invited-by ::sm/text]
|
|
||||||
[:organization-name ::sm/text]
|
|
||||||
[:org-initials ::sm/text]
|
|
||||||
[:org-logo ::sm/uri]
|
|
||||||
[:user-name [:maybe ::sm/text]]
|
|
||||||
[:token ::sm/text]])
|
|
||||||
|
|
||||||
(def invite-to-org
|
|
||||||
"Org member invitation email."
|
|
||||||
(template-factory
|
|
||||||
:id ::invite-to-org
|
|
||||||
:schema schema:invite-to-org))
|
|
||||||
|
|
||||||
(def ^:private schema:join-team
|
(def ^:private schema:join-team
|
||||||
[:map
|
[:map
|
||||||
[:invited-by ::sm/text]
|
[:invited-by ::sm/text]
|
||||||
|
|||||||
@ -36,18 +36,10 @@
|
|||||||
:cause cause)))))
|
:cause cause)))))
|
||||||
|
|
||||||
(defn contains?
|
(defn contains?
|
||||||
"Check if email is in the blacklist. Also matches subdomains: if
|
"Check if email is in the blacklist."
|
||||||
'somedomain.com' is blacklisted, 'xxx@foo.somedomain.com' will also
|
|
||||||
be rejected."
|
|
||||||
[{:keys [::email/blacklist]} email]
|
[{:keys [::email/blacklist]} email]
|
||||||
(let [[_ domain] (str/split email "@" 2)
|
(let [[_ domain] (str/split email "@" 2)]
|
||||||
parts (str/split (str/lower domain) #"\.")]
|
(c/contains? blacklist (str/lower domain))))
|
||||||
(loop [parts parts]
|
|
||||||
(if (empty? parts)
|
|
||||||
false
|
|
||||||
(if (c/contains? blacklist (str/join "." parts))
|
|
||||||
true
|
|
||||||
(recur (rest parts)))))))
|
|
||||||
|
|
||||||
(defn enabled?
|
(defn enabled?
|
||||||
"Check if the blacklist is enabled"
|
"Check if the blacklist is enabled"
|
||||||
|
|||||||
@ -112,9 +112,8 @@
|
|||||||
THEN (c.deleted_at IS NULL OR c.deleted_at >= ?::timestamptz)
|
THEN (c.deleted_at IS NULL OR c.deleted_at >= ?::timestamptz)
|
||||||
END"))
|
END"))
|
||||||
|
|
||||||
(defn get-snapshot-data
|
(defn- get-snapshot
|
||||||
"Get a fully decoded snapshot for read-only preview or restoration.
|
"Get snapshot with decoded data"
|
||||||
Returns the snapshot map with decoded :data field."
|
|
||||||
[cfg file-id snapshot-id]
|
[cfg file-id snapshot-id]
|
||||||
(let [now (ct/now)]
|
(let [now (ct/now)]
|
||||||
(->> (db/get-with-sql cfg [sql:get-snapshot file-id snapshot-id now]
|
(->> (db/get-with-sql cfg [sql:get-snapshot file-id snapshot-id now]
|
||||||
@ -139,7 +138,6 @@
|
|||||||
c.deleted_at
|
c.deleted_at
|
||||||
FROM snapshots1 AS c
|
FROM snapshots1 AS c
|
||||||
WHERE c.file_id = ?
|
WHERE c.file_id = ?
|
||||||
ORDER BY c.created_at DESC
|
|
||||||
), snapshots3 AS (
|
), snapshots3 AS (
|
||||||
(SELECT * FROM snapshots2
|
(SELECT * FROM snapshots2
|
||||||
WHERE created_by = 'system'
|
WHERE created_by = 'system'
|
||||||
@ -152,7 +150,8 @@
|
|||||||
AND deleted_at IS NULL
|
AND deleted_at IS NULL
|
||||||
LIMIT 500)
|
LIMIT 500)
|
||||||
)
|
)
|
||||||
SELECT * FROM snapshots3;"))
|
SELECT * FROM snapshots3
|
||||||
|
ORDER BY created_at DESC"))
|
||||||
|
|
||||||
(defn get-visible-snapshots
|
(defn get-visible-snapshots
|
||||||
"Return a list of snapshots fecheable from the API, it has a limited
|
"Return a list of snapshots fecheable from the API, it has a limited
|
||||||
@ -327,7 +326,7 @@
|
|||||||
(sto/resolve cfg {::db/reuse-conn true})
|
(sto/resolve cfg {::db/reuse-conn true})
|
||||||
|
|
||||||
snapshot
|
snapshot
|
||||||
(get-snapshot-data cfg file-id snapshot-id)]
|
(get-snapshot cfg file-id snapshot-id)]
|
||||||
|
|
||||||
(when-not snapshot
|
(when-not snapshot
|
||||||
(ex/raise :type :not-found
|
(ex/raise :type :not-found
|
||||||
|
|||||||
@ -42,8 +42,8 @@
|
|||||||
(def default-params
|
(def default-params
|
||||||
{::port 6060
|
{::port 6060
|
||||||
::host "0.0.0.0"
|
::host "0.0.0.0"
|
||||||
::max-body-size 367001600 ; default 350 MiB
|
::max-body-size 31457280 ; default 30 MiB
|
||||||
})
|
::max-multipart-body-size 367001600}) ; default 350 MiB
|
||||||
|
|
||||||
(defmethod ig/expand-key ::server
|
(defmethod ig/expand-key ::server
|
||||||
[k v]
|
[k v]
|
||||||
@ -56,6 +56,7 @@
|
|||||||
[::io-threads {:optional true} ::sm/int]
|
[::io-threads {:optional true} ::sm/int]
|
||||||
[::max-worker-threads {:optional true} ::sm/int]
|
[::max-worker-threads {:optional true} ::sm/int]
|
||||||
[::max-body-size {:optional true} ::sm/int]
|
[::max-body-size {:optional true} ::sm/int]
|
||||||
|
[::max-multipart-body-size {:optional true} ::sm/int]
|
||||||
[::router {:optional true} [:fn r/router?]]
|
[::router {:optional true} [:fn r/router?]]
|
||||||
[::handler {:optional true} ::sm/fn]])
|
[::handler {:optional true} ::sm/fn]])
|
||||||
|
|
||||||
@ -78,7 +79,7 @@
|
|||||||
{:http/port port
|
{:http/port port
|
||||||
:http/host host
|
:http/host host
|
||||||
:http/max-body-size (::max-body-size cfg)
|
:http/max-body-size (::max-body-size cfg)
|
||||||
:http/max-multipart-body-size (::max-body-size cfg)
|
:http/max-multipart-body-size (::max-multipart-body-size cfg)
|
||||||
:xnio/direct-buffers false
|
:xnio/direct-buffers false
|
||||||
:xnio/io-threads (::io-threads cfg)
|
:xnio/io-threads (::io-threads cfg)
|
||||||
:xnio/max-worker-threads (::max-worker-threads cfg)
|
:xnio/max-worker-threads (::max-worker-threads cfg)
|
||||||
|
|||||||
@ -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]
|
||||||
|
|||||||
@ -31,6 +31,7 @@
|
|||||||
[app.srepl.main :as srepl]
|
[app.srepl.main :as srepl]
|
||||||
[app.storage :as-alias sto]
|
[app.storage :as-alias sto]
|
||||||
[app.storage.tmp :as tmp]
|
[app.storage.tmp :as tmp]
|
||||||
|
[app.util.blob :as blob]
|
||||||
[app.util.template :as tmpl]
|
[app.util.template :as tmpl]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[datoteka.io :as io]
|
[datoteka.io :as io]
|
||||||
@ -48,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")
|
||||||
@ -70,7 +68,8 @@
|
|||||||
|
|
||||||
(defn- get-resolved-file
|
(defn- get-resolved-file
|
||||||
[cfg file-id]
|
[cfg file-id]
|
||||||
(bfc/get-file cfg file-id :migrate? false :decode? false))
|
(some-> (bfc/get-file cfg file-id :migrate? false)
|
||||||
|
(update :data blob/encode)))
|
||||||
|
|
||||||
(defn prepare-download
|
(defn prepare-download
|
||||||
[file filename]
|
[file filename]
|
||||||
@ -230,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"
|
||||||
@ -261,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"}}))
|
||||||
|
|
||||||
@ -468,15 +448,14 @@
|
|||||||
(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"}}))))
|
||||||
|
|
||||||
@ -516,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)))))
|
||||||
|
|||||||
@ -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 _ _]
|
||||||
@ -220,14 +220,12 @@
|
|||||||
(assoc :hint (ex-message error)))}))))
|
(assoc :hint (ex-message error)))}))))
|
||||||
|
|
||||||
(defmethod handle-exception java.io.IOException
|
(defmethod handle-exception java.io.IOException
|
||||||
[cause request _]
|
[cause _ _]
|
||||||
(binding [l/*context* (request->context request)]
|
|
||||||
(l/wrn :hint "io exception" :cause cause)
|
(l/wrn :hint "io exception" :cause cause)
|
||||||
{::yres/status 500
|
{::yres/status 500
|
||||||
::yres/body {:type :server-error
|
::yres/body {:type :server-error
|
||||||
:code :io-exception
|
:code :io-exception
|
||||||
:hint (ex-message cause)
|
:hint (ex-message cause)}})
|
||||||
:path (:path request)}}))
|
|
||||||
|
|
||||||
(defmethod handle-exception java.util.concurrent.CompletionException
|
(defmethod handle-exception java.util.concurrent.CompletionException
|
||||||
[cause request _]
|
[cause request _]
|
||||||
|
|||||||
@ -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,25 +49,13 @@
|
|||||||
(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)
|
||||||
|
(get props :management-key))]
|
||||||
|
|
||||||
|
["" {:middleware [[mw/shared-key-auth management-key]
|
||||||
[default-system cfg]
|
[default-system cfg]
|
||||||
[transaction]]}
|
[transaction]]}
|
||||||
["/authenticate"
|
["/authenticate"
|
||||||
@ -82,7 +70,7 @@
|
|||||||
["/update-customer"
|
["/update-customer"
|
||||||
{:handler update-customer
|
{:handler update-customer
|
||||||
:allowed-methods #{:post}
|
:allowed-methods #{:post}
|
||||||
:transaction true}]])
|
:transaction true}]]))
|
||||||
|
|
||||||
;; ---- HELPERS
|
;; ---- HELPERS
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
(let [shared-key (if (string? shared-key)
|
||||||
|
shared-key
|
||||||
|
(bc/bytes->b64-str shared-key true))]
|
||||||
(fn [request]
|
(fn [request]
|
||||||
(if-let [[key-id key] (some-> (yreq/get-header request "x-shared-key")
|
(let [key (yreq/get-header request "x-shared-key")]
|
||||||
(str/split #"\s+" 2))]
|
(if (= key shared-key)
|
||||||
(let [key-id (-> key-id str/lower keyword)]
|
(handler request)
|
||||||
(if (and (string? key)
|
{::yres/status 403}))))
|
||||||
(contains? keys key-id)
|
|
||||||
(= key (get keys key-id)))
|
|
||||||
(-> request
|
|
||||||
(assoc ::http/auth-key-id key-id)
|
|
||||||
(handler))
|
|
||||||
{::yres/status 403}))
|
|
||||||
{::yres/status 403}))
|
|
||||||
(fn [_ _]
|
(fn [_ _]
|
||||||
{::yres/status 403})))
|
{::yres/status 403})))
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,14 +112,12 @@
|
|||||||
;; 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.
|
||||||
|
|
||||||
(def ^:private schema:event
|
(def ^:private schema:event
|
||||||
[:map {:title "AuditEvent"}
|
[:map {:title "event"}
|
||||||
[::type ::sm/text]
|
[::type ::sm/text]
|
||||||
[::name ::sm/text]
|
[::name ::sm/text]
|
||||||
[::profile-id ::sm/uuid]
|
[::profile-id ::sm/uuid]
|
||||||
@ -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)))))))
|
||||||
|
|||||||
@ -10,11 +10,14 @@
|
|||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.common.transit :as t]
|
[app.common.transit :as t]
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.http.client :as http]
|
[app.http.client :as http]
|
||||||
[app.setup :as-alias setup]
|
[app.setup :as-alias setup]
|
||||||
|
[app.tokens :as tokens]
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
|
[lambdaisland.uri :as u]
|
||||||
[promesa.exec :as px]))
|
[promesa.exec :as px]))
|
||||||
|
|
||||||
;; This is a task responsible to send the accumulated events to
|
;; This is a task responsible to send the accumulated events to
|
||||||
@ -49,18 +52,19 @@
|
|||||||
|
|
||||||
(defn- send!
|
(defn- send!
|
||||||
[{:keys [::uri] :as cfg} events]
|
[{:keys [::uri] :as cfg} events]
|
||||||
(let [skey (-> cfg ::setup/shared-keys :nexus)
|
(let [token (tokens/generate cfg
|
||||||
|
{:iss "authentication"
|
||||||
|
:uid uuid/zero})
|
||||||
body (t/encode {:events events})
|
body (t/encode {:events events})
|
||||||
headers {"content-type" "application/transit+json"
|
headers {"content-type" "application/transit+json"
|
||||||
"origin" (str (cf/get :public-uri))
|
"origin" (str (cf/get :public-uri))
|
||||||
"x-shared-key" (str "nexus " skey)}
|
"cookie" (u/map->query-string {:auth-token token})}
|
||||||
params {:uri uri
|
params {:uri uri
|
||||||
:timeout 12000
|
:timeout 12000
|
||||||
:method :post
|
:method :post
|
||||||
:headers headers
|
:headers headers
|
||||||
:body body}
|
:body body}
|
||||||
resp (http/req! cfg params)]
|
resp (http/req! cfg params)]
|
||||||
|
|
||||||
(if (= (:status resp) 204)
|
(if (= (:status resp) 204)
|
||||||
true
|
true
|
||||||
(do
|
(do
|
||||||
@ -81,7 +85,7 @@
|
|||||||
(def ^:private sql:get-audit-log-chunk
|
(def ^:private sql:get-audit-log-chunk
|
||||||
"SELECT *
|
"SELECT *
|
||||||
FROM audit_log
|
FROM audit_log
|
||||||
WHERE archived_at IS NULL
|
WHERE archived_at is null
|
||||||
ORDER BY created_at ASC
|
ORDER BY created_at ASC
|
||||||
LIMIT 128
|
LIMIT 128
|
||||||
FOR UPDATE
|
FOR UPDATE
|
||||||
@ -105,7 +109,7 @@
|
|||||||
(def ^:private schema:handler-params
|
(def ^:private schema:handler-params
|
||||||
[:map
|
[:map
|
||||||
::db/pool
|
::db/pool
|
||||||
::setup/shared-keys
|
::setup/props
|
||||||
::http/client])
|
::http/client])
|
||||||
|
|
||||||
(defmethod ig/assert-key ::handler
|
(defmethod ig/assert-key ::handler
|
||||||
|
|||||||
@ -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,30 +28,28 @@
|
|||||||
(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))
|
||||||
|
|
||||||
|
(let [data (ex-data cause)
|
||||||
ctx (-> context
|
ctx (-> context
|
||||||
(assoc :backend/tenant (cf/get :tenant))
|
(assoc :tenant (cf/get :tenant))
|
||||||
(assoc :backend/host (cf/get :host))
|
(assoc :host (cf/get :host))
|
||||||
(assoc :backend/public-uri (str (cf/get :public-uri)))
|
(assoc :public-uri (str (cf/get :public-uri)))
|
||||||
(assoc :backend/version (:full cf/version))
|
|
||||||
(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))]
|
||||||
@ -80,95 +76,21 @@
|
|||||||
{: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)
|
|
||||||
:src "logging"
|
|
||||||
:uri (str uri "/dbg/error/" id))
|
: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))
|
|
||||||
:origin (::audit/name record)
|
|
||||||
:href (get props :href)
|
|
||||||
: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))))
|
||||||
|
|
||||||
@ -178,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))
|
||||||
|
|
||||||
|
(px/thread {:name "penpot/database-reporter"}
|
||||||
(l/info :hint "initializing database error persistence")
|
(l/info :hint "initializing database error persistence")
|
||||||
(try
|
(try
|
||||||
(loop []
|
(loop []
|
||||||
(when-let [item (sp/take! input)]
|
(when-let [record (sp/take! input)]
|
||||||
(cond
|
(handle-event cfg record)
|
||||||
(::l/id item)
|
|
||||||
(handle-log-record cfg item)
|
|
||||||
|
|
||||||
(::audit/id item)
|
|
||||||
(handle-audit-event cfg item)
|
|
||||||
|
|
||||||
(::rlimit/id item)
|
|
||||||
(handle-rlimit-event cfg item)
|
|
||||||
|
|
||||||
:else
|
|
||||||
(l/warn :hint "received unexpected item" :item item))
|
|
||||||
|
|
||||||
(recur)))
|
(recur)))
|
||||||
|
|
||||||
(catch InterruptedException _
|
(catch InterruptedException _
|
||||||
(l/debug :hint "reporter interrupted"))
|
(l/debug :hint "reporter interrupted"))
|
||||||
(catch Throwable cause
|
(catch Throwable cause
|
||||||
(l/error :hint "unexpected error" :cause cause))
|
(l/error :hint "unexpected error" :cause cause))
|
||||||
(finally
|
(finally
|
||||||
(l/info :hint "reporter terminated"))))]
|
(sp/close! input)
|
||||||
|
(remove-watch l/log-record ::reporter)
|
||||||
(add-watch l/log-record ::reporter
|
(l/info :hint "reporter terminated"))))))
|
||||||
(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)))
|
|
||||||
|
|
||||||
|
|||||||
@ -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]
|
||||||
@ -23,34 +20,24 @@
|
|||||||
(defonce enabled (atom true))
|
(defonce enabled (atom true))
|
||||||
|
|
||||||
(defn- send-mattermost-notification!
|
(defn- send-mattermost-notification!
|
||||||
[cfg {:keys [id] :as report}]
|
[cfg {:keys [id public-uri] :as report}]
|
||||||
(let [type (get report :type)
|
|
||||||
text (str "#" type " | " (get report :hint) "\n"
|
|
||||||
(when id
|
|
||||||
(str (u/join (cf/get :public-uri) "/dbg/error/" id) " "))
|
|
||||||
|
|
||||||
|
|
||||||
|
(let [text (str "Exception: " public-uri "/dbg/error/" id " "
|
||||||
(when-let [pid (:profile-id report)]
|
(when-let [pid (:profile-id report)]
|
||||||
(if (uuid? pid)
|
(str "(pid: #uuid-" pid ")"))
|
||||||
(str "(pid: #uuid-" pid ")")
|
|
||||||
(str "(pid: #ip-" pid ")")))
|
|
||||||
"\n"
|
"\n"
|
||||||
"- host: #" (:host report) "\n"
|
"- host: #" (:host report) "\n"
|
||||||
"- tenant: #" (:tenant report) "\n"
|
"- tenant: #" (:tenant report) "\n"
|
||||||
"- origin: #" (:origin report) "\n"
|
"- logger: #" (:logger report) "\n"
|
||||||
(when-let [href (get report :href)]
|
"- request-path: `" (:request-path report) "`\n"
|
||||||
(str "- href: `" href "`\n"))
|
"- frontend-version: `" (:frontend-version report) "`\n"
|
||||||
(when-let [version (get report :frontend-version)]
|
"- backend-version: `" (:backend-version report) "`\n"
|
||||||
(str "- frontend-version: `" version "`\n"))
|
|
||||||
(when-let [version (get report :backend-version)]
|
|
||||||
(str "- backend-version: `" version "`\n"))
|
|
||||||
"\n"
|
"\n"
|
||||||
(when-let [info (:info report)]
|
"```\n"
|
||||||
(str "```\n" info "```"))
|
|
||||||
(when-let [trace (:trace report)]
|
|
||||||
(str "```\n"
|
|
||||||
"Trace:\n"
|
"Trace:\n"
|
||||||
trace
|
(:trace report)
|
||||||
"```")))
|
"```")
|
||||||
|
|
||||||
resp (http/req! cfg
|
resp (http/req! cfg
|
||||||
{:uri (cf/get :error-report-webhook)
|
{:uri (cf/get :error-report-webhook)
|
||||||
@ -63,70 +50,28 @@
|
|||||||
(l/warn :hint "error on sending data"
|
(l/warn :hint "error on sending data"
|
||||||
:response (pr-str resp)))))
|
:response (pr-str resp)))))
|
||||||
|
|
||||||
(defn- log-record->report
|
(defn 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 [public-uri (cf/get :public-uri)]
|
|
||||||
{:id id
|
{:id id
|
||||||
:type "exception"
|
|
||||||
:origin "logging"
|
|
||||||
:hint (or (some-> cause ex-message) @message)
|
|
||||||
:tenant (cf/get :tenant)
|
:tenant (cf/get :tenant)
|
||||||
:host (cf/get :host)
|
:host (cf/get :host)
|
||||||
:backend-version (:full cf/version)
|
:public-uri (cf/get :public-uri)
|
||||||
:frontend-version (:frontend/version context)
|
:backend-version (or (:version/backend context) (:full cf/version))
|
||||||
|
:frontend-version (:version/frontend context)
|
||||||
:profile-id (:request/profile-id context)
|
:profile-id (:request/profile-id context)
|
||||||
:href (-> public-uri
|
:request-path (:request/path context)
|
||||||
(assoc :path (:request/path context))
|
:logger (::l/logger record)
|
||||||
(str))
|
:trace (ex/format-throwable cause :detail? false :header? false)})
|
||||||
:trace (ex/format-throwable cause :detail? false :header? false)}))
|
|
||||||
|
|
||||||
(defn- audit-event->report
|
(defn handle-event
|
||||||
[{:keys [::audit/context ::audit/props ::audit/id] :as event}]
|
[cfg record]
|
||||||
{:id id
|
(when @enabled
|
||||||
:type "exception"
|
|
||||||
:origin "audit-log"
|
|
||||||
:hint (get props :hint)
|
|
||||||
:tenant (cf/get :tenant)
|
|
||||||
:host (cf/get :host)
|
|
||||||
:backend-version (:full cf/version)
|
|
||||||
:frontend-version (:version context)
|
|
||||||
:profile-id (:audit/profile-id event)
|
|
||||||
:href (get props :href)})
|
|
||||||
|
|
||||||
(defn- rlimit-event->report
|
|
||||||
[event]
|
|
||||||
{:id (::rlimit/id event)
|
|
||||||
:type "notification"
|
|
||||||
:origin "rlimit"
|
|
||||||
: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
|
|
||||||
[cfg event event->report]
|
|
||||||
(try
|
(try
|
||||||
(let [report (event->report event)]
|
(let [report (record->report record)]
|
||||||
(send-mattermost-notification! cfg report))
|
(send-mattermost-notification! cfg report))
|
||||||
(catch Throwable cause
|
(catch Throwable cause
|
||||||
(l/warn :hint "unhandled error" :cause cause))))
|
(l/warn :hint "unhandled error" :cause cause)))))
|
||||||
|
|
||||||
(defmethod ig/assert-key ::reporter
|
(defmethod ig/assert-key ::reporter
|
||||||
[_ params]
|
[_ params]
|
||||||
@ -135,52 +80,27 @@
|
|||||||
(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))
|
(px/thread
|
||||||
thread (px/thread
|
{:name "penpot/mattermost-reporter"
|
||||||
{:name "penpot/reporter/mattermost"}
|
:virtual true}
|
||||||
(l/info :hint "initializing error reporter" :uri uri)
|
(l/info :hint "initializing error reporter" :uri uri)
|
||||||
|
(let [input (sp/chan :buf (sp/sliding-buffer 128)
|
||||||
|
:xf (filter ldb/error-record?))]
|
||||||
|
(add-watch l/log-record ::reporter #(sp/put! input %4))
|
||||||
(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 "reporter interrupted"))
|
||||||
(catch Throwable cause
|
(catch Throwable cause
|
||||||
(l/error :hint "unexpected error" :cause cause))
|
(l/error :hint "unexpected error" :cause cause))
|
||||||
(finally
|
(finally
|
||||||
(l/info :hint "reporter terminated"))))]
|
(sp/close! input)
|
||||||
|
(remove-watch l/log-record ::reporter)
|
||||||
(add-watch l/log-record ::reporter
|
(l/info :hint "reporter terminated")))))))
|
||||||
(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-> 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)))
|
|
||||||
|
|||||||
@ -226,10 +226,11 @@
|
|||||||
::http/server
|
::http/server
|
||||||
{::http/port (cf/get :http-server-port)
|
{::http/port (cf/get :http-server-port)
|
||||||
::http/host (cf/get :http-server-host)
|
::http/host (cf/get :http-server-host)
|
||||||
|
::http/router (ig/ref ::http/router)
|
||||||
::http/io-threads (cf/get :http-server-io-threads)
|
::http/io-threads (cf/get :http-server-io-threads)
|
||||||
::http/max-worker-threads (cf/get :http-server-max-worker-threads)
|
::http/max-worker-threads (cf/get :http-server-max-worker-threads)
|
||||||
::http/max-body-size (cf/get :http-server-max-body-size)
|
::http/max-body-size (cf/get :http-server-max-body-size)
|
||||||
::http/router (ig/ref ::http/router)
|
::http/max-multipart-body-size (cf/get :http-server-max-multipart-body-size)
|
||||||
::mtx/metrics (ig/ref ::mtx/metrics)}
|
::mtx/metrics (ig/ref ::mtx/metrics)}
|
||||||
|
|
||||||
::ldap/provider
|
::ldap/provider
|
||||||
@ -316,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)
|
||||||
@ -343,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)
|
||||||
@ -364,7 +348,6 @@
|
|||||||
::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)}
|
||||||
|
|
||||||
@ -375,8 +358,7 @@
|
|||||||
;; 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)
|
||||||
@ -388,7 +370,6 @@
|
|||||||
:offload-file-data (ig/ref :app.tasks.offload-file-data/handler)
|
:offload-file-data (ig/ref :app.tasks.offload-file-data/handler)
|
||||||
:tasks-gc (ig/ref :app.tasks.tasks-gc/handler)
|
:tasks-gc (ig/ref :app.tasks.tasks-gc/handler)
|
||||||
:telemetry (ig/ref :app.tasks.telemetry/handler)
|
:telemetry (ig/ref :app.tasks.telemetry/handler)
|
||||||
:upload-session-gc (ig/ref :app.tasks.upload-session-gc/handler)
|
|
||||||
:storage-gc-deleted (ig/ref ::sto.gc-deleted/handler)
|
:storage-gc-deleted (ig/ref ::sto.gc-deleted/handler)
|
||||||
:storage-gc-touched (ig/ref ::sto.gc-touched/handler)
|
:storage-gc-touched (ig/ref ::sto.gc-touched/handler)
|
||||||
:session-gc (ig/ref ::session.tasks/gc)
|
:session-gc (ig/ref ::session.tasks/gc)
|
||||||
@ -424,9 +405,6 @@
|
|||||||
:app.tasks.tasks-gc/handler
|
:app.tasks.tasks-gc/handler
|
||||||
{::db/pool (ig/ref ::db/pool)}
|
{::db/pool (ig/ref ::db/pool)}
|
||||||
|
|
||||||
:app.tasks.upload-session-gc/handler
|
|
||||||
{::db/pool (ig/ref ::db/pool)}
|
|
||||||
|
|
||||||
:app.tasks.objects-gc/handler
|
:app.tasks.objects-gc/handler
|
||||||
{::db/pool (ig/ref ::db/pool)
|
{::db/pool (ig/ref ::db/pool)
|
||||||
::sto/storage (ig/ref ::sto/storage)}
|
::sto/storage (ig/ref ::sto/storage)}
|
||||||
@ -468,19 +446,13 @@
|
|||||||
;; 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)
|
|
||||||
:nexus (cf/get :nexus-shared-key)
|
|
||||||
:nitrate (cf/get :nitrate-shared-key)
|
|
||||||
:exporter (cf/get :exporter-shared-key)}
|
|
||||||
|
|
||||||
::setup/clock
|
::setup/clock
|
||||||
{}
|
{}
|
||||||
|
|
||||||
:app.loggers.audit.archive-task/handler
|
:app.loggers.audit.archive-task/handler
|
||||||
{::setup/shared-keys (ig/ref ::setup/shared-keys)
|
{::setup/props (ig/ref ::setup/props)
|
||||||
::http.client/client (ig/ref ::http.client/client)
|
::db/pool (ig/ref ::db/pool)
|
||||||
::db/pool (ig/ref ::db/pool)}
|
::http.client/client (ig/ref ::http.client/client)}
|
||||||
|
|
||||||
:app.loggers.audit.gc-task/handler
|
:app.loggers.audit.gc-task/handler
|
||||||
{::db/pool (ig/ref ::db/pool)}
|
{::db/pool (ig/ref ::db/pool)}
|
||||||
@ -548,9 +520,6 @@
|
|||||||
{:cron #penpot/cron "0 0 0 * * ?" ;; daily
|
{:cron #penpot/cron "0 0 0 * * ?" ;; daily
|
||||||
:task :tasks-gc}
|
:task :tasks-gc}
|
||||||
|
|
||||||
{:cron #penpot/cron "0 0 0 * * ?" ;; daily
|
|
||||||
:task :upload-session-gc}
|
|
||||||
|
|
||||||
{:cron #penpot/cron "0 0 2 * * ?" ;; daily
|
{:cron #penpot/cron "0 0 2 * * ?" ;; daily
|
||||||
:task :file-gc-scheduler}
|
:task :file-gc-scheduler}
|
||||||
|
|
||||||
|
|||||||
@ -31,11 +31,12 @@
|
|||||||
(:import
|
(:import
|
||||||
clojure.lang.XMLHandler
|
clojure.lang.XMLHandler
|
||||||
java.io.InputStream
|
java.io.InputStream
|
||||||
javax.xml.parsers.SAXParserFactory
|
|
||||||
javax.xml.XMLConstants
|
javax.xml.XMLConstants
|
||||||
|
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)]
|
||||||
(if (= 0 orientation-exit)
|
|
||||||
(case orientation
|
(case orientation
|
||||||
("6" "8") {:width h :height w} ; Rotated 90 or 270 degrees
|
("6" "8") {:width h :height w} ; Rotated 90 or 270 degrees
|
||||||
{:width w :height h}) ; Normal or unknown orientation
|
{:width w :height h})) ; Normal or unknown orientation
|
||||||
{:width w :height h}))))) ; If orientation can't be read, use dimensions as-is
|
nil)))
|
||||||
|
|
||||||
(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')))
|
||||||
|
(let [{:keys [width height]}
|
||||||
|
(or (get-dimensions-with-orientation (str path))
|
||||||
|
(do
|
||||||
|
(l/warn "Failed to read image dimensions with orientation; falling back to im4java"
|
||||||
|
{:path path})
|
||||||
|
{:width (.getPageWidth instance)
|
||||||
|
:height (.getPageHeight instance)}))]
|
||||||
(assoc input
|
(assoc input
|
||||||
:width width
|
:width width
|
||||||
:height height
|
:height height
|
||||||
:size (fs/size path)
|
:size (fs/size path)
|
||||||
:ts (ct/now))))))
|
:ts (ct/now)))))))
|
||||||
|
|
||||||
(defmethod process-error org.im4java.core.InfoException
|
(defmethod process-error org.im4java.core.InfoException
|
||||||
[error]
|
[error]
|
||||||
@ -293,17 +282,12 @@
|
|||||||
(defn download-image
|
(defn download-image
|
||||||
"Download an image from the provided URI and return the media input object"
|
"Download an image from the provided URI and return the media input object"
|
||||||
[{:keys [::http/client]} uri]
|
[{:keys [::http/client]} uri]
|
||||||
(letfn [(parse-and-validate [{:keys [status headers] :as response}]
|
(letfn [(parse-and-validate [{:keys [headers] :as response}]
|
||||||
(let [size (some-> (get headers "content-length") d/parse-integer)
|
(let [size (some-> (get headers "content-length") d/parse-integer)
|
||||||
mtype (get headers "content-type")
|
mtype (get headers "content-type")
|
||||||
format (cm/mtype->format mtype)
|
format (cm/mtype->format mtype)
|
||||||
max-size (cf/get :media-max-file-size default-max-file-size)]
|
max-size (cf/get :media-max-file-size default-max-file-size)]
|
||||||
|
|
||||||
(when-not (<= 200 status 299)
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :unable-to-download-image
|
|
||||||
:hint (str/ffmt "unable to download image from '%': unexpected status code %" uri status)))
|
|
||||||
|
|
||||||
(when-not size
|
(when-not size
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :unknown-size
|
:code :unknown-size
|
||||||
@ -323,32 +307,9 @@
|
|||||||
|
|
||||||
{:size size :mtype mtype :format format}))]
|
{:size size :mtype mtype :format format}))]
|
||||||
|
|
||||||
(let [{:keys [body] :as response}
|
(let [{:keys [body] :as response} (http/req! client
|
||||||
(try
|
|
||||||
(http/req! client
|
|
||||||
{:method :get :uri uri}
|
{:method :get :uri uri}
|
||||||
{:response-type :input-stream})
|
{:response-type :input-stream})
|
||||||
(catch java.net.ConnectException cause
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :unable-to-download-image
|
|
||||||
:hint (str/ffmt "unable to download image from '%': connection refused or host unreachable" uri)
|
|
||||||
:cause cause))
|
|
||||||
(catch java.net.http.HttpConnectTimeoutException cause
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :unable-to-download-image
|
|
||||||
:hint (str/ffmt "unable to download image from '%': connection timeout" uri)
|
|
||||||
:cause cause))
|
|
||||||
(catch java.net.http.HttpTimeoutException cause
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :unable-to-download-image
|
|
||||||
:hint (str/ffmt "unable to download image from '%': request timeout" uri)
|
|
||||||
:cause cause))
|
|
||||||
(catch java.io.IOException cause
|
|
||||||
(ex/raise :type :validation
|
|
||||||
:code :unable-to-download-image
|
|
||||||
:hint (str/ffmt "unable to download image from '%': I/O error" uri)
|
|
||||||
:cause cause)))
|
|
||||||
|
|
||||||
{:keys [size mtype]} (parse-and-validate response)
|
{:keys [size mtype]} (parse-and-validate response)
|
||||||
path (tmp/tempfile :prefix "penpot.media.download.")
|
path (tmp/tempfile :prefix "penpot.media.download.")
|
||||||
written (io/write* path body :size size)]
|
written (io/write* path body :size size)]
|
||||||
@ -409,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]
|
||||||
@ -474,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))))))))
|
|
||||||
|
|||||||
@ -15,16 +15,16 @@
|
|||||||
io.prometheus.client.CollectorRegistry
|
io.prometheus.client.CollectorRegistry
|
||||||
io.prometheus.client.Counter
|
io.prometheus.client.Counter
|
||||||
io.prometheus.client.Counter$Child
|
io.prometheus.client.Counter$Child
|
||||||
io.prometheus.client.exporter.common.TextFormat
|
|
||||||
io.prometheus.client.Gauge
|
io.prometheus.client.Gauge
|
||||||
io.prometheus.client.Gauge$Child
|
io.prometheus.client.Gauge$Child
|
||||||
io.prometheus.client.Histogram
|
io.prometheus.client.Histogram
|
||||||
io.prometheus.client.Histogram$Child
|
io.prometheus.client.Histogram$Child
|
||||||
io.prometheus.client.hotspot.DefaultExports
|
|
||||||
io.prometheus.client.SimpleCollector
|
io.prometheus.client.SimpleCollector
|
||||||
io.prometheus.client.Summary
|
io.prometheus.client.Summary
|
||||||
io.prometheus.client.Summary$Builder
|
io.prometheus.client.Summary$Builder
|
||||||
io.prometheus.client.Summary$Child
|
io.prometheus.client.Summary$Child
|
||||||
|
io.prometheus.client.exporter.common.TextFormat
|
||||||
|
io.prometheus.client.hotspot.DefaultExports
|
||||||
java.io.StringWriter))
|
java.io.StringWriter))
|
||||||
|
|
||||||
(set! *warn-on-reflection* true)
|
(set! *warn-on-reflection* true)
|
||||||
|
|||||||
@ -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,25 +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 "0145-mod-audit-log-table"
|
|
||||||
:fn (mg/resource "app/migrations/sql/0145-mod-audit-log-table.sql")}
|
|
||||||
|
|
||||||
{:name "0146-mod-access-token-table"
|
|
||||||
:fn (mg/resource "app/migrations/sql/0146-mod-access-token-table.sql")}
|
|
||||||
|
|
||||||
{:name "0147-mod-team-invitation-table"
|
|
||||||
:fn (mg/resource "app/migrations/sql/0147-mod-team-invitation-table.sql")}
|
|
||||||
|
|
||||||
{:name "0147-add-upload-session-table"
|
|
||||||
:fn (mg/resource "app/migrations/sql/0147-add-upload-session-table.sql")}])
|
|
||||||
|
|
||||||
(defn apply-migrations!
|
(defn apply-migrations!
|
||||||
[pool name migrations]
|
[pool name migrations]
|
||||||
|
|||||||
@ -58,3 +58,4 @@
|
|||||||
(when (nil? (:data file))
|
(when (nil? (:data file))
|
||||||
(migrate-file conn file)))
|
(migrate-file conn file)))
|
||||||
(db/exec-one! conn ["drop table page cascade;"])))
|
(db/exec-one! conn ["drop table page cascade;"])))
|
||||||
|
|
||||||
|
|||||||
@ -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}))))))))
|
|
||||||
|
|
||||||
@ -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 );
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
CREATE INDEX audit_log__created_at__idx ON audit_log(created_at) WHERE archived_at IS NULL;
|
|
||||||
CREATE INDEX audit_log__archived_at__idx ON audit_log(archived_at) WHERE archived_at IS NOT NULL;
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE access_token
|
|
||||||
ADD COLUMN type text NULL;
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
CREATE TABLE upload_session (
|
|
||||||
id uuid PRIMARY KEY,
|
|
||||||
|
|
||||||
created_at timestamptz NOT NULL DEFAULT now(),
|
|
||||||
|
|
||||||
profile_id uuid NOT NULL REFERENCES profile(id) ON DELETE CASCADE,
|
|
||||||
total_chunks integer NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX upload_session__profile_id__idx
|
|
||||||
ON upload_session(profile_id);
|
|
||||||
|
|
||||||
CREATE INDEX upload_session__created_at__idx
|
|
||||||
ON upload_session(created_at);
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
ALTER TABLE team_invitation
|
|
||||||
ADD COLUMN org_id uuid NULL;
|
|
||||||
|
|
||||||
ALTER TABLE team_invitation
|
|
||||||
ALTER COLUMN team_id DROP NOT NULL;
|
|
||||||
|
|
||||||
ALTER TABLE team_invitation
|
|
||||||
ADD CONSTRAINT team_invitation_team_or_org_not_null
|
|
||||||
CHECK (team_id IS NOT NULL OR org_id IS NOT NULL);
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX team_invitation_org_unique
|
|
||||||
ON team_invitation (org_id, email_to)
|
|
||||||
WHERE team_id IS NULL;
|
|
||||||
@ -1,412 +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.nitrate
|
|
||||||
"Module that make calls to the external nitrate aplication"
|
|
||||||
(:require
|
|
||||||
[app.common.exceptions :as ex]
|
|
||||||
[app.common.json :as json]
|
|
||||||
[app.common.logging :as l]
|
|
||||||
[app.common.schema :as sm]
|
|
||||||
[app.common.schema.generators :as sg]
|
|
||||||
[app.common.time :as ct]
|
|
||||||
[app.common.types.organization :as cto]
|
|
||||||
[app.config :as cf]
|
|
||||||
[app.http.client :as http]
|
|
||||||
[app.rpc :as-alias rpc]
|
|
||||||
[app.setup :as-alias setup]
|
|
||||||
[clojure.core :as c]
|
|
||||||
[integrant.core :as ig]))
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
;; HELPERS
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
|
|
||||||
(defn- request-builder
|
|
||||||
[cfg method uri shared-key profile-id request-params]
|
|
||||||
(fn []
|
|
||||||
(http/req! cfg (cond-> {: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}
|
|
||||||
(= method :post) (assoc :body (json/encode request-params :key-fn json/write-camel-key))))))
|
|
||||||
|
|
||||||
(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 [response (handler)
|
|
||||||
status (:status response)]
|
|
||||||
(when-not status
|
|
||||||
(l/error :hint "could't do the nitrate request, it is probably down"
|
|
||||||
:uri uri)
|
|
||||||
;; TODO decide what to do when Nitrate is inaccesible
|
|
||||||
nil)
|
|
||||||
(cond
|
|
||||||
(>= status 400)
|
|
||||||
;; For error status codes (4xx, 5xx), fail immediately without validation
|
|
||||||
(do
|
|
||||||
(when (not= status 404) ;; Don't need to log 404
|
|
||||||
(l/error :hint "nitrate request failed with error status"
|
|
||||||
:uri uri
|
|
||||||
:status status
|
|
||||||
:body (:body response)))
|
|
||||||
nil)
|
|
||||||
(= status 204) ;; 204 doesn't return any body
|
|
||||||
nil
|
|
||||||
:else ;; For success status codes, validate the response
|
|
||||||
(let [coercer-http (sm/coercer schema
|
|
||||||
:type :validation
|
|
||||||
:hint (str "invalid data received calling " uri))
|
|
||||||
data (-> response :body (json/decode :key-fn json/read-kebab-key))]
|
|
||||||
(try
|
|
||||||
(coercer-http data)
|
|
||||||
(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 request-params] :as params}]
|
|
||||||
(let [shared-key (-> cfg ::setup/shared-keys :nitrate)
|
|
||||||
full-http-call (-> (request-builder cfg method uri shared-key profile-id request-params)
|
|
||||||
(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:org-summary
|
|
||||||
[:map
|
|
||||||
[:id ::sm/uuid]
|
|
||||||
[:name ::sm/text]
|
|
||||||
[:owner-id ::sm/uuid]
|
|
||||||
[:teams
|
|
||||||
[:vector
|
|
||||||
[:map
|
|
||||||
[:id ::sm/uuid]
|
|
||||||
[:is-your-penpot :boolean]]]]])
|
|
||||||
|
|
||||||
(def ^:private schema:profile-org
|
|
||||||
[:map
|
|
||||||
[:is-member :boolean]
|
|
||||||
[:organization-id {:optional true} [:maybe ::sm/uuid]]
|
|
||||||
[:default-team-id {:optional true} [:maybe ::sm/uuid]]])
|
|
||||||
|
|
||||||
|
|
||||||
;; TODO Unify with schemas on backend/src/app/http/management.clj
|
|
||||||
(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 ct/inst
|
|
||||||
:encode/string inst-ms
|
|
||||||
:decode/json ct/inst
|
|
||||||
:encode/json inst-ms}}))
|
|
||||||
|
|
||||||
(def ^:private schema:subscription
|
|
||||||
[:map {:title "Subscription"}
|
|
||||||
[:id ::sm/text]
|
|
||||||
[:customer-id ::sm/text]
|
|
||||||
[:type [:enum
|
|
||||||
"unlimited"
|
|
||||||
"professional"
|
|
||||||
"enterprise"
|
|
||||||
"nitrate"]]
|
|
||||||
[: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 schema:connectivity
|
|
||||||
[:map
|
|
||||||
[:licenses ::sm/boolean]])
|
|
||||||
|
|
||||||
(defn- get-team-org-api
|
|
||||||
[cfg {:keys [team-id] :as params}]
|
|
||||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
|
||||||
(request-to-nitrate cfg :get
|
|
||||||
(str baseuri
|
|
||||||
"/api/teams/"
|
|
||||||
team-id)
|
|
||||||
cto/schema:team-with-organization params)))
|
|
||||||
|
|
||||||
(defn- get-org-membership-api
|
|
||||||
[cfg {:keys [profile-id organization-id] :as params}]
|
|
||||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
|
||||||
(request-to-nitrate cfg :get
|
|
||||||
(str baseuri
|
|
||||||
"/api/organizations/"
|
|
||||||
organization-id
|
|
||||||
"/members/"
|
|
||||||
profile-id)
|
|
||||||
schema:profile-org params)))
|
|
||||||
|
|
||||||
(defn- get-org-membership-by-team-api
|
|
||||||
[cfg {:keys [profile-id team-id] :as params}]
|
|
||||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
|
||||||
(request-to-nitrate cfg :get
|
|
||||||
(str baseuri
|
|
||||||
"/api/teams/"
|
|
||||||
team-id
|
|
||||||
"/users/"
|
|
||||||
profile-id)
|
|
||||||
schema:profile-org params)))
|
|
||||||
|
|
||||||
|
|
||||||
(defn- get-org-summary-api
|
|
||||||
[cfg {:keys [organization-id] :as params}]
|
|
||||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
|
||||||
(request-to-nitrate cfg :get
|
|
||||||
(str baseuri
|
|
||||||
"/api/organizations/"
|
|
||||||
organization-id
|
|
||||||
"/summary")
|
|
||||||
schema:org-summary params)))
|
|
||||||
|
|
||||||
|
|
||||||
(defn- set-team-org-api
|
|
||||||
[cfg {:keys [organization-id team-id is-default] :as params}]
|
|
||||||
(let [baseuri (cf/get :nitrate-backend-uri)
|
|
||||||
params (assoc params :request-params {:team-id team-id
|
|
||||||
:is-your-penpot (true? is-default)})
|
|
||||||
team (request-to-nitrate cfg :post
|
|
||||||
(str baseuri
|
|
||||||
"/api/organizations/"
|
|
||||||
organization-id
|
|
||||||
"/add-team")
|
|
||||||
cto/schema:team-with-organization params)
|
|
||||||
custom-photo (when-let [logo-id (get-in team [:organization :logo-id])]
|
|
||||||
(str (cf/get :public-uri) "/assets/by-id/" logo-id))]
|
|
||||||
(cond-> team
|
|
||||||
custom-photo
|
|
||||||
(assoc-in [:organization :custom-photo] custom-photo))))
|
|
||||||
|
|
||||||
(defn- add-profile-to-org-api
|
|
||||||
[cfg {:keys [profile-id organization-id team-id email] :as params}]
|
|
||||||
(let [baseuri (cf/get :nitrate-backend-uri)
|
|
||||||
request-params (cond-> {:user-id profile-id :team-id team-id}
|
|
||||||
(some? email) (assoc :email email))
|
|
||||||
params (assoc params :request-params request-params)]
|
|
||||||
(request-to-nitrate cfg :post
|
|
||||||
(str baseuri
|
|
||||||
"/api/organizations/"
|
|
||||||
organization-id
|
|
||||||
"/add-user")
|
|
||||||
schema:profile-org params)))
|
|
||||||
|
|
||||||
(defn- remove-profile-from-org-api
|
|
||||||
[cfg {:keys [profile-id organization-id] :as params}]
|
|
||||||
(let [baseuri (cf/get :nitrate-backend-uri)
|
|
||||||
params (assoc params :request-params {:user-id profile-id})]
|
|
||||||
(request-to-nitrate cfg :post
|
|
||||||
(str baseuri
|
|
||||||
"/api/organizations/"
|
|
||||||
organization-id
|
|
||||||
"/remove-user")
|
|
||||||
nil params)))
|
|
||||||
|
|
||||||
(defn- remove-profile-from-all-orgs-api
|
|
||||||
[cfg {:keys [profile-id] :as params}]
|
|
||||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
|
||||||
(request-to-nitrate cfg :post
|
|
||||||
(str baseuri
|
|
||||||
"/api/users/"
|
|
||||||
profile-id
|
|
||||||
"/remove-organizations")
|
|
||||||
nil params)))
|
|
||||||
|
|
||||||
(defn- remove-team-from-org-api
|
|
||||||
[cfg {:keys [team-id organization-id] :as params}]
|
|
||||||
(let [baseuri (cf/get :nitrate-backend-uri)
|
|
||||||
params (assoc params :request-params {:team-id team-id})]
|
|
||||||
(request-to-nitrate cfg :post
|
|
||||||
(str baseuri
|
|
||||||
"/api/organizations/"
|
|
||||||
organization-id
|
|
||||||
"/remove-team")
|
|
||||||
nil params)))
|
|
||||||
|
|
||||||
(defn- delete-team-api
|
|
||||||
[cfg {:keys [team-id] :as params}]
|
|
||||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
|
||||||
(request-to-nitrate cfg :delete
|
|
||||||
(str baseuri
|
|
||||||
"/api/teams/"
|
|
||||||
team-id)
|
|
||||||
nil params)))
|
|
||||||
|
|
||||||
(defn- get-subscription-api
|
|
||||||
[cfg {:keys [profile-id] :as params}]
|
|
||||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
|
||||||
(request-to-nitrate cfg :get
|
|
||||||
(str baseuri
|
|
||||||
"/api/subscriptions/"
|
|
||||||
profile-id)
|
|
||||||
schema:subscription params)))
|
|
||||||
|
|
||||||
(defn- get-connectivity-api
|
|
||||||
[cfg params]
|
|
||||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
|
||||||
(request-to-nitrate cfg :get
|
|
||||||
(str baseuri
|
|
||||||
"/api/connectivity")
|
|
||||||
schema:connectivity params)))
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
;; INITIALIZATION
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
|
|
||||||
(defmethod ig/init-key ::client
|
|
||||||
[_ cfg]
|
|
||||||
(when (contains? cf/flags :nitrate)
|
|
||||||
{:get-team-org (partial get-team-org-api cfg)
|
|
||||||
:set-team-org (partial set-team-org-api cfg)
|
|
||||||
:get-org-membership (partial get-org-membership-api cfg)
|
|
||||||
:get-org-membership-by-team (partial get-org-membership-by-team-api cfg)
|
|
||||||
:get-org-summary (partial get-org-summary-api cfg)
|
|
||||||
:add-profile-to-org (partial add-profile-to-org-api cfg)
|
|
||||||
:remove-profile-from-org (partial remove-profile-from-org-api cfg)
|
|
||||||
:remove-profile-from-all-orgs (partial remove-profile-from-all-orgs-api cfg)
|
|
||||||
:delete-team (partial delete-team-api cfg)
|
|
||||||
:remove-team-from-org (partial remove-team-from-org-api cfg)
|
|
||||||
:get-subscription (partial get-subscription-api cfg)
|
|
||||||
:connectivity (partial get-connectivity-api cfg)}))
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
;; UTILS
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
||||||
|
|
||||||
|
|
||||||
(defn add-nitrate-licence-to-profile
|
|
||||||
"Enriches a profile map with subscription information from Nitrate.
|
|
||||||
Adds a :subscription field containing the user's license details.
|
|
||||||
Returns the original profile unchanged if the request fails."
|
|
||||||
[cfg profile]
|
|
||||||
(try
|
|
||||||
(let [subscription (call cfg :get-subscription {:profile-id (:id profile)})]
|
|
||||||
(assoc profile :subscription subscription))
|
|
||||||
(catch Throwable cause
|
|
||||||
(l/error :hint "failed to get nitrate licence"
|
|
||||||
:profile-id (:id profile)
|
|
||||||
:cause cause)
|
|
||||||
profile)))
|
|
||||||
|
|
||||||
(defn add-org-info-to-team
|
|
||||||
"Enriches a team map with organization information from Nitrate.
|
|
||||||
Adds organization-id, organization-name, organization-slug, organization-owner-id, and your-penpot fields.
|
|
||||||
Returns the original team unchanged if the request fails or org data is nil."
|
|
||||||
[cfg team params]
|
|
||||||
(try
|
|
||||||
(let [params (assoc (or params {}) :team-id (:id team))
|
|
||||||
team-with-org (call cfg :get-team-org params)
|
|
||||||
org (:organization team-with-org)]
|
|
||||||
(if (some? org)
|
|
||||||
(-> (cto/apply-organization team (assoc org :custom-photo
|
|
||||||
(when-let [logo-id (:logo-id org)]
|
|
||||||
(str (cf/get :public-uri) "/assets/by-id/" logo-id))))
|
|
||||||
(assoc :is-default (or (:is-default team) (true? (:is-your-penpot team-with-org)))))
|
|
||||||
team))
|
|
||||||
(catch Throwable cause
|
|
||||||
(l/error :hint "failed to get team organization info"
|
|
||||||
:team-id (:id team)
|
|
||||||
:cause cause)
|
|
||||||
team)))
|
|
||||||
|
|
||||||
(defn set-team-organization
|
|
||||||
"Associates a team with an organization in Nitrate.
|
|
||||||
Requires organization-id and is-default in params.
|
|
||||||
Throws an exception if the request fails."
|
|
||||||
[cfg team params]
|
|
||||||
(let [params (assoc (or params {})
|
|
||||||
:team-id (:id team)
|
|
||||||
:organization-id (:organization-id params)
|
|
||||||
:is-default (:is-default params))
|
|
||||||
result (call cfg :set-team-org params)]
|
|
||||||
(when (nil? result)
|
|
||||||
(ex/raise :type :internal
|
|
||||||
:code :failed-to-set-team-org
|
|
||||||
:context {:team-id (:id team)
|
|
||||||
:organization-id (:organization-id params)}))
|
|
||||||
team))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -24,28 +24,28 @@
|
|||||||
[integrant.core :as ig])
|
[integrant.core :as ig])
|
||||||
(:import
|
(:import
|
||||||
clojure.lang.MapEntry
|
clojure.lang.MapEntry
|
||||||
io.lettuce.core.api.StatefulRedisConnection
|
|
||||||
io.lettuce.core.api.sync.RedisCommands
|
|
||||||
io.lettuce.core.api.sync.RedisScriptingCommands
|
|
||||||
io.lettuce.core.codec.RedisCodec
|
|
||||||
io.lettuce.core.codec.StringCodec
|
|
||||||
io.lettuce.core.KeyValue
|
io.lettuce.core.KeyValue
|
||||||
io.lettuce.core.pubsub.api.sync.RedisPubSubCommands
|
|
||||||
io.lettuce.core.pubsub.RedisPubSubListener
|
|
||||||
io.lettuce.core.pubsub.StatefulRedisPubSubConnection
|
|
||||||
io.lettuce.core.RedisClient
|
io.lettuce.core.RedisClient
|
||||||
io.lettuce.core.RedisCommandInterruptedException
|
io.lettuce.core.RedisCommandInterruptedException
|
||||||
io.lettuce.core.RedisCommandTimeoutException
|
io.lettuce.core.RedisCommandTimeoutException
|
||||||
io.lettuce.core.RedisException
|
io.lettuce.core.RedisException
|
||||||
io.lettuce.core.RedisURI
|
io.lettuce.core.RedisURI
|
||||||
io.lettuce.core.resource.ClientResources
|
|
||||||
io.lettuce.core.resource.DefaultClientResources
|
|
||||||
io.lettuce.core.ScriptOutputType
|
io.lettuce.core.ScriptOutputType
|
||||||
io.lettuce.core.SetArgs
|
io.lettuce.core.SetArgs
|
||||||
|
io.lettuce.core.api.StatefulRedisConnection
|
||||||
|
io.lettuce.core.api.sync.RedisCommands
|
||||||
|
io.lettuce.core.api.sync.RedisScriptingCommands
|
||||||
|
io.lettuce.core.codec.RedisCodec
|
||||||
|
io.lettuce.core.codec.StringCodec
|
||||||
|
io.lettuce.core.pubsub.RedisPubSubListener
|
||||||
|
io.lettuce.core.pubsub.StatefulRedisPubSubConnection
|
||||||
|
io.lettuce.core.pubsub.api.sync.RedisPubSubCommands
|
||||||
|
io.lettuce.core.resource.ClientResources
|
||||||
|
io.lettuce.core.resource.DefaultClientResources
|
||||||
io.netty.channel.nio.NioEventLoopGroup
|
io.netty.channel.nio.NioEventLoopGroup
|
||||||
io.netty.util.concurrent.EventExecutorGroup
|
|
||||||
io.netty.util.HashedWheelTimer
|
io.netty.util.HashedWheelTimer
|
||||||
io.netty.util.Timer
|
io.netty.util.Timer
|
||||||
|
io.netty.util.concurrent.EventExecutorGroup
|
||||||
java.lang.AutoCloseable
|
java.lang.AutoCloseable
|
||||||
java.time.Duration))
|
java.time.Duration))
|
||||||
|
|
||||||
|
|||||||
@ -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,22 +89,16 @@
|
|||||||
[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")
|
||||||
session-id (yreq/get-header request "x-session-id")
|
|
||||||
|
|
||||||
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
|
||||||
(assoc ::handler-name handler-name)
|
(assoc ::handler-name handler-name)
|
||||||
(assoc ::ip-addr ip-addr)
|
(assoc ::ip-addr ip-addr)
|
||||||
(assoc ::request-at (ct/now))
|
(assoc ::request-at (ct/now))
|
||||||
(assoc ::session-id (some-> session-id uuid/parse*))
|
|
||||||
(assoc ::cond/key etag)
|
(assoc ::cond/key etag)
|
||||||
(cond-> (uuid? profile-id)
|
(cond-> (uuid? profile-id)
|
||||||
(assoc ::profile-id profile-id)))
|
(assoc ::profile-id profile-id)))
|
||||||
@ -233,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)]]))
|
||||||
@ -245,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
|
||||||
@ -264,7 +253,6 @@
|
|||||||
'app.rpc.commands.ldap
|
'app.rpc.commands.ldap
|
||||||
'app.rpc.commands.management
|
'app.rpc.commands.management
|
||||||
'app.rpc.commands.media
|
'app.rpc.commands.media
|
||||||
'app.rpc.commands.nitrate
|
|
||||||
'app.rpc.commands.profile
|
'app.rpc.commands.profile
|
||||||
'app.rpc.commands.projects
|
'app.rpc.commands.projects
|
||||||
'app.rpc.commands.search
|
'app.rpc.commands.search
|
||||||
@ -273,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
|
||||||
@ -305,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
|
||||||
@ -354,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)}]
|
||||||
|
|
||||||
@ -377,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]
|
||||||
@ -395,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]
|
||||||
|
|||||||
@ -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 #(and (some? (:expires-at %))
|
|
||||||
(ct/is-after? request-at (:expires-at %))))
|
|
||||||
(map decode-row)
|
|
||||||
(first)))
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -253,15 +253,12 @@
|
|||||||
:hint "email has complaint reports")))
|
:hint "email has complaint reports")))
|
||||||
|
|
||||||
(defn prepare-register
|
(defn prepare-register
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [fullname email] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [fullname email accept-newsletter-updates] :as params}]
|
||||||
|
|
||||||
(validate-register-attempt! cfg params)
|
(validate-register-attempt! cfg params)
|
||||||
|
|
||||||
(let [email (profile/clean-email email)
|
(let [email (profile/clean-email email)
|
||||||
profile (profile/get-profile-by-email pool email)
|
profile (profile/get-profile-by-email pool email)
|
||||||
props (-> (audit/extract-utm-params params)
|
|
||||||
(cond-> (:accept-newsletter-updates params)
|
|
||||||
(assoc :newsletter-updates true)))
|
|
||||||
params {:email email
|
params {:email email
|
||||||
:fullname fullname
|
:fullname fullname
|
||||||
:password (:password params)
|
:password (:password params)
|
||||||
@ -270,12 +267,13 @@
|
|||||||
:iss :prepared-register
|
:iss :prepared-register
|
||||||
:profile-id (:id profile)
|
:profile-id (:id profile)
|
||||||
:exp (ct/in-future {:days 7})
|
:exp (ct/in-future {:days 7})
|
||||||
:props props}
|
:props {:newsletter-updates (or accept-newsletter-updates false)}}
|
||||||
|
|
||||||
params (d/without-nils params)
|
params (d/without-nils params)
|
||||||
token (tokens/generate cfg params)]
|
token (tokens/generate cfg params)]
|
||||||
|
|
||||||
(-> {:token token}
|
(with-meta {:token token}
|
||||||
(with-meta {::audit/profile-id uuid/zero}))))
|
{::audit/profile-id uuid/zero})))
|
||||||
|
|
||||||
(def schema:prepare-register-profile
|
(def schema:prepare-register-profile
|
||||||
[:map {:title "prepare-register-profile"}
|
[:map {:title "prepare-register-profile"}
|
||||||
@ -283,7 +281,6 @@
|
|||||||
[:email ::sm/email]
|
[:email ::sm/email]
|
||||||
[:password schema:password]
|
[:password schema:password]
|
||||||
[:create-welcome-file {:optional true} :boolean]
|
[:create-welcome-file {:optional true} :boolean]
|
||||||
[:accept-newsletter-updates {:optional true} :boolean]
|
|
||||||
[:invitation-token {:optional true} schema:token]])
|
[:invitation-token {:optional true} schema:token]])
|
||||||
|
|
||||||
(sv/defmethod ::prepare-register-profile
|
(sv/defmethod ::prepare-register-profile
|
||||||
@ -320,7 +317,8 @@
|
|||||||
attrs (all the other attrs are filled with default values)."
|
attrs (all the other attrs are filled with default values)."
|
||||||
[{:keys [::db/conn] :as cfg} {:keys [email] :as params}]
|
[{:keys [::db/conn] :as cfg} {:keys [email] :as params}]
|
||||||
(let [id (or (:id params) (uuid/next))
|
(let [id (or (:id params) (uuid/next))
|
||||||
props (-> (:props params)
|
props (-> (audit/extract-utm-params params)
|
||||||
|
(merge (:props params))
|
||||||
(merge {:viewed-tutorial? false
|
(merge {:viewed-tutorial? false
|
||||||
:viewed-walkthrough? false
|
:viewed-walkthrough? false
|
||||||
:nudge {:big 10 :small 1}
|
:nudge {:big 10 :small 1}
|
||||||
@ -371,12 +369,11 @@
|
|||||||
:cause cause)
|
:cause cause)
|
||||||
(throw cause))))))
|
(throw cause))))))
|
||||||
|
|
||||||
|
|
||||||
(defn create-profile-rels
|
(defn create-profile-rels
|
||||||
[{:keys [::db/conn] :as cfg} {:keys [id] :as profile}]
|
[conn {:keys [id] :as profile}]
|
||||||
(assert (db/connection-map? cfg)
|
|
||||||
"expected cfg with valid connection")
|
|
||||||
(let [features (cfeat/get-enabled-features cf/flags)
|
(let [features (cfeat/get-enabled-features cf/flags)
|
||||||
team (teams/create-team cfg
|
team (teams/create-team conn
|
||||||
{:profile-id id
|
{:profile-id id
|
||||||
:name "Default"
|
:name "Default"
|
||||||
:features features
|
:features features
|
||||||
@ -412,9 +409,7 @@
|
|||||||
(defn register-profile
|
(defn register-profile
|
||||||
[{:keys [::db/conn ::wrk/executor] :as cfg} {:keys [token] :as params}]
|
[{:keys [::db/conn ::wrk/executor] :as cfg} {:keys [token] :as params}]
|
||||||
(let [claims (tokens/verify cfg {:token token :iss :prepared-register})
|
(let [claims (tokens/verify cfg {:token token :iss :prepared-register})
|
||||||
params (cond-> claims
|
params (into claims params)
|
||||||
(:accept-newsletter-updates params)
|
|
||||||
(update :props assoc :newsletter-updates true))
|
|
||||||
|
|
||||||
profile (if-let [profile-id (:profile-id claims)]
|
profile (if-let [profile-id (:profile-id claims)]
|
||||||
(profile/get-profile conn profile-id)
|
(profile/get-profile conn profile-id)
|
||||||
@ -431,7 +426,7 @@
|
|||||||
(assoc :is-active is-active)
|
(assoc :is-active is-active)
|
||||||
(update :password auth/derive-password))
|
(update :password auth/derive-password))
|
||||||
profile (->> (create-profile cfg params)
|
profile (->> (create-profile cfg params)
|
||||||
(create-profile-rels cfg))]
|
(create-profile-rels conn))]
|
||||||
(vary-meta profile assoc :created true))))
|
(vary-meta profile assoc :created true))))
|
||||||
|
|
||||||
created? (-> profile meta :created true?)
|
created? (-> profile meta :created true?)
|
||||||
@ -448,7 +443,6 @@
|
|||||||
(when (:create-welcome-file params)
|
(when (:create-welcome-file params)
|
||||||
(let [cfg (dissoc cfg ::db/conn)]
|
(let [cfg (dissoc cfg ::db/conn)]
|
||||||
(wrk/submit! executor (create-welcome-file cfg profile)))))]
|
(wrk/submit! executor (create-welcome-file cfg profile)))))]
|
||||||
|
|
||||||
(cond
|
(cond
|
||||||
;; When profile is blocked, we just ignore it and return plain data
|
;; When profile is blocked, we just ignore it and return plain data
|
||||||
(:is-blocked profile)
|
(:is-blocked profile)
|
||||||
@ -456,8 +450,7 @@
|
|||||||
(l/wrn :hint "register attempt for already blocked profile"
|
(l/wrn :hint "register attempt for already blocked profile"
|
||||||
:profile-id (str (:id profile))
|
:profile-id (str (:id profile))
|
||||||
:profile-email (:email profile))
|
:profile-email (:email profile))
|
||||||
(rph/with-meta {:id (:id profile)
|
(rph/with-meta {:email (:email profile)}
|
||||||
:email (:email profile)}
|
|
||||||
{::audit/replace-props props
|
{::audit/replace-props props
|
||||||
::audit/context {:action "ignore-because-blocked"}
|
::audit/context {:action "ignore-because-blocked"}
|
||||||
::audit/profile-id (:id profile)
|
::audit/profile-id (:id profile)
|
||||||
@ -473,9 +466,7 @@
|
|||||||
(:member-email invitation)))
|
(:member-email invitation)))
|
||||||
(let [invitation (assoc invitation :member-id (:id profile))
|
(let [invitation (assoc invitation :member-id (:id profile))
|
||||||
token (tokens/generate cfg invitation)]
|
token (tokens/generate cfg invitation)]
|
||||||
(-> {:id (:id profile)
|
(-> {:invitation-token token}
|
||||||
:email (:email profile)
|
|
||||||
:invitation-token token}
|
|
||||||
(rph/with-transform (session/create-fn cfg profile claims))
|
(rph/with-transform (session/create-fn cfg profile claims))
|
||||||
(rph/with-meta {::audit/replace-props props
|
(rph/with-meta {::audit/replace-props props
|
||||||
::audit/context {:action "accept-invitation"}
|
::audit/context {:action "accept-invitation"}
|
||||||
@ -498,8 +489,7 @@
|
|||||||
(when-not (eml/has-reports? conn (:email profile))
|
(when-not (eml/has-reports? conn (:email profile))
|
||||||
(send-email-verification! cfg profile))
|
(send-email-verification! cfg profile))
|
||||||
|
|
||||||
(-> {:id (:id profile)
|
(-> {:email (:email profile)}
|
||||||
:email (:email profile)}
|
|
||||||
(rph/with-defer create-welcome-file-when-needed)
|
(rph/with-defer create-welcome-file-when-needed)
|
||||||
(rph/with-meta
|
(rph/with-meta
|
||||||
{::audit/replace-props props
|
{::audit/replace-props props
|
||||||
@ -526,8 +516,7 @@
|
|||||||
{:id (:id profile)})
|
{:id (:id profile)})
|
||||||
(send-email-verification! cfg profile))
|
(send-email-verification! cfg profile))
|
||||||
|
|
||||||
(rph/with-meta {:email (:email profile)
|
(rph/with-meta {:email (:email profile)}
|
||||||
:id (:id profile)}
|
|
||||||
{::audit/replace-props (audit/profile->props profile)
|
{::audit/replace-props (audit/profile->props profile)
|
||||||
::audit/context {:action action}
|
::audit/context {:action action}
|
||||||
::audit/profile-id (:id profile)
|
::audit/profile-id (:id profile)
|
||||||
@ -535,8 +524,7 @@
|
|||||||
|
|
||||||
(def schema:register-profile
|
(def schema:register-profile
|
||||||
[:map {:title "register-profile"}
|
[:map {:title "register-profile"}
|
||||||
[:token schema:token]
|
[:token schema:token]])
|
||||||
[:accept-newsletter-updates {:optional true} :boolean]])
|
|
||||||
|
|
||||||
(sv/defmethod ::register-profile
|
(sv/defmethod ::register-profile
|
||||||
{::rpc/auth false
|
{::rpc/auth false
|
||||||
|
|||||||
@ -22,7 +22,6 @@
|
|||||||
[app.media :as media]
|
[app.media :as media]
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
[app.rpc.commands.files :as files]
|
[app.rpc.commands.files :as files]
|
||||||
[app.rpc.commands.media :as media-cmd]
|
|
||||||
[app.rpc.commands.projects :as projects]
|
[app.rpc.commands.projects :as projects]
|
||||||
[app.rpc.commands.teams :as teams]
|
[app.rpc.commands.teams :as teams]
|
||||||
[app.rpc.doc :as-alias doc]
|
[app.rpc.doc :as-alias doc]
|
||||||
@ -81,33 +80,20 @@
|
|||||||
;; --- Command: import-binfile
|
;; --- Command: import-binfile
|
||||||
|
|
||||||
(defn- import-binfile
|
(defn- import-binfile
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [profile-id project-id version name file upload-id]}]
|
[{:keys [::db/pool] :as cfg} {:keys [profile-id project-id version name file]}]
|
||||||
(let [team
|
(let [team (teams/get-team pool
|
||||||
(teams/get-team pool
|
|
||||||
:profile-id profile-id
|
:profile-id profile-id
|
||||||
:project-id project-id)
|
:project-id project-id)
|
||||||
|
cfg (-> cfg
|
||||||
cfg
|
|
||||||
(-> cfg
|
|
||||||
(assoc ::bfc/features (cfeat/get-team-enabled-features cf/flags team))
|
(assoc ::bfc/features (cfeat/get-team-enabled-features cf/flags team))
|
||||||
(assoc ::bfc/project-id project-id)
|
(assoc ::bfc/project-id project-id)
|
||||||
(assoc ::bfc/profile-id profile-id)
|
(assoc ::bfc/profile-id profile-id)
|
||||||
(assoc ::bfc/name name))
|
(assoc ::bfc/name name)
|
||||||
|
(assoc ::bfc/input (:path file)))
|
||||||
|
|
||||||
input-path (:path file)
|
result (case (int version)
|
||||||
owned? (some? upload-id)
|
|
||||||
|
|
||||||
cfg
|
|
||||||
(assoc cfg ::bfc/input input-path)
|
|
||||||
|
|
||||||
result
|
|
||||||
(try
|
|
||||||
(case (int version)
|
|
||||||
1 (bf.v1/import-files! cfg)
|
1 (bf.v1/import-files! cfg)
|
||||||
3 (bf.v3/import-files! cfg))
|
3 (bf.v3/import-files! cfg))]
|
||||||
(finally
|
|
||||||
(when owned?
|
|
||||||
(fs/delete input-path))))]
|
|
||||||
|
|
||||||
(db/update! pool :project
|
(db/update! pool :project
|
||||||
{:modified-at (ct/now)}
|
{:modified-at (ct/now)}
|
||||||
@ -117,18 +103,13 @@
|
|||||||
result))
|
result))
|
||||||
|
|
||||||
(def ^:private schema:import-binfile
|
(def ^:private schema:import-binfile
|
||||||
[:and
|
|
||||||
[:map {:title "import-binfile"}
|
[:map {:title "import-binfile"}
|
||||||
[:name [:or [:string {:max 250}]
|
[:name [:or [:string {:max 250}]
|
||||||
[:map-of ::sm/uuid [:string {:max 250}]]]]
|
[:map-of ::sm/uuid [:string {:max 250}]]]]
|
||||||
[:project-id ::sm/uuid]
|
[:project-id ::sm/uuid]
|
||||||
[:file-id {:optional true} ::sm/uuid]
|
[:file-id {:optional true} ::sm/uuid]
|
||||||
[:version {:optional true} ::sm/int]
|
[:version {:optional true} ::sm/int]
|
||||||
[:file {:optional true} media/schema:upload]
|
[:file media/schema:upload]])
|
||||||
[:upload-id {:optional true} ::sm/uuid]]
|
|
||||||
[:fn {:error/message "one of :file or :upload-id is required"}
|
|
||||||
(fn [{:keys [file upload-id]}]
|
|
||||||
(or (some? file) (some? upload-id)))]])
|
|
||||||
|
|
||||||
(sv/defmethod ::import-binfile
|
(sv/defmethod ::import-binfile
|
||||||
"Import a penpot file in a binary format. If `file-id` is provided,
|
"Import a penpot file in a binary format. If `file-id` is provided,
|
||||||
@ -136,20 +117,15 @@
|
|||||||
|
|
||||||
The in-place imports are only supported for binfile-v3 and when a
|
The in-place imports are only supported for binfile-v3 and when a
|
||||||
.penpot file only contains one penpot file.
|
.penpot file only contains one penpot file.
|
||||||
|
|
||||||
The file content may be provided either as a multipart `file` upload
|
|
||||||
or as an `upload-id` referencing a completed chunked-upload session,
|
|
||||||
which allows importing files larger than the multipart size limit.
|
|
||||||
"
|
"
|
||||||
{::doc/added "1.15"
|
{::doc/added "1.15"
|
||||||
::doc/changes ["1.20" "Add file-id param for in-place import"
|
::doc/changes ["1.20" "Add file-id param for in-place import"
|
||||||
"1.20" "Set default version to 3"
|
"1.20" "Set default version to 3"]
|
||||||
"2.15" "Add upload-id param for chunked upload support"]
|
|
||||||
|
|
||||||
::webhooks/event? true
|
::webhooks/event? true
|
||||||
::sse/stream? true
|
::sse/stream? true
|
||||||
::sm/params schema:import-binfile}
|
::sm/params schema:import-binfile}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id version file-id upload-id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id version file-id file] :as params}]
|
||||||
(projects/check-edition-permissions! pool profile-id project-id)
|
(projects/check-edition-permissions! pool profile-id project-id)
|
||||||
(let [version (or version 3)
|
(let [version (or version 3)
|
||||||
params (-> params
|
params (-> params
|
||||||
@ -160,16 +136,9 @@
|
|||||||
(uuid? file-id)
|
(uuid? file-id)
|
||||||
(assoc ::bfc/file-id file-id))
|
(assoc ::bfc/file-id file-id))
|
||||||
|
|
||||||
params
|
manifest (case (int version)
|
||||||
(if (some? upload-id)
|
|
||||||
(let [file (db/tx-run! cfg media-cmd/assemble-chunks upload-id)]
|
|
||||||
(assoc params :file file))
|
|
||||||
params)
|
|
||||||
|
|
||||||
manifest
|
|
||||||
(case (int version)
|
|
||||||
1 nil
|
1 nil
|
||||||
3 (bf.v3/get-manifest (-> params :file :path)))]
|
3 (bf.v3/get-manifest (:path file)))]
|
||||||
|
|
||||||
(with-meta
|
(with-meta
|
||||||
(sse/response (partial import-binfile cfg params))
|
(sse/response (partial import-binfile cfg params))
|
||||||
|
|||||||
@ -49,9 +49,9 @@
|
|||||||
:deleted-at (ct/in-future (cf/get-deletion-delay))
|
:deleted-at (ct/in-future (cf/get-deletion-delay))
|
||||||
:password (derive-password password)
|
:password (derive-password password)
|
||||||
:props {}}
|
:props {}}
|
||||||
profile (db/tx-run! cfg (fn [cfg]
|
profile (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||||
(->> (auth/create-profile cfg params)
|
(->> (auth/create-profile cfg params)
|
||||||
(auth/create-profile-rels cfg))))]
|
(auth/create-profile-rels conn))))]
|
||||||
(with-meta {:email email
|
(with-meta {:email email
|
||||||
:password password}
|
:password password}
|
||||||
{::audit/profile-id (:id profile)})))
|
{::audit/profile-id (:id profile)})))
|
||||||
|
|||||||
@ -13,7 +13,6 @@
|
|||||||
[app.common.features :as cfeat]
|
[app.common.features :as cfeat]
|
||||||
[app.common.files.helpers :as cfh]
|
[app.common.files.helpers :as cfh]
|
||||||
[app.common.files.migrations :as fmg]
|
[app.common.files.migrations :as fmg]
|
||||||
[app.common.files.stats :as cfs]
|
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.common.schema.desc-js-like :as-alias smdj]
|
[app.common.schema.desc-js-like :as-alias smdj]
|
||||||
@ -80,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?))
|
||||||
@ -100,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)
|
||||||
@ -152,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
|
||||||
@ -178,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
|
||||||
@ -241,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))))))
|
||||||
@ -386,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)})
|
||||||
@ -607,76 +678,6 @@
|
|||||||
(get-file-summary cfg id))
|
(get-file-summary cfg id))
|
||||||
|
|
||||||
|
|
||||||
;; --- COMMAND QUERY: get-file-stats
|
|
||||||
|
|
||||||
(def ^:private sql:file-stats-library-counts
|
|
||||||
"SELECT
|
|
||||||
(SELECT COUNT(*)
|
|
||||||
FROM file_library_rel AS flr
|
|
||||||
JOIN file AS fl ON (fl.id = flr.library_file_id)
|
|
||||||
WHERE flr.file_id = ?::uuid
|
|
||||||
AND (fl.deleted_at IS NULL OR fl.deleted_at > now())) AS library_count,
|
|
||||||
(SELECT COUNT(*)
|
|
||||||
FROM file_library_rel AS flr
|
|
||||||
JOIN file AS fl ON (fl.id = flr.file_id)
|
|
||||||
WHERE flr.library_file_id = ?::uuid
|
|
||||||
AND (fl.deleted_at IS NULL OR fl.deleted_at > now())) AS referenced_by_count")
|
|
||||||
|
|
||||||
(defn- get-file-stats-library-counts
|
|
||||||
[conn file-id]
|
|
||||||
(let [row (db/exec-one! conn [sql:file-stats-library-counts file-id file-id])]
|
|
||||||
{:library-count (or (:library-count row) 0)
|
|
||||||
:referenced-by-count (or (:referenced-by-count row) 0)}))
|
|
||||||
|
|
||||||
(defn- get-file-stats
|
|
||||||
[{:keys [::db/conn] :as cfg} file-id]
|
|
||||||
(let [file (bfc/get-file cfg file-id)
|
|
||||||
base (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)]
|
|
||||||
(cfs/calc-file-stats (:data file)))
|
|
||||||
lib-cnt (get-file-stats-library-counts conn file-id)]
|
|
||||||
(-> base
|
|
||||||
(merge lib-cnt)
|
|
||||||
(assoc :file-id file-id
|
|
||||||
:revn (:revn file)
|
|
||||||
:updated-at (:modified-at file)))))
|
|
||||||
|
|
||||||
(def ^:private schema:shape-counts
|
|
||||||
[:map {:title "FileStatsShapeCounts"}
|
|
||||||
[:total [::sm/int {:min 0}]]
|
|
||||||
[:by-type [:map-of :keyword [::sm/int {:min 0}]]]])
|
|
||||||
|
|
||||||
(def ^:private schema:get-file-stats-result
|
|
||||||
[:map {:title "FileStats"}
|
|
||||||
[:file-id ::sm/uuid]
|
|
||||||
[:page-count [::sm/int {:min 0}]]
|
|
||||||
[:shape-counts schema:shape-counts]
|
|
||||||
[:component-count [::sm/int {:min 0}]]
|
|
||||||
[:deleted-component-count [::sm/int {:min 0}]]
|
|
||||||
[:color-count [::sm/int {:min 0}]]
|
|
||||||
[:typography-count [::sm/int {:min 0}]]
|
|
||||||
[:library-count [::sm/int {:min 0}]]
|
|
||||||
[:referenced-by-count [::sm/int {:min 0}]]
|
|
||||||
[:revn [::sm/int {:min 0}]]
|
|
||||||
[:updated-at ::ct/inst]])
|
|
||||||
|
|
||||||
(def ^:private schema:get-file-stats
|
|
||||||
[:map {:title "get-file-stats"}
|
|
||||||
[:id ::sm/uuid]])
|
|
||||||
|
|
||||||
(sv/defmethod ::get-file-stats
|
|
||||||
"Return aggregate statistics for a single file: page count, shape
|
|
||||||
counts by type, component/color/typography counts, and inbound and
|
|
||||||
outbound library reference counts. Cheap alternative to `get-file`
|
|
||||||
when only metrics are needed."
|
|
||||||
{::doc/added "2.17"
|
|
||||||
::sm/params schema:get-file-stats
|
|
||||||
::sm/result schema:get-file-stats-result
|
|
||||||
::db/transaction true}
|
|
||||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id id]}]
|
|
||||||
(check-read-permissions! conn profile-id id)
|
|
||||||
(get-file-stats cfg id))
|
|
||||||
|
|
||||||
|
|
||||||
;; --- COMMAND QUERY: get-file-libraries
|
;; --- COMMAND QUERY: get-file-libraries
|
||||||
|
|
||||||
(def ^:private schema:get-file-libraries
|
(def ^:private schema:get-file-libraries
|
||||||
@ -687,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
|
||||||
|
|
||||||
@ -775,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,
|
||||||
@ -782,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)
|
||||||
@ -884,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)
|
||||||
@ -908,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}]
|
||||||
@ -923,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
|
||||||
@ -943,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}]
|
||||||
@ -956,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)))
|
||||||
@ -1010,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
|
||||||
@ -1076,19 +1075,19 @@
|
|||||||
"Link a file to a library. Returns the recursive list of libraries used by that library"
|
"Link a file to a library. Returns the recursive list of libraries used by that library"
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
::webhooks/event? true
|
::webhooks/event? true
|
||||||
::sm/params schema:link-file-to-library
|
::sm/params schema:link-file-to-library}
|
||||||
::db/transaction true}
|
[cfg {:keys [::rpc/profile-id file-id library-id] :as params}]
|
||||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id file-id library-id] :as params}]
|
|
||||||
|
|
||||||
(when (= file-id library-id)
|
(when (= file-id library-id)
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :invalid-library
|
:code :invalid-library
|
||||||
:hint "A file cannot be linked to itself"))
|
:hint "A file cannot be linked to itself"))
|
||||||
|
|
||||||
|
(db/tx-run! cfg
|
||||||
|
(fn [{:keys [::db/conn]}]
|
||||||
(check-edition-permissions! conn profile-id file-id)
|
(check-edition-permissions! conn profile-id file-id)
|
||||||
(check-edition-permissions! conn profile-id library-id)
|
(check-edition-permissions! conn profile-id library-id)
|
||||||
(link-file-to-library conn params)
|
(link-file-to-library conn params)
|
||||||
(bfc/get-libraries cfg [library-id]))
|
(bfc/get-libraries cfg [library-id]))))
|
||||||
|
|
||||||
;; --- MUTATION COMMAND: unlink-file-from-library
|
;; --- MUTATION COMMAND: unlink-file-from-library
|
||||||
|
|
||||||
@ -1108,9 +1107,8 @@
|
|||||||
::webhooks/event? true
|
::webhooks/event? true
|
||||||
::sm/params schema:unlink-file-to-library
|
::sm/params schema:unlink-file-to-library
|
||||||
::db/transaction true}
|
::db/transaction true}
|
||||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id file-id library-id] :as params}]
|
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id file-id] :as params}]
|
||||||
(check-edition-permissions! conn profile-id file-id)
|
(check-edition-permissions! conn profile-id file-id)
|
||||||
(check-edition-permissions! conn profile-id library-id)
|
|
||||||
(unlink-file-from-library conn params)
|
(unlink-file-from-library conn params)
|
||||||
nil)
|
nil)
|
||||||
|
|
||||||
@ -1134,9 +1132,8 @@
|
|||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
::sm/params schema:update-file-library-sync-status
|
::sm/params schema:update-file-library-sync-status
|
||||||
::db/transaction true}
|
::db/transaction true}
|
||||||
[{:keys [::db/conn]} {:keys [::rpc/profile-id file-id library-id] :as params}]
|
[{:keys [::db/conn]} {:keys [::rpc/profile-id file-id] :as params}]
|
||||||
(check-edition-permissions! conn profile-id file-id)
|
(check-edition-permissions! conn profile-id file-id)
|
||||||
(check-edition-permissions! conn profile-id library-id)
|
|
||||||
(update-sync conn params))
|
(update-sync conn params))
|
||||||
|
|
||||||
;; --- MUTATION COMMAND: ignore-sync
|
;; --- MUTATION COMMAND: ignore-sync
|
||||||
@ -1167,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 (
|
||||||
|
SELECT f.id
|
||||||
FROM file AS f
|
FROM file AS f
|
||||||
JOIN project AS p ON (p.id = f.project_id)
|
JOIN project AS p ON (p.id = f.project_id)
|
||||||
JOIN team AS t ON (t.id = p.team_id)
|
JOIN team AS t ON (t.id = p.team_id)
|
||||||
WHERE t.deleted_at IS NULL
|
WHERE t.deleted_at IS NULL
|
||||||
AND t.id = ?
|
AND t.id = ?
|
||||||
AND f.id = ANY(?::uuid[])")
|
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
|
||||||
|
|
||||||
@ -1277,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
|
||||||
@ -1300,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]
|
||||||
|
|||||||
@ -8,7 +8,6 @@
|
|||||||
(:require
|
(:require
|
||||||
[app.binfile.common :as bfc]
|
[app.binfile.common :as bfc]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.features :as-alias cfeat]
|
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.common.time :as ct]
|
[app.common.time :as ct]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
@ -36,43 +35,6 @@
|
|||||||
(files/check-read-permissions! conn profile-id file-id)
|
(files/check-read-permissions! conn profile-id file-id)
|
||||||
(fsnap/get-visible-snapshots conn file-id))))
|
(fsnap/get-visible-snapshots conn file-id))))
|
||||||
|
|
||||||
;; --- COMMAND QUERY: get-file-snapshot
|
|
||||||
|
|
||||||
(def ^:private schema:get-file-snapshot
|
|
||||||
[:map {:title "get-file-snapshot"}
|
|
||||||
[:file-id ::sm/uuid]
|
|
||||||
[:id ::sm/uuid]
|
|
||||||
[:features {:optional true} ::cfeat/features]])
|
|
||||||
|
|
||||||
(sv/defmethod ::get-file-snapshot
|
|
||||||
"Retrieve a file bundle with data from a specific snapshot for
|
|
||||||
read-only preview. Does not modify any database state."
|
|
||||||
{::doc/added "2.16"
|
|
||||||
::sm/params schema:get-file-snapshot
|
|
||||||
::sm/result files/schema:file-with-permissions
|
|
||||||
::db/transaction true}
|
|
||||||
[{:keys [::db/conn] :as cfg} {:keys [::rpc/profile-id file-id id] :as params}]
|
|
||||||
(let [perms (bfc/get-file-permissions conn profile-id file-id)]
|
|
||||||
(files/check-read-permissions! perms)
|
|
||||||
(let [snapshot (fsnap/get-snapshot-data cfg file-id id)]
|
|
||||||
(when-not snapshot
|
|
||||||
(ex/raise :type :not-found
|
|
||||||
:code :snapshot-not-found
|
|
||||||
:hint "unable to find snapshot with the provided id"
|
|
||||||
:snapshot-id id
|
|
||||||
:file-id file-id))
|
|
||||||
;; Load current file metadata only (no data decoding) then overlay
|
|
||||||
;; the snapshot data so the client receives the same shape as a
|
|
||||||
;; normal get-file response but with historical page/object content.
|
|
||||||
(let [base-file (bfc/get-file cfg file-id :load-data? false)]
|
|
||||||
(-> base-file
|
|
||||||
(assoc :data (:data snapshot))
|
|
||||||
(assoc :version (:version snapshot))
|
|
||||||
(assoc :features (:features snapshot))
|
|
||||||
(assoc :revn (:revn snapshot))
|
|
||||||
(assoc :vern (rand-int 100000))
|
|
||||||
(assoc :permissions perms))))))
|
|
||||||
|
|
||||||
(def ^:private schema:create-file-snapshot
|
(def ^:private schema:create-file-snapshot
|
||||||
[:map
|
[:map
|
||||||
[:file-id ::sm/uuid]
|
[:file-id ::sm/uuid]
|
||||||
@ -109,7 +71,7 @@
|
|||||||
{::doc/added "1.20"
|
{::doc/added "1.20"
|
||||||
::sm/params schema:restore-file-snapshot
|
::sm/params schema:restore-file-snapshot
|
||||||
::db/transaction true}
|
::db/transaction true}
|
||||||
[{:keys [::db/conn ::mbus/msgbus] :as cfg} {:keys [::rpc/profile-id ::rpc/session-id file-id id] :as params}]
|
[{:keys [::db/conn ::mbus/msgbus] :as cfg} {:keys [::rpc/profile-id file-id id] :as params}]
|
||||||
(files/check-edition-permissions! conn profile-id file-id)
|
(files/check-edition-permissions! conn profile-id file-id)
|
||||||
(let [file (bfc/get-file cfg file-id)
|
(let [file (bfc/get-file cfg file-id)
|
||||||
team (teams/get-team conn
|
team (teams/get-team conn
|
||||||
@ -126,8 +88,7 @@
|
|||||||
;; Send to the clients a notification to reload the file
|
;; Send to the clients a notification to reload the file
|
||||||
(mbus/pub! msgbus
|
(mbus/pub! msgbus
|
||||||
:topic (:id file)
|
:topic (:id file)
|
||||||
:message {:type :file-restored
|
:message {:type :file-restore
|
||||||
:session-id session-id
|
|
||||||
:file-id (:id file)
|
:file-id (:id file)
|
||||||
:vern vern})
|
:vern vern})
|
||||||
nil)))
|
nil)))
|
||||||
|
|||||||
@ -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)})))))
|
||||||
|
|||||||
@ -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")}))))
|
|
||||||
|
|||||||
@ -84,5 +84,5 @@
|
|||||||
(profile/get-profile-by-email conn))
|
(profile/get-profile-by-email conn))
|
||||||
(->> (assoc info :is-active true :is-demo false)
|
(->> (assoc info :is-active true :is-demo false)
|
||||||
(auth/create-profile cfg)
|
(auth/create-profile cfg)
|
||||||
(auth/create-profile-rels cfg)
|
(auth/create-profile-rels conn)
|
||||||
(profile/strip-private-attrs))))))
|
(profile/strip-private-attrs))))))
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user