Compare commits
11 Commits
main
...
feat-audit
| Author | SHA1 | Date |
|---|---|---|
|
|
ab038ec6c4 | |
|
|
ae908be716 | |
|
|
126ea78dac | |
|
|
3fc5813888 | |
|
|
3d8c259242 | |
|
|
6ab03d3f87 | |
|
|
de22af0048 | |
|
|
4ff8181a47 | |
|
|
939daaf1ab | |
|
|
a4bbd02d6a | |
|
|
5db25ab75c |
|
|
@ -0,0 +1,3 @@
|
|||
source_url "https://raw.githubusercontent.com/cachix/devenv/95f329d49a8a5289d31e0982652f7058a189bfca/direnvrc" "sha256-d+8cBpDfDBj41inrADaJt+bDWhOktwslgoP5YiGJ1v0="
|
||||
|
||||
use devenv
|
||||
|
|
@ -79,7 +79,7 @@ runs:
|
|||
} >> "$GITHUB_ENV"
|
||||
|
||||
- name: Download Mage binary
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
name: mage_bin
|
||||
|
||||
|
|
@ -89,7 +89,7 @@ runs:
|
|||
|
||||
- name: Download frontend dist (vikunja only)
|
||||
if: inputs.project == 'vikunja'
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
name: frontend_dist
|
||||
path: frontend/dist
|
||||
|
|
@ -110,7 +110,7 @@ runs:
|
|||
sudo mv upx-5.0.0-amd64_linux/upx /usr/local/bin
|
||||
|
||||
- name: Setup xgo cache
|
||||
uses: useblacksmith/cache@c5fe29eb0efdf1cf4186b9f7fcbbcbc0cf025662 # v5.1.0
|
||||
uses: useblacksmith/cache@71c7c918062ba3861252d84b07fe5ab2a6b467a6 # v5
|
||||
with:
|
||||
path: /home/runner/.xgo-cache
|
||||
key: xgo-${{ inputs.project }}-${{ hashFiles('**/go.sum') }}
|
||||
|
|
@ -133,7 +133,7 @@ runs:
|
|||
cd build && mage release:build "$PROJECT"
|
||||
|
||||
- name: GPG setup
|
||||
uses: kolaente/action-gpg@eb0fd8f16fe9b499f060f659092c470cb9f76eb7 # main
|
||||
uses: kolaente/action-gpg@main
|
||||
with:
|
||||
gpg-passphrase: ${{ inputs.gpg-passphrase }}
|
||||
gpg-sign-key: ${{ inputs.gpg-sign-key }}
|
||||
|
|
@ -164,7 +164,7 @@ runs:
|
|||
done
|
||||
|
||||
- name: Upload zips to S3
|
||||
uses: kolaente/s3-action@7f58dddd682b2f93a6c6799c9f68e7a38f2da558 # main
|
||||
uses: kolaente/s3-action@main
|
||||
with:
|
||||
s3-access-key-id: ${{ inputs.s3-access-key-id }}
|
||||
s3-secret-access-key: ${{ inputs.s3-secret-access-key }}
|
||||
|
|
@ -176,14 +176,14 @@ runs:
|
|||
strip-path-prefix: ${{ env.DIST_PREFIX }}/zip/
|
||||
|
||||
- name: Store binaries
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: ${{ env.ARTIFACT_BINARIES_NAME }}
|
||||
path: ./${{ env.DIST_PREFIX }}/binaries/*
|
||||
|
||||
- name: Store binary packages
|
||||
if: github.ref_type == 'tag'
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: ${{ env.ARTIFACT_ZIPS_NAME }}
|
||||
path: ./${{ env.DIST_PREFIX }}/zip/*
|
||||
|
|
|
|||
|
|
@ -91,12 +91,12 @@ runs:
|
|||
echo "S3_TARGET_PATH=/${PROJECT}/${VERSION_OR_UNSTABLE}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Download project binaries
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
name: ${{ env.BINARIES_ARTIFACT_NAME }}
|
||||
path: ${{ env.BINARIES_DOWNLOAD_PATH }}
|
||||
|
||||
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
|
||||
with:
|
||||
go-version: stable
|
||||
|
||||
|
|
@ -123,7 +123,7 @@ runs:
|
|||
|
||||
- name: GPG setup for archlinux signing
|
||||
if: inputs.packager == 'archlinux'
|
||||
uses: kolaente/action-gpg@eb0fd8f16fe9b499f060f659092c470cb9f76eb7 # main
|
||||
uses: kolaente/action-gpg@main
|
||||
with:
|
||||
gpg-passphrase: ${{ inputs.gpg-passphrase }}
|
||||
gpg-sign-key: ${{ inputs.gpg-sign-key }}
|
||||
|
|
@ -163,7 +163,7 @@ runs:
|
|||
run: mkdir -p "$PACKAGE_OUTPUT_DIR"
|
||||
|
||||
- name: Create package
|
||||
uses: kolaente/action-gh-nfpm@08460c16ce3baaa48eaf94d51eea0e653b15d955 # master
|
||||
uses: kolaente/action-gh-nfpm@master
|
||||
with:
|
||||
packager: ${{ inputs.packager }}
|
||||
target: ${{ env.PACKAGE_OUTPUT_DIR }}/${{ env.PACKAGE_FILENAME }}
|
||||
|
|
@ -186,7 +186,7 @@ runs:
|
|||
"$PACKAGE_OUTPUT_DIR/$PACKAGE_FILENAME"
|
||||
|
||||
- name: Upload to S3
|
||||
uses: kolaente/s3-action@7f58dddd682b2f93a6c6799c9f68e7a38f2da558 # main
|
||||
uses: kolaente/s3-action@main
|
||||
with:
|
||||
s3-access-key-id: ${{ inputs.s3-access-key-id }}
|
||||
s3-secret-access-key: ${{ inputs.s3-secret-access-key }}
|
||||
|
|
@ -198,7 +198,7 @@ runs:
|
|||
strip-path-prefix: ${{ env.PACKAGE_OUTPUT_DIR }}/
|
||||
|
||||
- name: Store OS package
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: ${{ env.ARTIFACT_NAME }}
|
||||
path: ${{ env.PACKAGE_OUTPUT_DIR }}/*
|
||||
|
|
|
|||
|
|
@ -16,11 +16,11 @@ runs:
|
|||
echo "CYPRESS_INSTALL_BINARY=0" >> $GITHUB_ENV
|
||||
echo "PUPPETEER_SKIP_DOWNLOAD=true" >> $GITHUB_ENV
|
||||
echo "PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1" >> $GITHUB_ENV
|
||||
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0
|
||||
- uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
with:
|
||||
run_install: false
|
||||
package_json_file: frontend/package.json
|
||||
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
with:
|
||||
node-version-file: frontend/.nvmrc
|
||||
cache: 'pnpm'
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout (for prompt template)
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
sparse-checkout: |
|
||||
.github/workflows/auto-label.prompt.md
|
||||
|
|
@ -29,7 +29,7 @@ jobs:
|
|||
|
||||
- name: Render system prompt from live labels
|
||||
id: render
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
env:
|
||||
PROMPT_TEMPLATE_PATH: .github/workflows/auto-label.prompt.md
|
||||
with:
|
||||
|
|
@ -122,7 +122,7 @@ jobs:
|
|||
|
||||
- name: Classify with AI
|
||||
id: classify
|
||||
uses: actions/ai-inference@a7805884c80886efc241e94a5351df715968a0ad # v2.1.1
|
||||
uses: actions/ai-inference@e09e65981758de8b2fdab13c2bfb7c7d5493b0b6 # v2.0.7
|
||||
with:
|
||||
model: openai/gpt-4.1-mini
|
||||
# GPT-5 is a reasoning model: output tokens include reasoning, so budget generously.
|
||||
|
|
@ -132,7 +132,7 @@ jobs:
|
|||
prompt-file: ${{ steps.prep.outputs.prompt_path }}
|
||||
|
||||
- name: Apply labels
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
env:
|
||||
AI_RESPONSE: ${{ steps.classify.outputs.response }}
|
||||
with:
|
||||
|
|
|
|||
|
|
@ -9,19 +9,19 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
persist-credentials: true
|
||||
- name: push source files
|
||||
uses: crowdin/github-action@52aa776766211d83d975df51f3b9c53c2f8ba35f # v2.16.3
|
||||
uses: crowdin/github-action@b4b468cffefb50bdd99dd83e5d2eaeb63c880380 # v2
|
||||
with:
|
||||
command: 'push'
|
||||
env:
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
- name: pull translations
|
||||
uses: crowdin/github-action@52aa776766211d83d975df51f3b9c53c2f8ba35f # v2.16.3
|
||||
uses: crowdin/github-action@b4b468cffefb50bdd99dd83e5d2eaeb63c880380 # v2
|
||||
with:
|
||||
command: 'download'
|
||||
command_args: '--export-only-approved --skip-untranslated-strings'
|
||||
|
|
@ -29,7 +29,7 @@ jobs:
|
|||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
with:
|
||||
node-version-file: frontend/.nvmrc
|
||||
- name: Ensure file permissions
|
||||
|
|
@ -55,7 +55,7 @@ jobs:
|
|||
git commit -m "chore(i18n): update translations via Crowdin"
|
||||
- name: Push changes
|
||||
if: steps.check_changes.outputs.changes_exist != '0'
|
||||
uses: ad-m/github-push-action@881a6320fdb16eb5318c5054f31c218aec2b324c # master
|
||||
uses: ad-m/github-push-action@master
|
||||
with:
|
||||
ssh: true
|
||||
branch: ${{ github.ref }}
|
||||
|
|
|
|||
|
|
@ -18,11 +18,11 @@ jobs:
|
|||
directory: [frontend, desktop]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Create Diff
|
||||
uses: e18e/action-dependency-diff@8e9b8c1957ab066d36235a43f4c1ff1522e1bdbc # v1.6.1
|
||||
uses: e18e/action-dependency-diff@v1
|
||||
with:
|
||||
working-directory: ${{ matrix.directory }}
|
||||
|
||||
|
|
@ -33,11 +33,11 @@ jobs:
|
|||
directory: [frontend, desktop]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Check provenance downgrades
|
||||
uses: danielroe/provenance-action@81568f71211c1839d6d3583c6a93037f5348c816 # main
|
||||
uses: danielroe/provenance-action@main
|
||||
with:
|
||||
workspace-path: ${{ matrix.directory }}
|
||||
fail-on-provenance-change: true
|
||||
|
|
|
|||
|
|
@ -10,14 +10,14 @@ jobs:
|
|||
steps:
|
||||
- name: Generate GitHub App token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2.2.2
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
|
||||
with:
|
||||
app-id: ${{ secrets.BOT_APP_ID }}
|
||||
private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Find closing PR or commit
|
||||
id: find-closer
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
|
|
@ -82,7 +82,7 @@ jobs:
|
|||
|
||||
- name: Comment on issue
|
||||
if: steps.find-closer.outputs.closed_by_code == 'true'
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
with:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ jobs:
|
|||
docker-images: false
|
||||
swap-storage: false
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
# For pull_request_target, we need to explicitly fetch the PR ref from forks
|
||||
# since the PR's commit SHA is not reachable in the base repository.
|
||||
|
|
@ -34,27 +34,27 @@ jobs:
|
|||
ref: refs/pull/${{ github.event.pull_request.number }}/head
|
||||
- name: Git describe
|
||||
id: ghd
|
||||
uses: proudust/gh-describe@80412be8ce0e77d8afba6b340e34790bc772aa45 # v2.2.0
|
||||
uses: proudust/gh-describe@v2
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
||||
with:
|
||||
version: latest
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}
|
||||
tags: |
|
||||
type=ref,event=pr
|
||||
type=sha,format=long
|
||||
- name: Build and push PR image
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
|
|
@ -66,7 +66,7 @@ jobs:
|
|||
build-args: |
|
||||
RELEASE_VERSION=${{ steps.ghd.outputs.describe }}
|
||||
- name: Comment on PR
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
||||
env:
|
||||
DOCKER_META_TAGS: ${{ steps.meta.outputs.tags }}
|
||||
with:
|
||||
|
|
|
|||
|
|
@ -8,14 +8,14 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
name: prepare-build-mage
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
|
||||
with:
|
||||
go-version: stable
|
||||
- name: Cache build mage
|
||||
id: cache-build-mage
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5
|
||||
with:
|
||||
key: ${{ runner.os }}-build-mage-build-${{ hashFiles('build/magefile.go') }}
|
||||
path: |
|
||||
|
|
@ -33,7 +33,7 @@ jobs:
|
|||
export PATH=$PATH:$GOPATH/bin
|
||||
mage -compile ./build-mage-static
|
||||
- name: Store build mage binary
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: build_mage_bin
|
||||
path: ./build/build-mage-static
|
||||
|
|
@ -43,14 +43,14 @@ jobs:
|
|||
steps:
|
||||
- name: Git describe
|
||||
id: ghd
|
||||
uses: proudust/gh-describe@80412be8ce0e77d8afba6b340e34790bc772aa45 # v2.2.0
|
||||
uses: proudust/gh-describe@v2
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
|
|
@ -58,7 +58,7 @@ jobs:
|
|||
- name: Docker meta version
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
id: meta
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5
|
||||
with:
|
||||
images: |
|
||||
vikunja/vikunja
|
||||
|
|
@ -70,7 +70,7 @@ jobs:
|
|||
type=raw,value=latest
|
||||
- name: Build and push unstable
|
||||
if: ${{ github.ref_type != 'tag' }}
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
|
||||
push: true
|
||||
|
|
@ -81,7 +81,7 @@ jobs:
|
|||
RELEASE_VERSION=${{ steps.ghd.outputs.describe }}
|
||||
- name: Build and push version
|
||||
if: ${{ github.ref_type == 'tag' }}
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
|
||||
push: true
|
||||
|
|
@ -93,10 +93,10 @@ jobs:
|
|||
binaries:
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2204
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
- name: Git describe
|
||||
id: ghd
|
||||
uses: proudust/gh-describe@80412be8ce0e77d8afba6b340e34790bc772aa45 # v2.2.0
|
||||
uses: proudust/gh-describe@v2
|
||||
- uses: ./.github/actions/release-binaries
|
||||
with:
|
||||
project: vikunja
|
||||
|
|
@ -112,10 +112,10 @@ jobs:
|
|||
veans-binaries:
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2204
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
- name: Git describe
|
||||
id: ghd
|
||||
uses: proudust/gh-describe@80412be8ce0e77d8afba6b340e34790bc772aa45 # v2.2.0
|
||||
uses: proudust/gh-describe@v2
|
||||
- uses: ./.github/actions/release-binaries
|
||||
with:
|
||||
project: veans
|
||||
|
|
@ -147,10 +147,10 @@ jobs:
|
|||
pkg: armv7
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
- name: Git describe
|
||||
id: ghd
|
||||
uses: proudust/gh-describe@80412be8ce0e77d8afba6b340e34790bc772aa45 # v2.2.0
|
||||
uses: proudust/gh-describe@v2
|
||||
- uses: ./.github/actions/release-os-package
|
||||
with:
|
||||
project: vikunja
|
||||
|
|
@ -186,10 +186,10 @@ jobs:
|
|||
pkg: armv7
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
- name: Git describe
|
||||
id: ghd
|
||||
uses: proudust/gh-describe@80412be8ce0e77d8afba6b340e34790bc772aa45 # v2.2.0
|
||||
uses: proudust/gh-describe@v2
|
||||
- uses: ./.github/actions/release-os-package
|
||||
with:
|
||||
project: veans
|
||||
|
|
@ -235,19 +235,19 @@ jobs:
|
|||
REPO_SUITE: ${{ github.ref_type == 'tag' && 'stable' || 'unstable' }}
|
||||
RELEASE_VERSION: unstable
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
|
||||
- name: Download build mage binary
|
||||
# Statically compiled in test.yml's build-mage job so it runs inside
|
||||
# ubuntu/fedora/archlinux containers without a Go toolchain.
|
||||
if: matrix.format != 'apk'
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
name: build_mage_bin
|
||||
path: build
|
||||
|
||||
- name: Download all server OS packages
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
pattern: vikunja_os_package_*
|
||||
merge-multiple: true
|
||||
|
|
@ -257,14 +257,14 @@ jobs:
|
|||
# Merged into the same incoming dir so reprepro / createrepo_c /
|
||||
# repo-add / the apk loop pick them up alongside vikunja's packages
|
||||
# — same suite, same arch fan-out, no extra source entry for users.
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
pattern: veans_os_package_*
|
||||
merge-multiple: true
|
||||
path: dist/repo-work/incoming
|
||||
|
||||
- name: Download desktop packages (Linux)
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
name: vikunja_desktop_packages_ubuntu-latest
|
||||
path: dist/repo-work/incoming-desktop
|
||||
|
|
@ -309,7 +309,7 @@ jobs:
|
|||
|
||||
- name: GPG setup
|
||||
if: matrix.format != 'apk'
|
||||
uses: kolaente/action-gpg@eb0fd8f16fe9b499f060f659092c470cb9f76eb7 # main
|
||||
uses: kolaente/action-gpg@main
|
||||
with:
|
||||
gpg-passphrase: "${{ secrets.RELEASE_GPG_PASSPHRASE }}"
|
||||
gpg-sign-key: "${{ secrets.RELEASE_GPG_SIGN_KEY }}"
|
||||
|
|
@ -384,7 +384,7 @@ jobs:
|
|||
find dist/repo-output -type d -empty -delete 2>/dev/null || true
|
||||
|
||||
- name: Upload to R2
|
||||
uses: kolaente/s3-action@7f58dddd682b2f93a6c6799c9f68e7a38f2da558 # main
|
||||
uses: kolaente/s3-action@main
|
||||
with:
|
||||
s3-access-key-id: ${{ secrets.S3_ACCESS_KEY }}
|
||||
s3-secret-access-key: ${{ secrets.S3_SECRET_KEY }}
|
||||
|
|
@ -398,12 +398,12 @@ jobs:
|
|||
config-yaml:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
- name: Git describe
|
||||
id: ghd
|
||||
uses: proudust/gh-describe@80412be8ce0e77d8afba6b340e34790bc772aa45 # v2.2.0
|
||||
uses: proudust/gh-describe@v2
|
||||
- name: Download Mage Binary
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
name: mage_bin
|
||||
- name: generate
|
||||
|
|
@ -411,7 +411,7 @@ jobs:
|
|||
chmod +x ./mage-static
|
||||
./mage-static generate:config-yaml 1
|
||||
- name: Upload to S3
|
||||
uses: kolaente/s3-action@7f58dddd682b2f93a6c6799c9f68e7a38f2da558 # main
|
||||
uses: kolaente/s3-action@main
|
||||
with:
|
||||
s3-access-key-id: ${{ secrets.S3_ACCESS_KEY }}
|
||||
s3-secret-access-key: ${{ secrets.S3_SECRET_KEY }}
|
||||
|
|
@ -431,16 +431,16 @@ jobs:
|
|||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
- name: Git describe
|
||||
id: ghd
|
||||
uses: proudust/gh-describe@80412be8ce0e77d8afba6b340e34790bc772aa45 # v2.2.0
|
||||
uses: proudust/gh-describe@v2
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
|
||||
with:
|
||||
package_json_file: desktop/package.json
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
||||
with:
|
||||
node-version-file: frontend/.nvmrc
|
||||
cache: pnpm
|
||||
|
|
@ -451,7 +451,7 @@ jobs:
|
|||
sudo apt-get update
|
||||
sudo apt-get install --no-install-recommends -y libopenjp2-tools rpm libarchive-tools
|
||||
- name: get frontend
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
name: frontend_dist
|
||||
path: frontend/dist
|
||||
|
|
@ -461,7 +461,7 @@ jobs:
|
|||
pnpm install --frozen-lockfile --prefer-offline --fetch-timeout 100000
|
||||
node build.js "${{ steps.ghd.outputs.describe }}" ${{ github.ref_type == 'tag' }}
|
||||
- name: Upload to S3
|
||||
uses: kolaente/s3-action@7f58dddd682b2f93a6c6799c9f68e7a38f2da558 # main
|
||||
uses: kolaente/s3-action@main
|
||||
with:
|
||||
s3-access-key-id: ${{ secrets.S3_ACCESS_KEY }}
|
||||
s3-secret-access-key: ${{ secrets.S3_SECRET_KEY }}
|
||||
|
|
@ -473,7 +473,7 @@ jobs:
|
|||
strip-path-prefix: desktop/dist/
|
||||
exclude: "desktop/dist/*.blockmap"
|
||||
- name: Store Desktop Package
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: vikunja_desktop_packages_${{ matrix.os }}
|
||||
path: |
|
||||
|
|
@ -486,16 +486,16 @@ jobs:
|
|||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
with:
|
||||
ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
persist-credentials: true
|
||||
- name: Download Mage Binary
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
name: mage_bin
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
|
||||
with:
|
||||
go-version: stable
|
||||
- name: generate
|
||||
|
|
@ -520,7 +520,7 @@ jobs:
|
|||
git commit -am "[skip ci] Updated swagger docs"
|
||||
- name: Push changes
|
||||
if: steps.check_changes.outputs.changes_exist != '0'
|
||||
uses: ad-m/github-push-action@881a6320fdb16eb5318c5054f31c218aec2b324c # master
|
||||
uses: ad-m/github-push-action@master
|
||||
with:
|
||||
ssh: true
|
||||
branch: ${{ github.ref }}
|
||||
|
|
@ -539,44 +539,44 @@ jobs:
|
|||
contents: write
|
||||
steps:
|
||||
- name: Download Binaries
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
name: vikunja_bin_packages
|
||||
|
||||
- name: Download OS Packages
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
pattern: vikunja_os_package_*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Download Veans Binaries
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
name: veans_bin_packages
|
||||
|
||||
- name: Download Veans OS Packages
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
pattern: veans_os_package_*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Download Desktop Package Linux
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
name: vikunja_desktop_packages_ubuntu-latest
|
||||
|
||||
- name: Download Desktop Package MacOS
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
name: vikunja_desktop_packages_macos-latest
|
||||
|
||||
- name: Download Desktop Package Windows
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
name: vikunja_desktop_packages_windows-latest
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2
|
||||
if: github.ref_type == 'tag'
|
||||
with:
|
||||
draft: true
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
only-labels: 'waiting for reply'
|
||||
days-before-issue-stale: 30
|
||||
|
|
@ -24,7 +24,6 @@ jobs:
|
|||
questions. If you're still seeing this on a recent version, just
|
||||
drop a comment with the requested info and we'll reopen. Thanks
|
||||
for the report!
|
||||
stale-pr-label: 'waiting for reply'
|
||||
days-before-pr-stale: 30
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
operations-per-run: 100
|
||||
|
|
|
|||
|
|
@ -8,26 +8,26 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
name: prepare-mage
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
|
||||
with:
|
||||
go-version: stable
|
||||
- name: Cache Mage
|
||||
id: cache-mage
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5
|
||||
with:
|
||||
key: ${{ runner.os }}-build-mage-${{ hashFiles('magefile.go') }}
|
||||
path: |
|
||||
./mage-static
|
||||
- name: Compile Mage
|
||||
if: ${{ steps.cache-mage.outputs.cache-hit != 'true' }}
|
||||
uses: magefile/mage-action@6f50bbb8ea47d56e62dee92392788acbc8192d0b # v3.1.0
|
||||
uses: magefile/mage-action@6f50bbb8ea47d56e62dee92392788acbc8192d0b # v3
|
||||
with:
|
||||
version: latest
|
||||
args: -compile ./mage-static
|
||||
- name: Store Mage Binary
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: mage_bin
|
||||
path: ./mage-static
|
||||
|
|
@ -36,16 +36,16 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
needs: mage
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
- name: Download Mage Binary
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
name: mage_bin
|
||||
- name: Git describe
|
||||
id: ghd
|
||||
uses: proudust/gh-describe@80412be8ce0e77d8afba6b340e34790bc772aa45 # v2.2.0
|
||||
uses: proudust/gh-describe@v2
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
|
||||
with:
|
||||
go-version: stable
|
||||
- name: Build
|
||||
|
|
@ -57,7 +57,7 @@ jobs:
|
|||
chmod +x ./mage-static
|
||||
./mage-static build
|
||||
- name: Store Vikunja Binary
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: vikunja_bin
|
||||
path: ./vikunja
|
||||
|
|
@ -65,8 +65,8 @@ jobs:
|
|||
api-lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
|
||||
with:
|
||||
go-version: stable
|
||||
- name: prepare frontend files
|
||||
|
|
@ -74,19 +74,19 @@ jobs:
|
|||
mkdir -p frontend/dist
|
||||
touch frontend/dist/index.html
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0
|
||||
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9
|
||||
with:
|
||||
version: v2.10.1
|
||||
|
||||
veans-lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
|
||||
with:
|
||||
go-version: stable
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0
|
||||
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9
|
||||
with:
|
||||
version: v2.10.1
|
||||
working-directory: veans
|
||||
|
|
@ -94,8 +94,8 @@ jobs:
|
|||
veans-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
|
||||
with:
|
||||
go-version: stable
|
||||
- name: Install mage
|
||||
|
|
@ -115,9 +115,9 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
needs: mage
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
- name: Download Mage Binary
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
name: mage_bin
|
||||
- name: Check
|
||||
|
|
@ -152,7 +152,7 @@ jobs:
|
|||
ports:
|
||||
- 3306:3306
|
||||
migration-smoke-db-postgres:
|
||||
image: postgres:18@sha256:4aabea78cf39b90e834caf3af7d602a18565f6fe2508705c8d01aa63245c2e20
|
||||
image: postgres:18@sha256:5773fe724c49c42a7a9ca70202e11e1dff21fb7235b335a73f39297d200b73a2
|
||||
env:
|
||||
POSTGRES_PASSWORD: vikunjatest
|
||||
POSTGRES_DB: vikunjatest
|
||||
|
|
@ -164,7 +164,7 @@ jobs:
|
|||
wget https://dl.vikunja.io/vikunja/unstable/vikunja-unstable-linux-amd64-full.zip -q -O vikunja-latest.zip
|
||||
unzip vikunja-latest.zip vikunja-unstable-linux-amd64
|
||||
- name: Download Vikunja Binary
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
name: vikunja_bin
|
||||
- name: run migration
|
||||
|
|
@ -254,13 +254,13 @@ jobs:
|
|||
ports:
|
||||
- 389:389
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
- name: Download Mage Binary
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
name: mage_bin
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
|
||||
with:
|
||||
go-version: stable
|
||||
- name: Configure Postgres for faster tests
|
||||
|
|
@ -300,13 +300,13 @@ jobs:
|
|||
needs:
|
||||
- mage
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
- name: Download Mage Binary
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
name: mage_bin
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
|
||||
with:
|
||||
go-version: stable
|
||||
- name: test
|
||||
|
|
@ -321,13 +321,13 @@ jobs:
|
|||
needs:
|
||||
- mage
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
- name: Download Mage Binary
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
name: mage_bin
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
|
||||
with:
|
||||
go-version: stable
|
||||
- name: test
|
||||
|
|
@ -351,13 +351,13 @@ jobs:
|
|||
ports:
|
||||
- 9000:9000
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
- name: Download Mage Binary
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
name: mage_bin
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
|
||||
with:
|
||||
go-version: stable
|
||||
- name: test S3 file storage integration
|
||||
|
|
@ -382,7 +382,7 @@ jobs:
|
|||
frontend-lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
- uses: ./.github/actions/setup-frontend
|
||||
- name: Lint
|
||||
working-directory: frontend
|
||||
|
|
@ -391,7 +391,7 @@ jobs:
|
|||
frontend-stylelint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
- uses: ./.github/actions/setup-frontend
|
||||
- name: Lint styles
|
||||
working-directory: frontend
|
||||
|
|
@ -400,7 +400,7 @@ jobs:
|
|||
frontend-typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
- uses: ./.github/actions/setup-frontend
|
||||
- name: Typecheck
|
||||
continue-on-error: true
|
||||
|
|
@ -410,7 +410,7 @@ jobs:
|
|||
test-frontend-unit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
- uses: ./.github/actions/setup-frontend
|
||||
- name: Run unit tests
|
||||
working-directory: frontend
|
||||
|
|
@ -419,11 +419,11 @@ jobs:
|
|||
frontend-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
- uses: ./.github/actions/setup-frontend
|
||||
- name: Git describe
|
||||
id: ghd
|
||||
uses: proudust/gh-describe@80412be8ce0e77d8afba6b340e34790bc772aa45 # v2.2.0
|
||||
uses: proudust/gh-describe@v2
|
||||
- name: Inject frontend version
|
||||
working-directory: frontend
|
||||
run: |
|
||||
|
|
@ -432,7 +432,7 @@ jobs:
|
|||
working-directory: frontend
|
||||
run: pnpm build
|
||||
- name: Store Frontend
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: frontend_dist
|
||||
path: ./frontend/dist
|
||||
|
|
@ -442,13 +442,13 @@ jobs:
|
|||
needs:
|
||||
- api-build
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
- name: Download Vikunja Binary
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
name: vikunja_bin
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
|
||||
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
|
||||
with:
|
||||
go-version: stable
|
||||
- name: Install mage
|
||||
|
|
@ -501,7 +501,7 @@ jobs:
|
|||
(cd veans && mage test:e2e)
|
||||
- name: Upload API log on failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: veans-e2e-vikunja-log
|
||||
path: /tmp/vikunja.log
|
||||
|
|
@ -523,19 +523,19 @@ jobs:
|
|||
ports:
|
||||
- 5556:5556
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.61.1-jammy@sha256:7b86926fff94374389e8e1f4fdc5c76d050d4a06a7886bb537bf412b20e2b71e
|
||||
image: mcr.microsoft.com/playwright:v1.58.2-jammy@sha256:4698a73749c5848d3f5fcd42a2174d172fcad2b2283e087843b115424303a565
|
||||
options: --user 1001
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||
- name: Download Vikunja Binary
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
name: vikunja_bin
|
||||
- uses: ./.github/actions/setup-frontend
|
||||
with:
|
||||
install-e2e-binaries: false # Playwright browsers already in container
|
||||
- name: Download Frontend
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
name: frontend_dist
|
||||
path: ./frontend/dist
|
||||
|
|
@ -570,14 +570,14 @@ jobs:
|
|||
VIKUNJA_AUTH_OPENID_PROVIDERS_DEX_CLIENTID: vikunja
|
||||
VIKUNJA_AUTH_OPENID_PROVIDERS_DEX_CLIENTSECRET: secret
|
||||
- name: Upload Playwright Report
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report-${{ matrix.shard }}
|
||||
path: frontend/playwright-report/
|
||||
retention-days: 30
|
||||
- name: Upload Test Results
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-test-results-${{ matrix.shard }}
|
||||
|
|
|
|||
|
|
@ -145,13 +145,6 @@ linters:
|
|||
- revive
|
||||
path: pkg/utils/*
|
||||
text: 'var-naming: avoid meaningless package names'
|
||||
- linters:
|
||||
- revive
|
||||
path: pkg/routes/api/shared/*
|
||||
text: 'var-naming: avoid meaningless package names'
|
||||
- linters:
|
||||
- contextcheck
|
||||
path: pkg/routes/api/v2/backgrounds.go # the unsplash provider intentionally uses context.Background(); its interface is shared with v1 and can't take a context
|
||||
- linters:
|
||||
- revive
|
||||
text: 'var-naming: avoid package names that conflict with Go standard library package names'
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# syntax=docker/dockerfile:1@sha256:87999aa3d42bdc6bea60565083ee17e86d1f3339802f543c0d03998580f9cb89
|
||||
FROM --platform=$BUILDPLATFORM node:24.18.0-alpine@sha256:a0b9bf06e4e6193cf7a0f58816cc935ff8c2a908f81e6f1a95432d679c54fbfd AS frontendbuilder
|
||||
# syntax=docker/dockerfile:1@sha256:b6afd42430b15f2d2a4c5a02b919e98a525b785b1aaff16747d2f623364e39b6
|
||||
FROM --platform=$BUILDPLATFORM node:24.13.0-alpine@sha256:931d7d57f8c1fd0e2179dbff7cc7da4c9dd100998bc2b32afc85142d8efbc213 AS frontendbuilder
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
|
|
@ -14,7 +14,7 @@ COPY frontend/ ./
|
|||
ARG RELEASE_VERSION=dev
|
||||
RUN echo "{\"VERSION\": \"${RELEASE_VERSION/-g/-}\"}" > src/version.json && pnpm run build
|
||||
|
||||
FROM --platform=$BUILDPLATFORM ghcr.io/techknowlogick/xgo:go-1.26.x@sha256:57c62857168cee9213045d65044e990d8b181ed6df30ba7097d2dcddd42b9908 AS apibuilder
|
||||
FROM --platform=$BUILDPLATFORM ghcr.io/techknowlogick/xgo:go-1.25.x@sha256:11ac5e6cb8767caea0c62c420e053cb69554638ec255f9bbef8ed411e70c9eec AS apibuilder
|
||||
|
||||
RUN go install github.com/magefile/mage@latest && \
|
||||
mv /go/bin/mage /usr/local/go/bin
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
module code.vikunja.io/build
|
||||
|
||||
go 1.26.4
|
||||
go 1.25.0
|
||||
|
||||
require github.com/magefile/mage v1.17.2
|
||||
|
|
|
|||
|
|
@ -849,11 +849,6 @@
|
|||
"default_value": "(&(objectclass=*)(|(objectclass=group)(objectclass=groupOfNames)))",
|
||||
"comment": "The filter to search for group objects in the ldap directory. Only used when `groupsyncenabled` is set to `true`."
|
||||
},
|
||||
{
|
||||
"key": "groupsyncuseserviceaccount",
|
||||
"default_value": "false",
|
||||
"comment": "If true, Vikunja re-binds as the service account (binddn/bindpassword) before searching for groups during group sync. Enable this when the authenticating user does not have sufficient rights to enumerate group membership in the directory."
|
||||
},
|
||||
{
|
||||
"key": "avatarsyncattribute",
|
||||
"default_value": "",
|
||||
|
|
@ -1030,6 +1025,10 @@
|
|||
"comment": "Delete rotated audit log files older than this many days. This only applies to the local rotated files, it is not a retention policy. Set to 0 to keep rotated files forever."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "forwarders",
|
||||
"comment": "A list of sinks to forward each audit entry to, in addition to the local logfile. Each entry needs a `type` of `stdout`, `syslog` or `webhook`. `syslog` requires `address` (e.g. `udp://logs.example.com:514`) and accepts an optional `facility` (default `local0`). `webhook` requires `url` and accepts an optional `headers` map sent with each request.\nExample:\n\n```yaml\nforwarders:\n- type: stdout\n- type: syslog\n address: udp://logs.example.com:514\n facility: local0\n- type: webhook\n url: https://siem.example.com/ingest\n headers:\n Authorization: Bearer something\n```"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -100,15 +100,10 @@ app.on('second-instance', (_event, argv) => {
|
|||
return
|
||||
}
|
||||
|
||||
// Reveal the main window. It may be hidden in the tray (not just minimized),
|
||||
// so show() is required — focus() alone won't surface a hidden window, which
|
||||
// made the app look dead when relaunched while running in the tray.
|
||||
// Focus the main window
|
||||
if (mainWindow) {
|
||||
if (mainWindow.isMinimized()) mainWindow.restore()
|
||||
mainWindow.show()
|
||||
mainWindow.focus()
|
||||
} else if (serverPort) {
|
||||
createMainWindow()
|
||||
}
|
||||
|
||||
// Find the deep link URL in argv
|
||||
|
|
@ -241,11 +236,6 @@ function createMainWindow() {
|
|||
mainWindow = new BrowserWindow({
|
||||
width: 1680,
|
||||
height: 960,
|
||||
// Without an explicit window icon, X11/XWayland compositors (e.g. KDE
|
||||
// Plasma) fall back to a generic placeholder when WM_CLASS doesn't match
|
||||
// an installed .desktop file. icon.png lives at the app root because
|
||||
// build/ is electron-builder's buildResources dir and isn't packaged.
|
||||
icon: path.join(__dirname, 'icon.png'),
|
||||
webPreferences: {
|
||||
...BASE_WEB_PREFERENCES,
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
|
|
@ -553,14 +543,3 @@ app.on('window-all-closed', () => {
|
|||
app.quit()
|
||||
}
|
||||
})
|
||||
|
||||
// Quit on termination signals (DE/systemd shutdown, `kill`). Without an explicit
|
||||
// handler the app ignores SIGTERM because the tray and express server keep the
|
||||
// event loop alive — leaving users to `kill -9`. isQuitting must be set first so
|
||||
// the hide-to-tray close handler doesn't swallow the quit.
|
||||
for (const signal of ['SIGINT', 'SIGTERM']) {
|
||||
process.on(signal, () => {
|
||||
isQuitting = true
|
||||
app.quit()
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
"main": "main.js",
|
||||
"repository": "https://code.vikunja.io/desktop",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"packageManager": "pnpm@10.34.4",
|
||||
"packageManager": "pnpm@10.28.1",
|
||||
"author": {
|
||||
"email": "maintainers@vikunja.io",
|
||||
"name": "Vikunja Team"
|
||||
|
|
@ -61,9 +61,9 @@
|
|||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "40.10.5",
|
||||
"electron-builder": "26.15.3",
|
||||
"unzipper": "0.12.5"
|
||||
"electron": "40.10.3",
|
||||
"electron-builder": "26.15.2",
|
||||
"unzipper": "0.12.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "5.2.1"
|
||||
|
|
@ -73,16 +73,12 @@
|
|||
"electron"
|
||||
],
|
||||
"overrides": {
|
||||
"minimatch": "10.2.5",
|
||||
"tar": "7.5.17",
|
||||
"@tootallnate/once": "3.0.1",
|
||||
"picomatch": "4.0.4",
|
||||
"tmp": "0.2.7",
|
||||
"ip-address": "10.2.0",
|
||||
"form-data": "4.0.6",
|
||||
"js-yaml": "5.2.0",
|
||||
"undici@6": "6.27.0",
|
||||
"undici@7": "7.28.0"
|
||||
"minimatch": "^10.2.3",
|
||||
"tar": "^7.5.11",
|
||||
"@tootallnate/once": "^3.0.1",
|
||||
"picomatch": ">=4.0.4",
|
||||
"tmp": ">=0.2.6",
|
||||
"ip-address": ">=10.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,16 +5,12 @@ settings:
|
|||
excludeLinksFromLockfile: false
|
||||
|
||||
overrides:
|
||||
minimatch: 10.2.5
|
||||
tar: 7.5.17
|
||||
'@tootallnate/once': 3.0.1
|
||||
picomatch: 4.0.4
|
||||
tmp: 0.2.7
|
||||
ip-address: 10.2.0
|
||||
form-data: 4.0.6
|
||||
js-yaml: 5.2.0
|
||||
undici@6: 6.27.0
|
||||
undici@7: 7.28.0
|
||||
minimatch: ^10.2.3
|
||||
tar: ^7.5.11
|
||||
'@tootallnate/once': ^3.0.1
|
||||
picomatch: '>=4.0.4'
|
||||
tmp: '>=0.2.6'
|
||||
ip-address: '>=10.1.1'
|
||||
|
||||
importers:
|
||||
|
||||
|
|
@ -25,14 +21,14 @@ importers:
|
|||
version: 5.2.1
|
||||
devDependencies:
|
||||
electron:
|
||||
specifier: 40.10.5
|
||||
version: 40.10.5
|
||||
specifier: 40.10.3
|
||||
version: 40.10.3
|
||||
electron-builder:
|
||||
specifier: 26.15.3
|
||||
version: 26.15.3(electron-builder-squirrel-windows@24.13.3)
|
||||
specifier: 26.15.2
|
||||
version: 26.15.2(electron-builder-squirrel-windows@24.13.3)
|
||||
unzipper:
|
||||
specifier: 0.12.5
|
||||
version: 0.12.5
|
||||
specifier: 0.12.3
|
||||
version: 0.12.3
|
||||
|
||||
packages:
|
||||
|
||||
|
|
@ -238,12 +234,12 @@ packages:
|
|||
dmg-builder: 24.13.3
|
||||
electron-builder-squirrel-windows: 24.13.3
|
||||
|
||||
app-builder-lib@26.15.3:
|
||||
resolution: {integrity: sha512-2VnyWkqsP5v5XbBhL3tD5Syx8iNPBYsoU7kY4S2fz7wg8Rj/nztWKCUzGKaFRTv0Xwf3/H058CR1Kvtd/3lRow==}
|
||||
app-builder-lib@26.15.2:
|
||||
resolution: {integrity: sha512-3mYfKOjr/ZY7gFESOcq8kylBMgGPpmlQYnpBVit4p6zIg0t/8bkWBILdMMtnjFyN2jllyBf225T8dLlz3D6oBQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
dmg-builder: 26.15.3
|
||||
electron-builder-squirrel-windows: 26.15.3
|
||||
dmg-builder: 26.15.2
|
||||
electron-builder-squirrel-windows: 26.15.2
|
||||
|
||||
archiver-utils@2.1.0:
|
||||
resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==}
|
||||
|
|
@ -333,8 +329,8 @@ packages:
|
|||
builder-util@24.13.1:
|
||||
resolution: {integrity: sha512-NhbCSIntruNDTOVI9fdXz0dihaqX2YuE1D6zZMrwiErzH4ELZHE6mdiB40wEgZNprDia+FghRFgKoAqMZRRjSA==}
|
||||
|
||||
builder-util@26.15.3:
|
||||
resolution: {integrity: sha512-q2hn7Mbo2nFNkVekPiHFx6Nfo3hURmES3tfBn+k5Pqxl2RkmP3QGqZUhH/q9Pch/4G05NRhPjDlVj1O8q4Txvw==}
|
||||
builder-util@26.15.0:
|
||||
resolution: {integrity: sha512-dUx+HxVbiNsNQ4mGe1PyoC/tBmsHwBNDLdBuqWCj+rhHFE9lHgrXiGYKAM1uNlznhAaUSyMlms84VeSSr3gOBA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
bytes@3.1.2:
|
||||
|
|
@ -487,8 +483,8 @@ packages:
|
|||
dir-compare@4.2.0:
|
||||
resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==}
|
||||
|
||||
dmg-builder@26.15.3:
|
||||
resolution: {integrity: sha512-O3zJUFUYHJKgzPqioHxfxzBzlSC1eXCSr79gMSBKBP5AgjjpmrydMsMLotEg9fAJF36vdUncb+4ndRNxoPdlSQ==}
|
||||
dmg-builder@26.15.2:
|
||||
resolution: {integrity: sha512-fMkjRqKyPtsz4Kzu/qGP0BGjqzMCIgp+/7kw/u6YH6lvn/8hvL3c0TXhoFayBoYdpPCnEinnCHztd4bW7/jetA==}
|
||||
|
||||
dotenv-expand@11.0.6:
|
||||
resolution: {integrity: sha512-8NHi73otpWsZGBSZwwknTXS5pqMOrk9+Ssrna8xCaxkzEpU9OTf9R5ArQGVw03//Zmk9MOwLPng9WwndvpAJ5g==}
|
||||
|
|
@ -526,19 +522,19 @@ packages:
|
|||
electron-builder-squirrel-windows@24.13.3:
|
||||
resolution: {integrity: sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==}
|
||||
|
||||
electron-builder@26.15.3:
|
||||
resolution: {integrity: sha512-a1KM5heqS3gQCZzizXEI8RjJy3QVogULPdeSknt76uLDpBIW/HDGsMg/XgP0riP6PI9COsRvFITKKGDqA8fJxA==}
|
||||
electron-builder@26.15.2:
|
||||
resolution: {integrity: sha512-veKM9+dCljaC5A74Pwc0ZWQ9arOHREXWh9hUIf8NGg49ch7x+IB4QhbMzIrV5ONZIXM2OEkaxW11cAPjPtoi4A==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
hasBin: true
|
||||
|
||||
electron-publish@24.13.1:
|
||||
resolution: {integrity: sha512-2ZgdEqJ8e9D17Hwp5LEq5mLQPjqU3lv/IALvgp+4W8VeNhryfGhYEQC/PgDPMrnWUp+l60Ou5SJLsu+k4mhQ8A==}
|
||||
|
||||
electron-publish@26.15.3:
|
||||
resolution: {integrity: sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q==}
|
||||
electron-publish@26.15.1:
|
||||
resolution: {integrity: sha512-BMgMHOyexWn0UnOC+Afffw0DMrr0yfLp4U8YsLXwoJ3Da7LS7WUnz21teYZqO0gaApE1KgsjREWmbPqvF5JcPg==}
|
||||
|
||||
electron@40.10.5:
|
||||
resolution: {integrity: sha512-VzTIvwOYXZZufT9B83GDQogR1TFqREygRYhm0LE++QhGPjvBeg+W7siOP9K5+9rHMUnRuCX4YU/0ivLekN/UZQ==}
|
||||
electron@40.10.3:
|
||||
resolution: {integrity: sha512-DdWRsHm4j5wH9TMcfnB2Dqx44G/6BgLKSG/oeRe9kS60pfqCUwzUkHk0ClwvZzBVXtJ1kcdkHVRrJsl1ooKp+g==}
|
||||
engines: {node: '>= 22.12.0'}
|
||||
hasBin: true
|
||||
|
||||
|
|
@ -620,7 +616,7 @@ packages:
|
|||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
peerDependencies:
|
||||
picomatch: 4.0.4
|
||||
picomatch: '>=4.0.4'
|
||||
peerDependenciesMeta:
|
||||
picomatch:
|
||||
optional: true
|
||||
|
|
@ -636,8 +632,8 @@ packages:
|
|||
resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
form-data@4.0.6:
|
||||
resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==}
|
||||
form-data@4.0.5:
|
||||
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
forwarded@0.2.0:
|
||||
|
|
@ -655,6 +651,10 @@ packages:
|
|||
resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
fs-extra@11.2.0:
|
||||
resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==}
|
||||
engines: {node: '>=14.14'}
|
||||
|
||||
fs-extra@11.3.1:
|
||||
resolution: {integrity: sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==}
|
||||
engines: {node: '>=14.14'}
|
||||
|
|
@ -736,10 +736,6 @@ packages:
|
|||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hasown@2.0.4:
|
||||
resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hosted-git-info@4.1.0:
|
||||
resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -834,8 +830,8 @@ packages:
|
|||
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
||||
hasBin: true
|
||||
|
||||
js-yaml@5.2.0:
|
||||
resolution: {integrity: sha512-YeLUMlvR4Ou1B119LIaM0r65JvbOBooJDc9yEu0dClb/uSC5P4FrLU8OCCz/HXWvtPoIrR0dRzABTjo1sTN9Bw==}
|
||||
js-yaml@4.1.1:
|
||||
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
|
||||
hasBin: true
|
||||
|
||||
json-buffer@3.0.1:
|
||||
|
|
@ -858,6 +854,9 @@ packages:
|
|||
jsonfile@4.0.0:
|
||||
resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
|
||||
|
||||
jsonfile@6.1.0:
|
||||
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
||||
|
||||
jsonfile@6.2.0:
|
||||
resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
|
||||
|
||||
|
|
@ -1299,8 +1298,8 @@ packages:
|
|||
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
tar@7.5.17:
|
||||
resolution: {integrity: sha512-wPEBwzapC+2PaTYPH6e2L+cNOEE227S47wUYFqlegcs8zlLLmeb9Fcff1HVZY4Fwku/1Eyv38n7GYwB2aaS71g==}
|
||||
tar@7.5.15:
|
||||
resolution: {integrity: sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
temp-file@3.4.0:
|
||||
|
|
@ -1316,8 +1315,8 @@ packages:
|
|||
tmp-promise@3.0.3:
|
||||
resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==}
|
||||
|
||||
tmp@0.2.7:
|
||||
resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==}
|
||||
tmp@0.2.6:
|
||||
resolution: {integrity: sha512-5sJPdPjfI5Kx+qbrDesxkglRBxW//g7hCsqspEjwkewGvBMGIKMOTKzLt1hFVJzyadba3lDUN20O9qhvbQUSTA==}
|
||||
engines: {node: '>=14.14'}
|
||||
|
||||
toidentifier@1.0.1:
|
||||
|
|
@ -1346,12 +1345,12 @@ packages:
|
|||
undici-types@7.16.0:
|
||||
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
|
||||
|
||||
undici@6.27.0:
|
||||
resolution: {integrity: sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==}
|
||||
undici@6.26.0:
|
||||
resolution: {integrity: sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==}
|
||||
engines: {node: '>=18.17'}
|
||||
|
||||
undici@7.28.0:
|
||||
resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==}
|
||||
undici@7.27.2:
|
||||
resolution: {integrity: sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==}
|
||||
engines: {node: '>=20.18.1'}
|
||||
|
||||
universalify@0.1.2:
|
||||
|
|
@ -1366,8 +1365,8 @@ packages:
|
|||
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
unzipper@0.12.5:
|
||||
resolution: {integrity: sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A==}
|
||||
unzipper@0.12.3:
|
||||
resolution: {integrity: sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA==}
|
||||
|
||||
uri-js@4.4.1:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
|
|
@ -1488,7 +1487,7 @@ snapshots:
|
|||
semver: 7.8.1
|
||||
sumchecker: 3.0.1
|
||||
optionalDependencies:
|
||||
undici: 7.28.0
|
||||
undici: 7.27.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
@ -1712,7 +1711,7 @@ snapshots:
|
|||
|
||||
app-builder-bin@4.0.0: {}
|
||||
|
||||
app-builder-lib@24.13.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@24.13.3):
|
||||
app-builder-lib@24.13.3(dmg-builder@26.15.2)(electron-builder-squirrel-windows@24.13.3):
|
||||
dependencies:
|
||||
'@develar/schema-utils': 2.6.5
|
||||
'@electron/notarize': 2.2.1
|
||||
|
|
@ -1726,27 +1725,27 @@ snapshots:
|
|||
builder-util-runtime: 9.2.4
|
||||
chromium-pickle-js: 0.2.0
|
||||
debug: 4.4.3
|
||||
dmg-builder: 26.15.3(electron-builder-squirrel-windows@24.13.3)
|
||||
dmg-builder: 26.15.2(electron-builder-squirrel-windows@24.13.3)
|
||||
ejs: 3.1.10
|
||||
electron-builder-squirrel-windows: 24.13.3(dmg-builder@26.15.3)
|
||||
electron-builder-squirrel-windows: 24.13.3(dmg-builder@26.15.2)
|
||||
electron-publish: 24.13.1
|
||||
form-data: 4.0.6
|
||||
form-data: 4.0.5
|
||||
fs-extra: 10.1.0
|
||||
hosted-git-info: 4.1.0
|
||||
is-ci: 3.0.1
|
||||
isbinaryfile: 5.0.7
|
||||
js-yaml: 5.2.0
|
||||
js-yaml: 4.1.1
|
||||
lazy-val: 1.0.5
|
||||
minimatch: 10.2.5
|
||||
read-config-file: 6.3.2
|
||||
sanitize-filename: 1.6.4
|
||||
semver: 7.8.1
|
||||
tar: 7.5.17
|
||||
tar: 7.5.15
|
||||
temp-file: 3.4.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
app-builder-lib@26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@24.13.3):
|
||||
app-builder-lib@26.15.2(dmg-builder@26.15.2)(electron-builder-squirrel-windows@24.13.3):
|
||||
dependencies:
|
||||
'@electron/asar': 3.4.1
|
||||
'@electron/fuses': 1.8.0
|
||||
|
|
@ -1762,22 +1761,22 @@ snapshots:
|
|||
ajv: 8.20.0
|
||||
asn1js: 3.0.10
|
||||
async-exit-hook: 2.0.1
|
||||
builder-util: 26.15.3
|
||||
builder-util: 26.15.0
|
||||
builder-util-runtime: 9.7.0
|
||||
chromium-pickle-js: 0.2.0
|
||||
ci-info: 4.3.1
|
||||
debug: 4.4.3
|
||||
dmg-builder: 26.15.3(electron-builder-squirrel-windows@24.13.3)
|
||||
dmg-builder: 26.15.2(electron-builder-squirrel-windows@24.13.3)
|
||||
dotenv: 16.4.5
|
||||
dotenv-expand: 11.0.6
|
||||
ejs: 3.1.10
|
||||
electron-builder-squirrel-windows: 24.13.3(dmg-builder@26.15.3)
|
||||
electron-publish: 26.15.3
|
||||
electron-builder-squirrel-windows: 24.13.3(dmg-builder@26.15.2)
|
||||
electron-publish: 26.15.1
|
||||
fs-extra: 10.1.0
|
||||
hosted-git-info: 4.1.0
|
||||
isbinaryfile: 5.0.7
|
||||
jiti: 2.6.1
|
||||
js-yaml: 5.2.0
|
||||
js-yaml: 4.1.1
|
||||
json5: 2.2.3
|
||||
lazy-val: 1.0.5
|
||||
minimatch: 10.2.5
|
||||
|
|
@ -1786,10 +1785,10 @@ snapshots:
|
|||
proper-lockfile: 4.1.2
|
||||
resedit: 1.7.2
|
||||
semver: 7.7.4
|
||||
tar: 7.5.17
|
||||
tar: 7.5.15
|
||||
temp-file: 3.4.0
|
||||
tiny-async-pool: 1.3.0
|
||||
unzipper: 0.12.5
|
||||
unzipper: 0.12.3
|
||||
which: 5.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
|
@ -1924,14 +1923,14 @@ snapshots:
|
|||
http-proxy-agent: 5.0.0
|
||||
https-proxy-agent: 5.0.1
|
||||
is-ci: 3.0.1
|
||||
js-yaml: 5.2.0
|
||||
js-yaml: 4.1.1
|
||||
source-map-support: 0.5.21
|
||||
stat-mode: 1.0.0
|
||||
temp-file: 3.4.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
builder-util@26.15.3:
|
||||
builder-util@26.15.0:
|
||||
dependencies:
|
||||
'@types/debug': 4.1.13
|
||||
builder-util-runtime: 9.7.0
|
||||
|
|
@ -1941,7 +1940,7 @@ snapshots:
|
|||
fs-extra: 10.1.0
|
||||
http-proxy-agent: 7.0.2
|
||||
https-proxy-agent: 7.0.5
|
||||
js-yaml: 5.2.0
|
||||
js-yaml: 4.1.1
|
||||
sanitize-filename: 1.6.4
|
||||
source-map-support: 0.5.21
|
||||
stat-mode: 1.0.0
|
||||
|
|
@ -2089,12 +2088,12 @@ snapshots:
|
|||
minimatch: 10.2.5
|
||||
p-limit: 3.1.0
|
||||
|
||||
dmg-builder@26.15.3(electron-builder-squirrel-windows@24.13.3):
|
||||
dmg-builder@26.15.2(electron-builder-squirrel-windows@24.13.3):
|
||||
dependencies:
|
||||
app-builder-lib: 26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@24.13.3)
|
||||
builder-util: 26.15.3
|
||||
app-builder-lib: 26.15.2(dmg-builder@26.15.2)(electron-builder-squirrel-windows@24.13.3)
|
||||
builder-util: 26.15.0
|
||||
fs-extra: 10.1.0
|
||||
js-yaml: 5.2.0
|
||||
js-yaml: 4.1.1
|
||||
transitivePeerDependencies:
|
||||
- electron-builder-squirrel-windows
|
||||
- supports-color
|
||||
|
|
@ -2127,9 +2126,9 @@ snapshots:
|
|||
dependencies:
|
||||
jake: 10.8.7
|
||||
|
||||
electron-builder-squirrel-windows@24.13.3(dmg-builder@26.15.3):
|
||||
electron-builder-squirrel-windows@24.13.3(dmg-builder@26.15.2):
|
||||
dependencies:
|
||||
app-builder-lib: 24.13.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@24.13.3)
|
||||
app-builder-lib: 24.13.3(dmg-builder@26.15.2)(electron-builder-squirrel-windows@24.13.3)
|
||||
archiver: 5.3.2
|
||||
builder-util: 24.13.1
|
||||
fs-extra: 10.1.0
|
||||
|
|
@ -2137,14 +2136,14 @@ snapshots:
|
|||
- dmg-builder
|
||||
- supports-color
|
||||
|
||||
electron-builder@26.15.3(electron-builder-squirrel-windows@24.13.3):
|
||||
electron-builder@26.15.2(electron-builder-squirrel-windows@24.13.3):
|
||||
dependencies:
|
||||
app-builder-lib: 26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@24.13.3)
|
||||
builder-util: 26.15.3
|
||||
app-builder-lib: 26.15.2(dmg-builder@26.15.2)(electron-builder-squirrel-windows@24.13.3)
|
||||
builder-util: 26.15.0
|
||||
builder-util-runtime: 9.7.0
|
||||
chalk: 4.1.2
|
||||
ci-info: 4.3.1
|
||||
dmg-builder: 26.15.3(electron-builder-squirrel-windows@24.13.3)
|
||||
dmg-builder: 26.15.2(electron-builder-squirrel-windows@24.13.3)
|
||||
fs-extra: 10.1.0
|
||||
lazy-val: 1.0.5
|
||||
simple-update-notifier: 2.0.0
|
||||
|
|
@ -2165,21 +2164,21 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
electron-publish@26.15.3:
|
||||
electron-publish@26.15.1:
|
||||
dependencies:
|
||||
'@types/fs-extra': 9.0.13
|
||||
aws4: 1.13.2
|
||||
builder-util: 26.15.3
|
||||
builder-util: 26.15.0
|
||||
builder-util-runtime: 9.7.0
|
||||
chalk: 4.1.2
|
||||
form-data: 4.0.6
|
||||
form-data: 4.0.5
|
||||
fs-extra: 10.1.0
|
||||
lazy-val: 1.0.5
|
||||
mime: 2.6.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
electron@40.10.5:
|
||||
electron@40.10.3:
|
||||
dependencies:
|
||||
'@electron-internal/extract-zip': 1.0.2
|
||||
'@electron/get': 5.0.0
|
||||
|
|
@ -2216,7 +2215,7 @@ snapshots:
|
|||
es-errors: 1.3.0
|
||||
get-intrinsic: 1.3.0
|
||||
has-tostringtag: 1.0.2
|
||||
hasown: 2.0.4
|
||||
hasown: 2.0.2
|
||||
|
||||
es6-error@4.1.1:
|
||||
optional: true
|
||||
|
|
@ -2295,12 +2294,12 @@ snapshots:
|
|||
cross-spawn: 7.0.6
|
||||
signal-exit: 4.1.0
|
||||
|
||||
form-data@4.0.6:
|
||||
form-data@4.0.5:
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
combined-stream: 1.0.8
|
||||
es-set-tostringtag: 2.1.0
|
||||
hasown: 2.0.4
|
||||
hasown: 2.0.2
|
||||
mime-types: 2.1.35
|
||||
|
||||
forwarded@0.2.0: {}
|
||||
|
|
@ -2315,6 +2314,12 @@ snapshots:
|
|||
jsonfile: 6.2.0
|
||||
universalify: 2.0.1
|
||||
|
||||
fs-extra@11.2.0:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
jsonfile: 6.1.0
|
||||
universalify: 2.0.1
|
||||
|
||||
fs-extra@11.3.1:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
|
|
@ -2431,10 +2436,6 @@ snapshots:
|
|||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
hasown@2.0.4:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
hosted-git-info@4.1.0:
|
||||
dependencies:
|
||||
lru-cache: 6.0.0
|
||||
|
|
@ -2533,7 +2534,7 @@ snapshots:
|
|||
|
||||
jiti@2.6.1: {}
|
||||
|
||||
js-yaml@5.2.0:
|
||||
js-yaml@4.1.1:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
|
|
@ -2552,6 +2553,12 @@ snapshots:
|
|||
optionalDependencies:
|
||||
graceful-fs: 4.2.11
|
||||
|
||||
jsonfile@6.1.0:
|
||||
dependencies:
|
||||
universalify: 2.0.1
|
||||
optionalDependencies:
|
||||
graceful-fs: 4.2.11
|
||||
|
||||
jsonfile@6.2.0:
|
||||
dependencies:
|
||||
universalify: 2.0.1
|
||||
|
|
@ -2649,9 +2656,9 @@ snapshots:
|
|||
nopt: 9.0.0
|
||||
proc-log: 6.1.0
|
||||
semver: 7.8.1
|
||||
tar: 7.5.17
|
||||
tar: 7.5.15
|
||||
tinyglobby: 0.2.15
|
||||
undici: 6.27.0
|
||||
undici: 6.26.0
|
||||
which: 6.0.1
|
||||
|
||||
node-int64@0.4.0: {}
|
||||
|
|
@ -2784,7 +2791,7 @@ snapshots:
|
|||
config-file-ts: 0.2.6
|
||||
dotenv: 9.0.2
|
||||
dotenv-expand: 5.1.0
|
||||
js-yaml: 5.2.0
|
||||
js-yaml: 4.1.1
|
||||
json5: 2.2.3
|
||||
lazy-val: 1.0.5
|
||||
|
||||
|
|
@ -3001,7 +3008,7 @@ snapshots:
|
|||
inherits: 2.0.4
|
||||
readable-stream: 3.6.2
|
||||
|
||||
tar@7.5.17:
|
||||
tar@7.5.15:
|
||||
dependencies:
|
||||
'@isaacs/fs-minipass': 4.0.1
|
||||
chownr: 3.0.0
|
||||
|
|
@ -3025,9 +3032,9 @@ snapshots:
|
|||
|
||||
tmp-promise@3.0.3:
|
||||
dependencies:
|
||||
tmp: 0.2.7
|
||||
tmp: 0.2.6
|
||||
|
||||
tmp@0.2.7: {}
|
||||
tmp@0.2.6: {}
|
||||
|
||||
toidentifier@1.0.1: {}
|
||||
|
||||
|
|
@ -3050,9 +3057,9 @@ snapshots:
|
|||
|
||||
undici-types@7.16.0: {}
|
||||
|
||||
undici@6.27.0: {}
|
||||
undici@6.26.0: {}
|
||||
|
||||
undici@7.28.0:
|
||||
undici@7.27.2:
|
||||
optional: true
|
||||
|
||||
universalify@0.1.2: {}
|
||||
|
|
@ -3061,11 +3068,11 @@ snapshots:
|
|||
|
||||
unpipe@1.0.0: {}
|
||||
|
||||
unzipper@0.12.5:
|
||||
unzipper@0.12.3:
|
||||
dependencies:
|
||||
bluebird: 3.7.2
|
||||
duplexer2: 0.1.4
|
||||
fs-extra: 11.3.1
|
||||
fs-extra: 11.2.0
|
||||
graceful-fs: 4.2.11
|
||||
node-int64: 0.4.0
|
||||
|
||||
|
|
|
|||
21
devenv.lock
21
devenv.lock
|
|
@ -3,11 +3,10 @@
|
|||
"devenv": {
|
||||
"locked": {
|
||||
"dir": "src/modules",
|
||||
"lastModified": 1782492839,
|
||||
"narHash": "sha256-j9wrcB4al5QhMelEghJ0Qs+RQPT+wyCcI4070NEgPLQ=",
|
||||
"lastModified": 1773012232,
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"rev": "3d39d0817d62069f7b18821c34a617b5141cb278",
|
||||
"rev": "46a4bd0299a26ad948b71d3053174ba7b90522f7",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -22,11 +21,10 @@
|
|||
"nixpkgs-src": "nixpkgs-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1782132010,
|
||||
"narHash": "sha256-ZnAVHdVrotp80iIMm5CSR1fdxPlw7Uwmwxb+O/wsgZ8=",
|
||||
"lastModified": 1772749504,
|
||||
"owner": "cachix",
|
||||
"repo": "devenv-nixpkgs",
|
||||
"rev": "12866ae2dddbc0ab8b329915f8072bb9c75bde89",
|
||||
"rev": "08543693199362c1fddb8f52126030d0d374ba2e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -39,11 +37,11 @@
|
|||
"nixpkgs-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1781607440,
|
||||
"narHash": "sha256-rxO+uc/KFbSJp+pgyXRuAX6QlG9hJdnt0BXpEQRXY+U=",
|
||||
"lastModified": 1769922788,
|
||||
"narHash": "sha256-H3AfG4ObMDTkTJYkd8cz1/RbY9LatN5Mk4UF48VuSXc=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "3e41b24abd260e8f71dbe2f5737d24122f972158",
|
||||
"rev": "207d15f1a6603226e1e223dc79ac29c7846da32e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
@ -55,11 +53,10 @@
|
|||
},
|
||||
"nixpkgs-unstable": {
|
||||
"locked": {
|
||||
"lastModified": 1782467914,
|
||||
"narHash": "sha256-pGvFkM8N0xEkIIXDe5YYfbEAvHrk4IxBrjB/x8OomhE=",
|
||||
"lastModified": 1772773019,
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e73de5be04e0eff4190a1432b946d469c794e7b4",
|
||||
"rev": "aca4d95fce4914b3892661bcb80b8087293536c6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
24.18.0
|
||||
24.13.0
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
},
|
||||
"homepage": "https://vikunja.io/",
|
||||
"funding": "https://opencollective.com/vikunja",
|
||||
"packageManager": "pnpm@10.34.4",
|
||||
"packageManager": "pnpm@10.28.1",
|
||||
"engines": {
|
||||
"node": ">=24.0.0"
|
||||
},
|
||||
|
|
@ -51,95 +51,95 @@
|
|||
"story:preview": "histoire preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "1.7.6",
|
||||
"@fortawesome/fontawesome-svg-core": "7.3.0",
|
||||
"@fortawesome/free-regular-svg-icons": "7.3.0",
|
||||
"@fortawesome/free-solid-svg-icons": "7.3.0",
|
||||
"@fortawesome/vue-fontawesome": "3.3.0",
|
||||
"@intlify/unplugin-vue-i18n": "11.2.4",
|
||||
"@floating-ui/dom": "1.7.4",
|
||||
"@fortawesome/fontawesome-svg-core": "7.1.0",
|
||||
"@fortawesome/free-regular-svg-icons": "7.1.0",
|
||||
"@fortawesome/free-solid-svg-icons": "7.1.0",
|
||||
"@fortawesome/vue-fontawesome": "3.1.3",
|
||||
"@intlify/unplugin-vue-i18n": "11.0.3",
|
||||
"@kyvg/vue3-notification": "3.4.2",
|
||||
"@sentry/vue": "10.62.0",
|
||||
"@tiptap/core": "3.27.1",
|
||||
"@tiptap/extension-blockquote": "3.27.1",
|
||||
"@tiptap/extension-code-block-lowlight": "3.27.1",
|
||||
"@tiptap/extension-hard-break": "3.27.1",
|
||||
"@tiptap/extension-image": "3.27.1",
|
||||
"@tiptap/extension-link": "3.27.1",
|
||||
"@tiptap/extension-list": "3.27.1",
|
||||
"@tiptap/extension-mention": "3.27.1",
|
||||
"@tiptap/extension-table": "3.27.1",
|
||||
"@tiptap/extension-typography": "3.27.1",
|
||||
"@tiptap/extension-underline": "3.27.1",
|
||||
"@tiptap/extensions": "3.27.1",
|
||||
"@tiptap/pm": "3.27.1",
|
||||
"@tiptap/starter-kit": "3.27.1",
|
||||
"@tiptap/suggestion": "3.27.1",
|
||||
"@tiptap/vue-3": "3.27.1",
|
||||
"@vueuse/core": "14.3.0",
|
||||
"@vueuse/router": "14.3.0",
|
||||
"axios": "1.18.1",
|
||||
"@sentry/vue": "10.36.0",
|
||||
"@tiptap/core": "3.17.0",
|
||||
"@tiptap/extension-blockquote": "3.17.0",
|
||||
"@tiptap/extension-code-block-lowlight": "3.17.0",
|
||||
"@tiptap/extension-hard-break": "3.17.0",
|
||||
"@tiptap/extension-image": "3.17.0",
|
||||
"@tiptap/extension-link": "3.17.0",
|
||||
"@tiptap/extension-list": "3.17.0",
|
||||
"@tiptap/extension-mention": "3.17.0",
|
||||
"@tiptap/extension-table": "3.17.0",
|
||||
"@tiptap/extension-typography": "3.17.0",
|
||||
"@tiptap/extension-underline": "3.17.0",
|
||||
"@tiptap/extensions": "3.17.0",
|
||||
"@tiptap/pm": "3.17.0",
|
||||
"@tiptap/starter-kit": "3.17.0",
|
||||
"@tiptap/suggestion": "3.17.0",
|
||||
"@tiptap/vue-3": "3.17.0",
|
||||
"@vueuse/core": "14.1.0",
|
||||
"@vueuse/router": "14.1.0",
|
||||
"axios": "1.16.0",
|
||||
"blurhash": "2.0.5",
|
||||
"bulma-css-variables": "0.9.33",
|
||||
"change-case": "5.4.4",
|
||||
"dayjs": "1.11.21",
|
||||
"dompurify": "3.4.11",
|
||||
"dayjs": "1.11.19",
|
||||
"dompurify": "3.4.0",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"flatpickr": "4.6.13",
|
||||
"floating-vue": "5.2.2",
|
||||
"is-touch-device": "1.0.1",
|
||||
"klona": "2.0.6",
|
||||
"lowlight": "3.3.0",
|
||||
"marked": "17.0.6",
|
||||
"nanoid": "5.1.16",
|
||||
"marked": "17.0.1",
|
||||
"nanoid": "5.1.6",
|
||||
"pinia": "3.0.4",
|
||||
"register-service-worker": "1.7.2",
|
||||
"sortablejs": "1.15.7",
|
||||
"ufo": "1.6.4",
|
||||
"vue": "3.5.39",
|
||||
"sortablejs": "1.15.6",
|
||||
"ufo": "1.6.3",
|
||||
"vue": "3.5.27",
|
||||
"vue-advanced-cropper": "2.8.9",
|
||||
"vue-flatpickr-component": "11.0.5",
|
||||
"vue-i18n": "11.4.6",
|
||||
"vue-i18n": "11.2.8",
|
||||
"vue-router": "4.6.4",
|
||||
"vuemoji-picker": "0.3.2",
|
||||
"workbox-precaching": "7.4.1",
|
||||
"zhyswan-vuedraggable": "4.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "10.5.0",
|
||||
"@faker-js/faker": "10.4.0",
|
||||
"@histoire/plugin-screenshot": "1.0.0-beta.1",
|
||||
"@histoire/plugin-vue": "1.0.0-beta.1",
|
||||
"@playwright/test": "1.61.1",
|
||||
"@playwright/test": "1.58.2",
|
||||
"@sentry/vite-plugin": "3.6.1",
|
||||
"@tailwindcss/vite": "4.3.1",
|
||||
"@tailwindcss/vite": "4.3.0",
|
||||
"@tsconfig/node24": "24.0.4",
|
||||
"@types/codemirror": "5.60.17",
|
||||
"@types/is-touch-device": "1.0.3",
|
||||
"@types/node": "24.13.2",
|
||||
"@types/node": "24.13.1",
|
||||
"@types/sortablejs": "1.15.9",
|
||||
"@types/ws": "8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.62.0",
|
||||
"@typescript-eslint/parser": "8.62.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.61.0",
|
||||
"@typescript-eslint/parser": "8.61.0",
|
||||
"@vitejs/plugin-vue": "6.0.7",
|
||||
"@vue/eslint-config-typescript": "14.9.0",
|
||||
"@vue/eslint-config-typescript": "14.8.0",
|
||||
"@vue/test-utils": "2.4.11",
|
||||
"@vue/tsconfig": "0.9.1",
|
||||
"@vueuse/shared": "14.3.0",
|
||||
"autoprefixer": "10.5.2",
|
||||
"browserslist": "4.28.4",
|
||||
"caniuse-lite": "1.0.30001799",
|
||||
"autoprefixer": "10.5.0",
|
||||
"browserslist": "4.28.2",
|
||||
"caniuse-lite": "1.0.30001797",
|
||||
"csstype": "3.2.3",
|
||||
"esbuild": "0.28.1",
|
||||
"esbuild": "0.28.0",
|
||||
"eslint": "9.39.4",
|
||||
"eslint-plugin-depend": "1.5.0",
|
||||
"eslint-plugin-vue": "10.9.2",
|
||||
"happy-dom": "20.10.6",
|
||||
"happy-dom": "20.10.2",
|
||||
"histoire": "1.0.0-beta.1",
|
||||
"otplib": "12.0.1",
|
||||
"postcss": "8.5.15",
|
||||
"postcss-easing-gradients": "3.0.1",
|
||||
"postcss-html": "1.8.1",
|
||||
"postcss-preset-env": "11.3.1",
|
||||
"rollup": "4.62.2",
|
||||
"postcss-preset-env": "11.3.0",
|
||||
"rollup": "4.61.1",
|
||||
"rollup-plugin-visualizer": "6.0.11",
|
||||
"sass-embedded": "1.100.0",
|
||||
"stylelint": "17.13.0",
|
||||
|
|
@ -147,15 +147,15 @@
|
|||
"stylelint-config-recommended-vue": "1.6.1",
|
||||
"stylelint-config-standard-scss": "17.0.0",
|
||||
"stylelint-use-logical": "2.1.3",
|
||||
"tailwindcss": "4.3.1",
|
||||
"tailwindcss": "4.3.0",
|
||||
"typescript": "5.9.3",
|
||||
"unplugin-inject-preload": "3.0.0",
|
||||
"vite": "7.3.6",
|
||||
"vite": "7.3.5",
|
||||
"vite-plugin-pwa": "1.3.0",
|
||||
"vite-plugin-vue-devtools": "8.1.4",
|
||||
"vite-plugin-vue-devtools": "8.1.2",
|
||||
"vite-svg-loader": "5.1.1",
|
||||
"vitest": "4.1.9",
|
||||
"vue-tsc": "3.3.5",
|
||||
"vitest": "4.1.8",
|
||||
"vue-tsc": "3.3.4",
|
||||
"wait-on": "9.0.10",
|
||||
"workbox-cli": "7.4.1",
|
||||
"ws": "8.21.0"
|
||||
|
|
@ -169,20 +169,14 @@
|
|||
"vue-demi"
|
||||
],
|
||||
"overrides": {
|
||||
"minimatch": "10.2.5",
|
||||
"minimatch": "^10.2.3",
|
||||
"rollup": "$rollup",
|
||||
"basic-ftp": "6.0.1",
|
||||
"serialize-javascript": "7.0.6",
|
||||
"flatted": "3.4.2",
|
||||
"ip-address": "10.2.0",
|
||||
"postcss": "8.5.15",
|
||||
"tmp": "0.2.7",
|
||||
"esbuild": "0.28.1",
|
||||
"form-data": "4.0.6",
|
||||
"markdown-it": "14.2.0",
|
||||
"launch-editor": "2.14.1",
|
||||
"@babel/core": "8.0.1",
|
||||
"js-yaml@4": "5.2.0"
|
||||
"basic-ftp": ">=5.2.2",
|
||||
"serialize-javascript": "^7.0.5",
|
||||
"flatted": "^3.4.1",
|
||||
"ip-address": ">=10.1.1",
|
||||
"postcss": ">=8.5.10",
|
||||
"tmp": ">=0.2.6"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 2.0 KiB |
|
|
@ -61,7 +61,6 @@ import {useAuthStore} from '@/stores/auth'
|
|||
import {useBaseStore} from '@/stores/base'
|
||||
|
||||
import {useColorScheme} from '@/composables/useColorScheme'
|
||||
import {useTimeTrackingFavicon} from '@/composables/useTimeTrackingFavicon'
|
||||
import {useBodyClass} from '@/composables/useBodyClass'
|
||||
import QuickAddOverlay from '@/components/quick-actions/QuickAddOverlay.vue'
|
||||
import AddToHomeScreen from '@/components/home/AddToHomeScreen.vue'
|
||||
|
|
@ -108,7 +107,6 @@ watch(accountDeletionConfirm, async (accountDeletionConfirm) => {
|
|||
|
||||
setLanguage(authStore.settings.language ?? DEFAULT_LANGUAGE)
|
||||
useColorScheme()
|
||||
useTimeTrackingFavicon()
|
||||
</script>
|
||||
|
||||
<style src="@/styles/tailwind.css" />
|
||||
|
|
|
|||
|
|
@ -13,14 +13,14 @@
|
|||
<div class="gantt-chart-wrapper">
|
||||
<GanttTimelineHeader
|
||||
:timeline-data="timelineData"
|
||||
:day-width-pixels="dayWidthPixels"
|
||||
:day-width-pixels="DAY_WIDTH_PIXELS"
|
||||
/>
|
||||
|
||||
<GanttVerticalGridLines
|
||||
:timeline-data="timelineData"
|
||||
:total-width="totalWidth"
|
||||
:height="ganttRows.length * 40"
|
||||
:day-width-pixels="dayWidthPixels"
|
||||
:day-width-pixels="DAY_WIDTH_PIXELS"
|
||||
/>
|
||||
|
||||
<GanttChartBody
|
||||
|
|
@ -57,7 +57,7 @@
|
|||
:total-width="totalWidth"
|
||||
:date-from-date="dateFromDate"
|
||||
:date-to-date="dateToDate"
|
||||
:day-width-pixels="dayWidthPixels"
|
||||
:day-width-pixels="DAY_WIDTH_PIXELS"
|
||||
:is-dragging="isDragging"
|
||||
:is-resizing="isResizing"
|
||||
:drag-state="dragState"
|
||||
|
|
@ -89,7 +89,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, ref, watch, toRefs, nextTick, onMounted, onBeforeUnmount, onUnmounted} from 'vue'
|
||||
import {computed, ref, watch, toRefs, onUnmounted} from 'vue'
|
||||
import {useRouter} from 'vue-router'
|
||||
import dayjs from 'dayjs'
|
||||
import {useDayjsLanguageSync} from '@/i18n/useDayjsLanguageSync'
|
||||
|
|
@ -126,9 +126,7 @@ const emit = defineEmits<{
|
|||
(e: 'update:task', task: ITaskPartialWithId): void
|
||||
}>()
|
||||
|
||||
const DAY_WIDTH_PIXELS_MIN = 30
|
||||
const dayWidthPixels = ref(0)
|
||||
let resizeObserver: ResizeObserver
|
||||
const DAY_WIDTH_PIXELS = 30
|
||||
|
||||
const {tasks, filters} = toRefs(props)
|
||||
|
||||
|
|
@ -160,7 +158,7 @@ const dateToDate = computed(() => dayjs(filters.value.dateTo).endOf('day').toDat
|
|||
|
||||
const totalWidth = computed(() => {
|
||||
const dateDiff = Math.ceil((dateToDate.value.valueOf() - dateFromDate.value.valueOf()) / MILLISECONDS_A_DAY)
|
||||
return dateDiff * dayWidthPixels.value
|
||||
return dateDiff * DAY_WIDTH_PIXELS
|
||||
})
|
||||
|
||||
const timelineData = computed(() => {
|
||||
|
|
@ -299,55 +297,6 @@ function transformTaskToGanttBar(node: GanttTaskTreeNode): GanttBarModel {
|
|||
}
|
||||
}
|
||||
|
||||
function updateDayWidthPixels() {
|
||||
const node = ganttContainer.value
|
||||
if (!node) return
|
||||
|
||||
const rect = node.getBoundingClientRect()
|
||||
const styles = window.getComputedStyle(node)
|
||||
|
||||
const marginLeft = parseFloat(styles.marginLeft) || 0
|
||||
const marginRight = parseFloat(styles.marginRight) || 0
|
||||
|
||||
// max width without overflow
|
||||
const maxWidth = rect.width - marginLeft - marginRight
|
||||
|
||||
const dayCount = Math.ceil(
|
||||
(dateToDate.value.valueOf() - dateFromDate.value.valueOf()) / MILLISECONDS_A_DAY,
|
||||
)
|
||||
|
||||
dayWidthPixels.value = Math.max(
|
||||
maxWidth / dayCount,
|
||||
DAY_WIDTH_PIXELS_MIN,
|
||||
)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
updateDayWidthPixels()
|
||||
|
||||
if (ganttContainer.value) {
|
||||
resizeObserver = new ResizeObserver(updateDayWidthPixels)
|
||||
resizeObserver.observe(ganttContainer.value)
|
||||
}
|
||||
|
||||
window.addEventListener('resize', updateDayWidthPixels)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
resizeObserver?.disconnect()
|
||||
window.removeEventListener('resize', updateDayWidthPixels)
|
||||
})
|
||||
|
||||
watch(
|
||||
[dateFromDate, dateToDate],
|
||||
async () => {
|
||||
await nextTick()
|
||||
updateDayWidthPixels()
|
||||
},
|
||||
{flush: 'post'},
|
||||
)
|
||||
|
||||
// Build the task tree when tasks change
|
||||
watch(
|
||||
[tasks, filters],
|
||||
|
|
@ -402,7 +351,7 @@ const ROW_HEIGHT = 40
|
|||
const barPositions = computed(() => {
|
||||
const positions = new Map<number, GanttBarPosition>()
|
||||
const ds = dragState.value
|
||||
const dragPixelOffset = ds ? ds.currentDays * dayWidthPixels.value : 0
|
||||
const dragPixelOffset = ds ? ds.currentDays * DAY_WIDTH_PIXELS : 0
|
||||
|
||||
ganttBars.value.forEach((rowBars, rowIndex) => {
|
||||
for (const bar of rowBars) {
|
||||
|
|
@ -437,7 +386,7 @@ function computeBarX(date: Date): number {
|
|||
(roundToNaturalDayBoundary(date, true).getTime() - dateFromDate.value.getTime()) /
|
||||
MILLISECONDS_A_DAY,
|
||||
)
|
||||
return diff * dayWidthPixels.value
|
||||
return diff * DAY_WIDTH_PIXELS
|
||||
}
|
||||
|
||||
function computeBarWidth(bar: GanttBarModel): number {
|
||||
|
|
@ -445,7 +394,7 @@ function computeBarWidth(bar: GanttBarModel): number {
|
|||
(roundToNaturalDayBoundary(bar.end).getTime() - roundToNaturalDayBoundary(bar.start, true).getTime()) /
|
||||
MILLISECONDS_A_DAY,
|
||||
)
|
||||
return diff * dayWidthPixels.value
|
||||
return diff * DAY_WIDTH_PIXELS
|
||||
}
|
||||
|
||||
// Compute relation arrows
|
||||
|
|
@ -641,7 +590,7 @@ function startDrag(bar: GanttBarModel, event: PointerEvent) {
|
|||
if (!dragState.value || !isDragging.value) return
|
||||
|
||||
const diff = e.clientX - dragState.value.startX
|
||||
const days = Math.round(diff / dayWidthPixels.value)
|
||||
const days = Math.round(diff / DAY_WIDTH_PIXELS)
|
||||
|
||||
if (days !== dragState.value.currentDays) {
|
||||
dragState.value.currentDays = days
|
||||
|
|
@ -703,7 +652,7 @@ function startResize(bar: GanttBarModel, edge: 'start' | 'end', event: PointerEv
|
|||
if (!dragState.value || !isResizing.value) return
|
||||
|
||||
const diff = e.clientX - dragState.value.startX
|
||||
const days = Math.round(diff / dayWidthPixels.value)
|
||||
const days = Math.round(diff / DAY_WIDTH_PIXELS)
|
||||
|
||||
if (edge === 'start') {
|
||||
const newStart = new Date(dragState.value.originalStart)
|
||||
|
|
@ -781,7 +730,7 @@ function focusTaskBar(rowId: string) {
|
|||
setTimeout(() => {
|
||||
const taskBarElement = document.querySelector(`[data-row-id="${rowId}"] [role="slider"]`) as HTMLElement
|
||||
if (taskBarElement) {
|
||||
taskBarElement.focus({preventScroll: true})
|
||||
taskBarElement.focus()
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -722,7 +722,7 @@ async function addImage(event: Event) {
|
|||
return
|
||||
}
|
||||
|
||||
const url = await inputPrompt(event.target.getBoundingClientRect(), '', editor.value)
|
||||
const url = await inputPrompt(event.target.getBoundingClientRect())
|
||||
|
||||
if (url) {
|
||||
editor.value?.chain().focus().setImage({src: url}).run()
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import {PluginKey, type EditorState} from '@tiptap/pm/state'
|
|||
|
||||
import EmojiList from './EmojiList.vue'
|
||||
import {loadEmojis, filterEmojis, type EmojiEntry} from './emojiData'
|
||||
import {getPopupContainer} from '../popupContainer'
|
||||
|
||||
export const EmojiSuggestionPluginKey = new PluginKey('emojiSuggestion')
|
||||
|
||||
|
|
@ -79,7 +78,7 @@ export default function emojiSuggestionSetup() {
|
|||
popupElement.style.left = '0'
|
||||
popupElement.style.zIndex = '4700'
|
||||
popupElement.appendChild(component.element!)
|
||||
getPopupContainer(props.editor).appendChild(popupElement)
|
||||
document.body.appendChild(popupElement)
|
||||
|
||||
const rect = props.clientRect()
|
||||
if (!rect) {
|
||||
|
|
@ -109,7 +108,7 @@ export default function emojiSuggestionSetup() {
|
|||
cleanupFloating = null
|
||||
}
|
||||
if (popupElement) {
|
||||
popupElement.remove()
|
||||
document.body.removeChild(popupElement)
|
||||
popupElement = null
|
||||
}
|
||||
component?.destroy()
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import inputPrompt from '@/helpers/inputPrompt'
|
|||
|
||||
export async function setLinkInEditor(pos: DOMRect, editor: Editor | null | undefined) {
|
||||
const previousUrl = editor?.getAttributes('link').href || ''
|
||||
const url = await inputPrompt(pos, previousUrl, editor ?? undefined)
|
||||
const url = await inputPrompt(pos, previousUrl)
|
||||
|
||||
// empty
|
||||
if (url === '') {
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ const props = withDefaults(defineProps<{
|
|||
enabled?: boolean,
|
||||
overflow?: boolean,
|
||||
wide?: boolean,
|
||||
variant?: 'default' | 'hint-modal' | 'scrolling' | 'top',
|
||||
variant?: 'default' | 'hint-modal' | 'scrolling',
|
||||
}>(), {
|
||||
enabled: true,
|
||||
overflow: false,
|
||||
|
|
@ -211,13 +211,7 @@ $modal-width: 1024px;
|
|||
// Reset UA dialog styles
|
||||
padding: 0;
|
||||
border: none;
|
||||
// The scrim lives on the dialog element, not on ::backdrop: Chromium
|
||||
// intermittently stops painting a styled ::backdrop (e.g. after the
|
||||
// dialog's subtree re-renders, or while display is transitioned) even
|
||||
// though getComputedStyle still reports the color. The dialog fills the
|
||||
// viewport anyway, and its opacity transition fades the scrim with it —
|
||||
// same as the old div-based .modal-mask.
|
||||
background: rgba(0, 0, 0, .8);
|
||||
background: transparent;
|
||||
color: #ffffff;
|
||||
// Fill viewport
|
||||
position: fixed;
|
||||
|
|
@ -227,12 +221,10 @@ $modal-width: 1024px;
|
|||
max-inline-size: 100%;
|
||||
max-block-size: 100%;
|
||||
|
||||
// Transitions. No display/allow-discrete transition needed: the close
|
||||
// fade runs while the dialog is still [open] (data-closing + timer in
|
||||
// closeDialog), and transitioning display triggers the Chromium paint
|
||||
// bug above.
|
||||
// Transitions
|
||||
opacity: 0;
|
||||
transition: opacity 150ms ease;
|
||||
transition: opacity 150ms ease,
|
||||
display 150ms ease allow-discrete;
|
||||
|
||||
&[open]:not([data-closing]) {
|
||||
opacity: 1;
|
||||
|
|
@ -244,11 +236,16 @@ $modal-width: 1024px;
|
|||
|
||||
&::backdrop {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
transition: background-color 150ms ease,
|
||||
display 150ms ease allow-discrete;
|
||||
}
|
||||
|
||||
// in quick-add mode the Electron window itself is the overlay — no scrim
|
||||
&:has(.is-quick-add-mode) {
|
||||
background: transparent;
|
||||
&[open]:not([data-closing])::backdrop {
|
||||
background-color: rgba(0, 0, 0, .8);
|
||||
|
||||
@starting-style {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -264,20 +261,13 @@ $modal-width: 1024px;
|
|||
}
|
||||
|
||||
.default .modal-content,
|
||||
.hint-modal .modal-content,
|
||||
.top .modal-content {
|
||||
.hint-modal .modal-content {
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
// fine to use top/left since we're only using this to position it centered
|
||||
inset-block-start: 50%;
|
||||
inset-inline-start: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
// Cap centered content to the viewport and scroll inside it. Without this a
|
||||
// taller-than-viewport modal centres its top edge above the viewport, where
|
||||
// the container's overflow can't scroll to it (the .top variant overrides
|
||||
// both values below).
|
||||
max-block-size: calc(100dvh - 2rem);
|
||||
overflow: auto;
|
||||
|
||||
[dir="rtl"] & {
|
||||
transform: translate(50%, -50%);
|
||||
|
|
@ -287,9 +277,6 @@ $modal-width: 1024px;
|
|||
margin: 0;
|
||||
position: static;
|
||||
transform: none;
|
||||
// the fullscreen mobile layout flows and scrolls in .modal-container
|
||||
max-block-size: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
|
|
@ -302,31 +289,11 @@ $modal-width: 1024px;
|
|||
}
|
||||
}
|
||||
|
||||
// anchored below the top edge instead of centered, used for QuickActions
|
||||
.top .modal-content {
|
||||
inset-block-start: 3rem;
|
||||
transform: translate(-50%, 0);
|
||||
max-block-size: calc(100dvh - 6rem);
|
||||
overflow: auto;
|
||||
|
||||
[dir="rtl"] & {
|
||||
transform: translate(50%, 0);
|
||||
}
|
||||
|
||||
// the fullscreen mobile layout flows and scrolls in .modal-container
|
||||
@media screen and (max-width: $tablet) {
|
||||
transform: none;
|
||||
max-block-size: none;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
// Default width for centered modals. Scoped with :not(.is-wide) so the
|
||||
// `wide` prop can still expand the modal (the .is-wide rule below would
|
||||
// otherwise be outranked by .default .modal-content's specificity).
|
||||
.default .modal-content:not(.is-wide),
|
||||
.hint-modal .modal-content:not(.is-wide),
|
||||
.top .modal-content:not(.is-wide) {
|
||||
.hint-modal .modal-content:not(.is-wide) {
|
||||
inline-size: calc(100% - 2rem);
|
||||
max-inline-size: 640px;
|
||||
|
||||
|
|
@ -436,7 +403,6 @@ $modal-width: 1024px;
|
|||
block-size: auto;
|
||||
max-inline-size: none;
|
||||
max-block-size: none;
|
||||
background: transparent;
|
||||
|
||||
&::backdrop {
|
||||
display: none;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
<Modal
|
||||
:enabled="active"
|
||||
:overflow="isNewTaskCommand"
|
||||
variant="top"
|
||||
@close="closeQuickActions"
|
||||
>
|
||||
<div
|
||||
|
|
@ -705,16 +704,15 @@ function reset() {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.quick-actions {
|
||||
// global Bulma .card styles are gone (ported into Card.vue, scoped),
|
||||
// so this bare .card div needs its own card visuals
|
||||
background-color: var(--white);
|
||||
border-radius: $radius;
|
||||
border: 1px solid var(--card-border-color);
|
||||
box-shadow: var(--shadow-sm);
|
||||
color: var(--text);
|
||||
overflow: hidden;
|
||||
justify-content: flex-start !important;
|
||||
|
||||
// FIXME: changed position should be an option of the modal
|
||||
:deep(.modal-content) {
|
||||
inset-block-start: 3rem;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
&.is-quick-add-mode {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@
|
|||
rows="1"
|
||||
@keydown="resetEmptyTitleError"
|
||||
@keydown.enter="handleEnter"
|
||||
@keydown.esc="blurTaskInput"
|
||||
/>
|
||||
<QuickAddMagic
|
||||
:highlight-hint-icon="taskAddHovered"
|
||||
|
|
@ -283,10 +282,6 @@ function focusTaskInput() {
|
|||
newTaskInput.value?.focus()
|
||||
}
|
||||
|
||||
function blurTaskInput() {
|
||||
newTaskInput.value?.blur()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
focusTaskInput,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@
|
|||
</XButton>
|
||||
|
||||
<!-- Dropzone -->
|
||||
<Teleport :to="dropzoneTeleportTarget">
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="editEnabled"
|
||||
:class="{hidden: !showDropzone}"
|
||||
|
|
@ -185,7 +185,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, shallowReactive, computed, watch, onMounted, onBeforeUnmount} from 'vue'
|
||||
import {ref, shallowReactive, computed, watch} from 'vue'
|
||||
import {useDropZone} from '@vueuse/core'
|
||||
|
||||
import User from '@/components/misc/User.vue'
|
||||
|
|
@ -322,34 +322,6 @@ const showDropzone = computed(() =>
|
|||
props.editEnabled && isDraggingFiles.value && !isDragOverEditor.value,
|
||||
)
|
||||
|
||||
// A <dialog> opened with showModal() (e.g. the Kanban task detail) renders in
|
||||
// the browser's top layer, so the full-screen dropzone overlay teleported to
|
||||
// <body> would paint behind it regardless of z-index. Teleport it into the
|
||||
// topmost open dialog instead, mirroring Notification.vue.
|
||||
const dropzoneTeleportTarget = ref<string | HTMLElement>('body')
|
||||
let dialogObserver: MutationObserver | null = null
|
||||
|
||||
function syncDropzoneTeleportTarget() {
|
||||
const dialogs = document.querySelectorAll<HTMLDialogElement>('dialog.modal-dialog[open]')
|
||||
dropzoneTeleportTarget.value = dialogs.item(dialogs.length - 1) ?? 'body'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
syncDropzoneTeleportTarget()
|
||||
dialogObserver = new MutationObserver(syncDropzoneTeleportTarget)
|
||||
dialogObserver.observe(document.body, {
|
||||
attributes: true,
|
||||
attributeFilter: ['open'],
|
||||
childList: true,
|
||||
subtree: true,
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
dialogObserver?.disconnect()
|
||||
dialogObserver = null
|
||||
})
|
||||
|
||||
watch(() => props.editEnabled, enabled => {
|
||||
if (!enabled) {
|
||||
resetDragState()
|
||||
|
|
@ -506,7 +478,7 @@ defineExpose({
|
|||
inset-inline-start: 0;
|
||||
inset-block-end: 0;
|
||||
inset-inline-end: 0;
|
||||
z-index: 4001; // above app chrome when teleported to body (no modal open)
|
||||
z-index: 4001; // modal z-index is 4000
|
||||
text-align: center;
|
||||
|
||||
&.hidden {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
v-if="editEnabled && Object.keys(relatedTasks).length > 0"
|
||||
id="showRelatedTasksFormButton"
|
||||
v-tooltip="$t('task.relation.add')"
|
||||
class="is-pulled-end add-task-relation-button d-print-none"
|
||||
class="is-pulled-right add-task-relation-button d-print-none"
|
||||
:class="{'is-active': showNewRelationForm}"
|
||||
variant="secondary"
|
||||
icon="plus"
|
||||
|
|
|
|||
|
|
@ -326,17 +326,9 @@ const isOverdue = computed(() => (
|
|||
let oldTask
|
||||
|
||||
async function markAsDone(checked: boolean, wasReverted: boolean = false) {
|
||||
oldTask = {...task.value}
|
||||
|
||||
// Fire the request immediately and with the intended done value snapshotted, so a re-render or
|
||||
// teardown during the animation delay can neither drop the save nor make it send a stale state.
|
||||
const updatePromise = taskStore.update({
|
||||
...task.value,
|
||||
done: checked,
|
||||
})
|
||||
|
||||
const finish = async () => {
|
||||
const newTask = await updatePromise
|
||||
const updateFunc = async () => {
|
||||
oldTask = {...task.value}
|
||||
const newTask = await taskStore.update(task.value)
|
||||
task.value = newTask
|
||||
|
||||
updateDueDate()
|
||||
|
|
@ -362,9 +354,9 @@ async function markAsDone(checked: boolean, wasReverted: boolean = false) {
|
|||
}
|
||||
|
||||
if (checked) {
|
||||
setTimeout(finish, 300) // Delay only the follow-up to show the animation when marking a task as done
|
||||
setTimeout(updateFunc, 300) // Delay it to show the animation when marking a task as done
|
||||
} else {
|
||||
await finish() // Don't delay it when un-marking it as it doesn't have an animation the other way around
|
||||
await updateFunc() // Don't delay it when un-marking it as it doesn't have an animation the other way around
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { getCurrentInstance, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { createGlobalState, useIntervalFn } from '@vueuse/core'
|
||||
import { onBeforeRouteUpdate } from 'vue-router'
|
||||
|
||||
|
|
@ -18,14 +18,10 @@ export const useGlobalNow = createGlobalState(() => {
|
|||
|
||||
useIntervalFn(update, GLOBAL_NOW_INTERVAL, { immediate: true })
|
||||
|
||||
// Now that this state can be initialised from a plain helper (formatDateSince), the
|
||||
// first caller is not guaranteed to be a component — guard the route hook accordingly.
|
||||
if (getCurrentInstance()) {
|
||||
// ensure the now value is refreshed when the route changes
|
||||
onBeforeRouteUpdate(() => {
|
||||
update()
|
||||
})
|
||||
}
|
||||
// ensure the now value is refreshed when the route changes
|
||||
onBeforeRouteUpdate(() => {
|
||||
update()
|
||||
})
|
||||
|
||||
return {
|
||||
now,
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
import {describe, it, expect} from 'vitest'
|
||||
import {buildStoredQuery} from './useTaskList'
|
||||
|
||||
describe('buildStoredQuery', () => {
|
||||
it('includes sort when set', () => {
|
||||
expect(buildStoredQuery({sort: 'due_date:asc', filter: undefined, s: undefined, page: 1}))
|
||||
.toEqual({sort: 'due_date:asc'})
|
||||
})
|
||||
|
||||
it('includes filter and search when set', () => {
|
||||
expect(buildStoredQuery({sort: undefined, filter: 'done = false', s: 'foo', page: 1}))
|
||||
.toEqual({filter: 'done = false', s: 'foo'})
|
||||
})
|
||||
|
||||
it('omits page when it equals the default of 1', () => {
|
||||
expect(buildStoredQuery({sort: 'id:desc', filter: undefined, s: undefined, page: 1}))
|
||||
.toEqual({sort: 'id:desc'})
|
||||
})
|
||||
|
||||
it('includes page when greater than 1', () => {
|
||||
expect(buildStoredQuery({sort: undefined, filter: undefined, s: undefined, page: 3}))
|
||||
.toEqual({page: '3'})
|
||||
})
|
||||
|
||||
it('returns an empty object when nothing is set', () => {
|
||||
expect(buildStoredQuery({sort: undefined, filter: undefined, s: undefined, page: 1}))
|
||||
.toEqual({})
|
||||
})
|
||||
|
||||
it('skips empty strings', () => {
|
||||
expect(buildStoredQuery({sort: '', filter: '', s: '', page: 1}))
|
||||
.toEqual({})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,6 +1,4 @@
|
|||
import {ref, shallowReactive, watch, computed, type ComputedGetter} from 'vue'
|
||||
import {useRouter, isNavigationFailure} from 'vue-router'
|
||||
import type {LocationQueryRaw} from 'vue-router'
|
||||
import {useRouteQuery} from '@vueuse/router'
|
||||
|
||||
import TaskCollectionService, {
|
||||
|
|
@ -12,7 +10,6 @@ import type {ITask} from '@/modelTypes/ITask'
|
|||
import {error} from '@/message'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useViewFiltersStore} from '@/stores/viewFilters'
|
||||
import type {IProjectView} from '@/modelTypes/IProjectView'
|
||||
|
||||
export type Order = 'asc' | 'desc' | 'none'
|
||||
|
|
@ -62,22 +59,6 @@ const SORT_BY_DEFAULT: SortBy = {
|
|||
id: 'desc',
|
||||
}
|
||||
|
||||
interface TaskListQueryState {
|
||||
sort: string | undefined
|
||||
filter: string | undefined
|
||||
s: string | undefined
|
||||
page: number
|
||||
}
|
||||
|
||||
export function buildStoredQuery(state: TaskListQueryState): LocationQueryRaw {
|
||||
const query: LocationQueryRaw = {}
|
||||
if (state.sort) query.sort = state.sort
|
||||
if (state.filter) query.filter = state.filter
|
||||
if (state.s) query.s = state.s
|
||||
if (state.page > 1) query.page = String(state.page)
|
||||
return query
|
||||
}
|
||||
|
||||
// This makes sure an id sort order is always sorted last.
|
||||
// When tasks would be sorted first by id and then by whatever else was specified, the id sort takes
|
||||
// precedence over everything else, making any other sort columns pretty useless.
|
||||
|
|
@ -113,9 +94,6 @@ export function useTaskList(
|
|||
const projectId = computed(() => projectIdGetter())
|
||||
const projectViewId = computed(() => projectViewIdGetter())
|
||||
|
||||
const router = useRouter()
|
||||
const viewFiltersStore = useViewFiltersStore()
|
||||
|
||||
const params = ref<TaskFilterParams>({...getDefaultTaskFilterParams()})
|
||||
|
||||
const page = useRouteQuery('page', '1', { transform: Number })
|
||||
|
|
@ -141,55 +119,6 @@ export function useTaskList(
|
|||
},
|
||||
})
|
||||
|
||||
// Mirror the URL query bits this composable owns into the store so
|
||||
// in-project tab switches and sidebar re-visits can restore them.
|
||||
//
|
||||
// `ProjectList`/`ProjectTable` are reused across project switches (no
|
||||
// `:key` on them in ProjectView.vue), so setup runs only once. We track
|
||||
// the last viewId we synced — on every viewId transition, if the URL has
|
||||
// none of our params and the store has an entry, restore it via
|
||||
// `router.replace` and skip writing back the empty state we'd otherwise
|
||||
// clobber the saved entry with.
|
||||
let lastSyncedViewId: number | undefined
|
||||
watch(
|
||||
[projectViewId, sortQuery, filter, s, page],
|
||||
([viewId, sortValue, filterValue, sValue, pageValue]) => {
|
||||
const viewIdChanged = viewId !== lastSyncedViewId
|
||||
lastSyncedViewId = viewId
|
||||
|
||||
// An invalid `?page=` becomes NaN via `transform: Number`; treat it as
|
||||
// the default so it neither blocks restoration nor wipes stored state.
|
||||
const currentPage = Number.isInteger(pageValue) ? pageValue : 1
|
||||
const urlIsEmpty = !sortValue && !filterValue && !sValue && currentPage === 1
|
||||
if (viewIdChanged && urlIsEmpty) {
|
||||
const storedQuery = viewFiltersStore.getViewQuery(viewId)
|
||||
if (Object.keys(storedQuery).length > 0) {
|
||||
// Merge so unrelated query params on the route survive the restore.
|
||||
// Swallow navigation failures (e.g. aborted/duplicated) so the
|
||||
// ignored promise can't surface as an unhandled rejection.
|
||||
router.replace({query: {...router.currentRoute.value.query, ...storedQuery}})
|
||||
.catch(failure => {
|
||||
if (!isNavigationFailure(failure)) throw failure
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const query = buildStoredQuery({
|
||||
sort: sortValue as string | undefined,
|
||||
filter: filterValue as string | undefined,
|
||||
s: sValue as string | undefined,
|
||||
page: currentPage,
|
||||
})
|
||||
if (Object.keys(query).length > 0) {
|
||||
viewFiltersStore.setViewQuery(viewId, query)
|
||||
} else {
|
||||
viewFiltersStore.clearViewQuery(viewId)
|
||||
}
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
const allParams = computed(() => {
|
||||
const loadParams = {...params.value}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
import {watch} from 'vue'
|
||||
import {createSharedComposable, tryOnMounted} from '@vueuse/core'
|
||||
import {storeToRefs} from 'pinia'
|
||||
|
||||
import {useTimeTrackingStore} from '@/stores/timeTracking'
|
||||
import {getFullBaseUrl} from '@/helpers/getFullBaseUrl'
|
||||
|
||||
const TRACKING_FAVICON = `${getFullBaseUrl()}images/icons/favicon-tracking-32x32.png`
|
||||
|
||||
function getFaviconLink(): HTMLLinkElement | null {
|
||||
return document.querySelector<HTMLLinkElement>('link[rel="icon"]')
|
||||
}
|
||||
|
||||
// Swaps in a favicon with a small red dot in the lower left corner while a timer
|
||||
// is running, so an active time tracking session is visible even when the tab
|
||||
// isn't focused.
|
||||
export const useTimeTrackingFavicon = createSharedComposable(() => {
|
||||
const {hasActiveTimer} = storeToRefs(useTimeTrackingStore())
|
||||
|
||||
const originalHref = getFaviconLink()?.getAttribute('href') ?? '/favicon.ico'
|
||||
|
||||
function update(active: boolean) {
|
||||
const link = getFaviconLink()
|
||||
if (link === null) {
|
||||
return
|
||||
}
|
||||
link.href = active ? TRACKING_FAVICON : originalHref
|
||||
}
|
||||
|
||||
watch(hasActiveTimer, update, {flush: 'post'})
|
||||
tryOnMounted(() => update(hasActiveTimer.value))
|
||||
})
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
/**
|
||||
* Hash-fragment prefix used to carry a post-login destination in the URL.
|
||||
*
|
||||
* Unlike the localStorage redirect, this lives in the address bar so the URL
|
||||
* stays copyable between browsers (needed for native OAuth clients that open
|
||||
* /oauth/authorize, see #2654). It uses the hash – not a query param – so the
|
||||
* embedded OAuth parameters never reach server or proxy access logs.
|
||||
*
|
||||
* Must stay distinct from LINK_SHARE_HASH_PREFIX, which router.beforeEach
|
||||
* special-cases.
|
||||
*/
|
||||
export const REDIRECT_HASH_PREFIX = '#redirect='
|
||||
|
|
@ -1,153 +0,0 @@
|
|||
import {describe, it, expect, vi, beforeEach, afterEach} from 'vitest'
|
||||
|
||||
import {refreshToken, removeToken} from './auth'
|
||||
|
||||
// Count how many times the refresh endpoint is actually POSTed. The whole point
|
||||
// of the in-flight dedup is that concurrent refreshToken() calls share a single
|
||||
// underlying POST, independent of the Web Locks API.
|
||||
let postCallCount = 0
|
||||
let resolvePost: ((value: unknown) => void) | null = null
|
||||
|
||||
vi.mock('@/helpers/fetcher', () => ({
|
||||
HTTPFactory: () => ({
|
||||
post: vi.fn(() => {
|
||||
postCallCount++
|
||||
return new Promise((resolve) => {
|
||||
resolvePost = resolve
|
||||
})
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/helpers/desktopAuth', () => ({
|
||||
isDesktopApp: () => false,
|
||||
refreshDesktopToken: vi.fn(),
|
||||
}))
|
||||
|
||||
const FAKE_TOKEN = 'header.payload.signature'
|
||||
|
||||
function settlePost() {
|
||||
resolvePost?.({data: {token: FAKE_TOKEN}})
|
||||
}
|
||||
|
||||
describe('refreshToken in-flight dedup', () => {
|
||||
const originalLocks = navigator.locks
|
||||
|
||||
beforeEach(() => {
|
||||
postCallCount = 0
|
||||
resolvePost = null
|
||||
removeToken()
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(navigator, 'locks', {
|
||||
value: originalLocks,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('coalesces concurrent calls into a single POST when Web Locks is available', async () => {
|
||||
// Stub a minimal Web Locks API: happy-dom leaves navigator.locks
|
||||
// undefined, so without this the test would silently fall through to
|
||||
// the insecure-HTTP branch and never exercise navigator.locks.request.
|
||||
const requestSpy = vi.fn((_name: string, cb: () => unknown) => cb())
|
||||
Object.defineProperty(navigator, 'locks', {
|
||||
value: {request: requestSpy},
|
||||
configurable: true,
|
||||
writable: true,
|
||||
})
|
||||
|
||||
const p1 = refreshToken(true)
|
||||
const p2 = refreshToken(true)
|
||||
|
||||
// Both calls share one underlying request.
|
||||
expect(postCallCount).toBe(1)
|
||||
|
||||
settlePost()
|
||||
await Promise.all([p1, p2])
|
||||
|
||||
// The Web Locks branch actually ran...
|
||||
expect(requestSpy).toHaveBeenCalledWith('vikunja-token-refresh', expect.any(Function))
|
||||
// ...and the in-flight dedup still collapsed both calls into one POST.
|
||||
expect(postCallCount).toBe(1)
|
||||
})
|
||||
|
||||
it('coalesces concurrent calls into a single POST on insecure HTTP (no Web Locks)', async () => {
|
||||
// Simulate an insecure HTTP context where navigator.locks is undefined.
|
||||
Object.defineProperty(navigator, 'locks', {
|
||||
value: undefined,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
})
|
||||
|
||||
const p1 = refreshToken(true)
|
||||
const p2 = refreshToken(true)
|
||||
const p3 = refreshToken(true)
|
||||
|
||||
expect(postCallCount).toBe(1)
|
||||
|
||||
settlePost()
|
||||
await Promise.all([p1, p2, p3])
|
||||
|
||||
expect(postCallCount).toBe(1)
|
||||
})
|
||||
|
||||
it('allows a fresh refresh after the previous one settled', async () => {
|
||||
const p1 = refreshToken(true)
|
||||
settlePost()
|
||||
await p1
|
||||
expect(postCallCount).toBe(1)
|
||||
|
||||
// The in-flight promise was reset, so a later refresh runs anew.
|
||||
const p2 = refreshToken(true)
|
||||
expect(postCallCount).toBe(2)
|
||||
settlePost()
|
||||
await p2
|
||||
})
|
||||
|
||||
it('does not re-persist the token when logout happens during an in-flight refresh', async () => {
|
||||
const p1 = refreshToken(true)
|
||||
expect(postCallCount).toBe(1)
|
||||
|
||||
// User logs out while the refresh POST is still in flight.
|
||||
removeToken()
|
||||
|
||||
// The in-flight POST resolves afterwards — it must not undo the logout.
|
||||
settlePost()
|
||||
await p1
|
||||
|
||||
expect(localStorage.getItem('token')).toBeNull()
|
||||
})
|
||||
|
||||
it('an older refresh settling does not clobber a newer in-flight one', async () => {
|
||||
// Refresh A starts and stays in flight.
|
||||
const pA = refreshToken(true)
|
||||
expect(postCallCount).toBe(1)
|
||||
const resolveA = resolvePost
|
||||
|
||||
// User logs out, which drops the in-flight reference to A.
|
||||
removeToken()
|
||||
|
||||
// Refresh B starts; it must claim the in-flight slot.
|
||||
const pB = refreshToken(true)
|
||||
expect(postCallCount).toBe(2)
|
||||
const resolveB = resolvePost
|
||||
|
||||
// A settles after B started. Its cleanup must NOT null the in-flight
|
||||
// slot, since that slot now belongs to B. Without the `=== p` guard,
|
||||
// A's .finally would clobber B and let a concurrent caller fire a
|
||||
// second parallel POST.
|
||||
resolveA?.({data: {token: FAKE_TOKEN}})
|
||||
await pA
|
||||
|
||||
// A concurrent caller while B is still in flight must dedup to B —
|
||||
// no third POST.
|
||||
const pB2 = refreshToken(true)
|
||||
expect(postCallCount).toBe(2)
|
||||
|
||||
resolveB?.({data: {token: FAKE_TOKEN}})
|
||||
await Promise.all([pB, pB2])
|
||||
})
|
||||
})
|
||||
|
|
@ -33,53 +33,18 @@ export const removeToken = () => {
|
|||
savedToken = null
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('desktopOAuthRefreshToken')
|
||||
|
||||
// Bump the epoch and drop the in-flight refresh so a refresh that started
|
||||
// before this logout can't re-persist a token after we cleared it.
|
||||
authEpoch++
|
||||
inFlightRefresh = null
|
||||
}
|
||||
|
||||
// Coalesces concurrent same-tab refreshes into one POST. Web Locks (below) is
|
||||
// secure-context-only, so on insecure HTTP there's no cross-tab coordination —
|
||||
// without this guard, refreshes firing close together each spend the single-use
|
||||
// cookie and all but one get a 401.
|
||||
let inFlightRefresh: Promise<void> | null = null
|
||||
|
||||
// Incremented on every removeToken()/logout. A refresh captures the epoch when
|
||||
// it starts and only persists its result if the epoch is unchanged, so a
|
||||
// refresh that resolves after a logout can't undo it.
|
||||
let authEpoch = 0
|
||||
|
||||
/**
|
||||
* Refreshes an auth token while ensuring it is updated everywhere.
|
||||
* The refresh token is sent automatically as an HttpOnly cookie.
|
||||
* The server rotates the cookie on every call.
|
||||
*
|
||||
* Same-tab concurrent calls share one in-flight refresh (always-on dedup); the
|
||||
* Web Locks API inside adds cross-tab coordination only in secure contexts.
|
||||
* Uses the Web Locks API to coordinate across browser tabs. Only one tab
|
||||
* performs the actual refresh; other tabs waiting for the lock detect that
|
||||
* the token in localStorage was already updated and adopt it directly.
|
||||
*/
|
||||
export async function refreshToken(persist: boolean): Promise<void> {
|
||||
if (inFlightRefresh) {
|
||||
return inFlightRefresh
|
||||
}
|
||||
const p = doRefresh(persist)
|
||||
inFlightRefresh = p
|
||||
// Only clear if it still points to this promise — a logout (or a newer
|
||||
// refresh started after it) may have replaced inFlightRefresh meanwhile.
|
||||
p.finally(() => {
|
||||
if (inFlightRefresh === p) {
|
||||
inFlightRefresh = null
|
||||
}
|
||||
})
|
||||
return p
|
||||
}
|
||||
|
||||
async function doRefresh(persist: boolean): Promise<void> {
|
||||
// Snapshot the epoch so we can tell if a logout happened while we awaited.
|
||||
const epochAtStart = authEpoch
|
||||
const loggedOutSinceStart = () => authEpoch !== epochAtStart
|
||||
|
||||
// In desktop mode, refresh via IPC to the Electron main process
|
||||
if (isDesktopApp()) {
|
||||
const storedRefreshToken = localStorage.getItem('desktopOAuthRefreshToken')
|
||||
|
|
@ -88,9 +53,6 @@ async function doRefresh(persist: boolean): Promise<void> {
|
|||
}
|
||||
try {
|
||||
const tokens = await refreshDesktopToken(window.API_URL, storedRefreshToken)
|
||||
if (loggedOutSinceStart()) {
|
||||
return
|
||||
}
|
||||
saveToken(tokens.access_token, persist)
|
||||
localStorage.setItem('desktopOAuthRefreshToken', tokens.refresh_token)
|
||||
} catch (e) {
|
||||
|
|
@ -103,13 +65,7 @@ async function doRefresh(persist: boolean): Promise<void> {
|
|||
// if another tab refreshed while we were queued.
|
||||
const tokenBeforeLock = localStorage.getItem('token')
|
||||
|
||||
const refreshUnderLock = async () => {
|
||||
// A logout may have happened while we waited for the lock — don't
|
||||
// re-adopt or re-fetch a token after the user signed out.
|
||||
if (loggedOutSinceStart()) {
|
||||
return
|
||||
}
|
||||
|
||||
const doRefresh = async () => {
|
||||
// If the token in localStorage changed while waiting for the lock,
|
||||
// another tab already refreshed. Just adopt the new token.
|
||||
const currentToken = localStorage.getItem('token')
|
||||
|
|
@ -122,9 +78,6 @@ async function doRefresh(persist: boolean): Promise<void> {
|
|||
const HTTP = HTTPFactory()
|
||||
try {
|
||||
const response = await HTTP.post('user/token/refresh')
|
||||
if (loggedOutSinceStart()) {
|
||||
return
|
||||
}
|
||||
saveToken(response.data.token, persist)
|
||||
} catch (e) {
|
||||
throw new Error('Error renewing token: ', {cause: e})
|
||||
|
|
@ -132,10 +85,10 @@ async function doRefresh(persist: boolean): Promise<void> {
|
|||
}
|
||||
|
||||
if (navigator.locks) {
|
||||
await navigator.locks.request('vikunja-token-refresh', refreshUnderLock)
|
||||
await navigator.locks.request('vikunja-token-refresh', doRefresh)
|
||||
} else {
|
||||
// Fallback for environments without Web Locks (e.g. insecure HTTP)
|
||||
await refreshUnderLock()
|
||||
await doRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,9 +10,5 @@ export function getProjectTitle(project: IProject) {
|
|||
return i18n.global.t('project.inboxTitle')
|
||||
}
|
||||
|
||||
if (project.title === 'My Open Tasks') {
|
||||
return i18n.global.t('project.myOpenTasksFilterTitle')
|
||||
}
|
||||
|
||||
return project.title
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,17 +2,10 @@ import {createRandomID} from '@/helpers/randomId'
|
|||
import {computePosition, flip, shift, offset} from '@floating-ui/dom'
|
||||
import {nextTick} from 'vue'
|
||||
import {eventToShortcutString} from '@/helpers/shortcut'
|
||||
import type {Editor} from '@tiptap/core'
|
||||
import {getPopupContainer} from '@/components/input/editor/popupContainer'
|
||||
|
||||
export default function inputPrompt(pos: ClientRect, oldValue: string = '', editor?: Editor): Promise<string> {
|
||||
export default function inputPrompt(pos: ClientRect, oldValue: string = ''): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
const id = 'link-input-' + createRandomID()
|
||||
// Append inside the open task <dialog> (top-layer) when present, otherwise
|
||||
// document.body. A body-level popup is painted behind a showModal() dialog
|
||||
// and unfocusable through its focus trap, breaking the link prompt in the
|
||||
// Kanban task popup (#2940).
|
||||
const container = getPopupContainer(editor)
|
||||
|
||||
// Create popup element
|
||||
const popupElement = document.createElement('div')
|
||||
|
|
@ -33,7 +26,7 @@ export default function inputPrompt(pos: ClientRect, oldValue: string = '', edit
|
|||
inputElement.value = oldValue
|
||||
wrapperDiv.appendChild(inputElement)
|
||||
popupElement.appendChild(wrapperDiv)
|
||||
container.appendChild(popupElement)
|
||||
document.body.appendChild(popupElement)
|
||||
|
||||
// Create a local mutable copy of the position for scroll tracking
|
||||
let currentRect = new DOMRect(pos.left, pos.top, pos.width, pos.height)
|
||||
|
|
@ -89,41 +82,15 @@ export default function inputPrompt(pos: ClientRect, oldValue: string = '', edit
|
|||
|
||||
nextTick(() => document.getElementById(id)?.focus())
|
||||
|
||||
// The prompt is a sub-modal of the enclosing task <dialog>. Native modal
|
||||
// dialogs close themselves on Escape ("cancel"); swallow that while the
|
||||
// prompt is open so Escape only dismisses the prompt, not the task dialog.
|
||||
const dialog = container.closest('dialog') as HTMLDialogElement | null
|
||||
const handleDialogCancel = (event: Event) => event.preventDefault()
|
||||
dialog?.addEventListener('cancel', handleDialogCancel)
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (!popupElement.contains(event.target as Node)) {
|
||||
resolve('')
|
||||
cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
window.removeEventListener('scroll', handleScroll, true)
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
dialog?.removeEventListener('cancel', handleDialogCancel)
|
||||
if (container.contains(popupElement)) {
|
||||
container.removeChild(popupElement)
|
||||
if (document.body.contains(popupElement)) {
|
||||
document.body.removeChild(popupElement)
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById(id)?.addEventListener('keydown', event => {
|
||||
const shortcutString = eventToShortcutString(event)
|
||||
|
||||
if (shortcutString === 'Escape') {
|
||||
// Stop the native <dialog> from closing on Escape; cancel the prompt only.
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
resolve('')
|
||||
cleanup()
|
||||
return
|
||||
}
|
||||
|
||||
if (shortcutString !== 'Enter') {
|
||||
return
|
||||
}
|
||||
|
|
@ -138,6 +105,15 @@ export default function inputPrompt(pos: ClientRect, oldValue: string = '', edit
|
|||
cleanup()
|
||||
})
|
||||
|
||||
// Close on click outside
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (!popupElement.contains(event.target as Node)) {
|
||||
resolve('')
|
||||
cleanup()
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
}
|
||||
}
|
||||
|
||||
// Add slight delay to prevent immediate closing
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
|
|
|
|||
|
|
@ -24,10 +24,8 @@ export const redirectToProvider = (provider: IProvider) => {
|
|||
window.location.href = `${provider.authUrl}?client_id=${provider.clientId}&redirect_uri=${redirectUrl}&response_type=code&scope=${scope}&state=${state}`
|
||||
}
|
||||
|
||||
export const redirectToProviderOnLogout = (provider: IProvider): boolean => {
|
||||
export const redirectToProviderOnLogout = (provider: IProvider) => {
|
||||
if (provider.logoutUrl.length > 0) {
|
||||
window.location.href = `${provider.logoutUrl}`
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import {i18n} from '@/i18n'
|
|||
import {createSharedComposable} from '@vueuse/core'
|
||||
import {computed, toValue, type MaybeRefOrGetter} from 'vue'
|
||||
import {useDateDisplay} from '@/composables/useDateDisplay'
|
||||
import {useGlobalNow} from '@/composables/useGlobalNow'
|
||||
import {useTimeFormat} from '@/composables/useTimeFormat'
|
||||
import {DATE_DISPLAY, type DateDisplay} from '@/constants/dateDisplay'
|
||||
import {TIME_FORMAT, type TimeFormat} from '@/constants/timeFormat'
|
||||
|
|
@ -50,13 +49,8 @@ export const formatDateSince = (date: Date | string | null) => {
|
|||
|
||||
const locale = DAYJS_LOCALE_MAPPING[i18n.global.locale.value.toLowerCase()] ?? 'en'
|
||||
|
||||
// Computing the relative string against the shared, ticking `now` (instead of fromNow's
|
||||
// internal Date.now()) makes every reactive caller re-render on the 60s tick, so open views
|
||||
// don't keep showing a stale "x minutes ago".
|
||||
const {now} = useGlobalNow()
|
||||
|
||||
return date
|
||||
? dayjs(date).locale(locale).from(now.value)
|
||||
? dayjs(date).locale(locale).fromNow()
|
||||
: ''
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ export const SUPPORTED_LOCALES = {
|
|||
'ja-JP': '日本語',
|
||||
'hu-HU': 'Magyar',
|
||||
'ar-SA': 'اَلْعَرَبِيَّةُ',
|
||||
'fa-IR': 'فارسی',
|
||||
'sl-SI': 'Slovenščina',
|
||||
'pt-BR': 'Português Brasileiro',
|
||||
'hr-HR': 'Hrvatski',
|
||||
|
|
@ -53,7 +52,7 @@ export const DEFAULT_LANGUAGE: SupportedLocale= 'en'
|
|||
|
||||
export type ISOLanguage = string
|
||||
|
||||
const RTL_LANGUAGES = ['ar-SA', 'he-IL', 'fa-IR'] as const
|
||||
const RTL_LANGUAGES = ['ar-SA', 'he-IL'] as const
|
||||
|
||||
export function isRTLLanguage(locale: SupportedLocale): boolean {
|
||||
return RTL_LANGUAGES.includes(locale as typeof RTL_LANGUAGES[number])
|
||||
|
|
|
|||
|
|
@ -349,7 +349,6 @@
|
|||
"shared": "Geteilte Projekte",
|
||||
"noDescriptionAvailable": "Keine Projektbeschreibung verfügbar.",
|
||||
"inboxTitle": "Eingang",
|
||||
"myOpenTasksFilterTitle": "Meine offenen Aufgaben",
|
||||
"favorite": "Dieses Projekt als Favorit markieren",
|
||||
"unfavorite": "Dieses Projekt von Favoriten entfernen",
|
||||
"openSettingsMenu": "Projekteinstellungen öffnen",
|
||||
|
|
@ -394,7 +393,6 @@
|
|||
"title": "Dupliziere dieses Projekt",
|
||||
"label": "Duplizieren",
|
||||
"text": "Wähle ein übergeordnetes Projekt aus, welches das duplizierte Projekt enthalten soll:",
|
||||
"shares": "Freigaben kopieren (Benutzer:innen, Teams und Linkfreigaben)",
|
||||
"success": "Das Projekt wurde erfolgreich dupliziert."
|
||||
},
|
||||
"edit": {
|
||||
|
|
|
|||
|
|
@ -349,7 +349,6 @@
|
|||
"shared": "Geteilte Projekte",
|
||||
"noDescriptionAvailable": "Keine Projektbeschreibung verfügbar.",
|
||||
"inboxTitle": "Eingang",
|
||||
"myOpenTasksFilterTitle": "Meine offenen Aufgaben",
|
||||
"favorite": "Dieses Projekt als Favorit markieren",
|
||||
"unfavorite": "Dieses Projekt von Favoriten entfernen",
|
||||
"openSettingsMenu": "Projekteinstellungen öffnen",
|
||||
|
|
@ -394,7 +393,6 @@
|
|||
"title": "Dupliziere dieses Projekt",
|
||||
"label": "Duplizieren",
|
||||
"text": "Wähle ein übergeordnetes Projekt aus, welches das duplizierte Projekt enthalten soll:",
|
||||
"shares": "Freigaben kopieren (Benutzer:innen, Teams und Linkfreigaben)",
|
||||
"success": "Das Projekt wurde erfolgreich dupliziert."
|
||||
},
|
||||
"edit": {
|
||||
|
|
|
|||
|
|
@ -172,7 +172,6 @@
|
|||
"yyyy/mm/dd": "ΕΕΕΕ/ΜΜ/ΗΗ"
|
||||
},
|
||||
"timeFormat": "Μορφή ώρας",
|
||||
"timeTrackingDefaultStart": "Ώρα έναρξης παρακολούθησης χρόνου με έξυπνο-γέμισμα",
|
||||
"timeFormatOptions": {
|
||||
"12h": "12 ώρες (ΠΜ/ΜΜ)",
|
||||
"24h": "24 ώρες (ΩΩ:ΛΛ)"
|
||||
|
|
@ -393,7 +392,6 @@
|
|||
"title": "Αντιγραφή του έργου",
|
||||
"label": "Αντιγραφή",
|
||||
"text": "Επιλέξτε ένα γονικό έργο που θα περιλαμβάνει το αντίγραφο του έργου:",
|
||||
"shares": "Αντιγραφή διαμοιρασμών (χρήστες, ομάδες και σύνδεσμοι διαμοιρασμού) στο αντίγραφο",
|
||||
"success": "Το έργο αντιγράφηκε με επιτυχία."
|
||||
},
|
||||
"edit": {
|
||||
|
|
@ -783,10 +781,7 @@
|
|||
"closeDialog": "Κλείσμο του διαλόγου",
|
||||
"closeQuickActions": "Κλείσιμο των γρήγορων ενεργειών",
|
||||
"skipToContent": "Μετάβαση στο κύριο περιεχόμενο",
|
||||
"sortBy": "Ταξινόμηση ανά",
|
||||
"dateRange": "Εύρος ημερομηνιών",
|
||||
"notSet": "Μη ορισμένο",
|
||||
"user": "Χρήστης"
|
||||
"sortBy": "Ταξινόμηση ανά"
|
||||
},
|
||||
"input": {
|
||||
"projectColor": "Χρώμα έργου",
|
||||
|
|
@ -996,7 +991,6 @@
|
|||
"repeatAfter": "Ορισμός Επαναλαμβανόμενου Διαστήματος",
|
||||
"percentDone": "Ορισμός Προόδου",
|
||||
"attachments": "Προσθήκη Συνημμένων",
|
||||
"timeTracking": "Χρόνος ίχνους",
|
||||
"relatedTasks": "Προσθήκη Συσχέτισης",
|
||||
"moveProject": "Μετακίνηση",
|
||||
"duplicate": "Αντιγραφή",
|
||||
|
|
@ -1466,32 +1460,6 @@
|
|||
"frontendVersion": "Έκδοση frontend: {version}",
|
||||
"apiVersion": "Έκδοση API: {version}"
|
||||
},
|
||||
"timeTracking": {
|
||||
"title": "Ιχνηλάτηση χρόνου",
|
||||
"stop": "Διακοπή χρονομέτρου",
|
||||
"logTime": "Καταγραφή χρόνου",
|
||||
"editEntry": "Επεξεργασία εγγραφής",
|
||||
"form": {
|
||||
"task": "Εργασία",
|
||||
"taskSearch": "Αναζήτηση για μια εργασία…",
|
||||
"commentPlaceholder": "Σε τι δουλέψατε;",
|
||||
"save": "Αποθήκευση εγγραφής",
|
||||
"startTimer": "Έναρξη χρονοµέτρου",
|
||||
"update": "Ενημέρωση εγγραφής",
|
||||
"smartFill": "Συμπλήρωση από την τελευταία καταχώριση"
|
||||
},
|
||||
"list": {
|
||||
"emptyTask": "Δεν καταγράφηκε ακόμη χρόνος για αυτήν την εργασία.",
|
||||
"emptyFiltered": "Δεν καταγράφηκε χρόνος με βάση τα επιλεγμένα φίλτρα.",
|
||||
"total": "Σύνολο",
|
||||
"time": "Ώρα",
|
||||
"duration": "Διάρκεια"
|
||||
},
|
||||
"browse": {
|
||||
"selectRange": "Επιλέξτε ένα εύρος",
|
||||
"userSearch": "Αναζήτηση για ένα χρήστη…"
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
"units": {
|
||||
"seconds": "δευτερόλεπτο|δευτερόλεπτα",
|
||||
|
|
|
|||
|
|
@ -349,7 +349,6 @@
|
|||
"shared": "Shared Projects",
|
||||
"noDescriptionAvailable": "No project description is available.",
|
||||
"inboxTitle": "Inbox",
|
||||
"myOpenTasksFilterTitle": "My Open Tasks",
|
||||
"favorite": "Mark this project as favorite",
|
||||
"unfavorite": "Remove this project from favorites",
|
||||
"openSettingsMenu": "Open project settings menu",
|
||||
|
|
@ -394,7 +393,6 @@
|
|||
"title": "Duplicate this project",
|
||||
"label": "Duplicate",
|
||||
"text": "Select a parent project which should hold the duplicated project:",
|
||||
"shares": "Copy shares (users, teams and link shares) to the duplicate",
|
||||
"success": "The project was successfully duplicated."
|
||||
},
|
||||
"edit": {
|
||||
|
|
|
|||
|
|
@ -5,32 +5,9 @@
|
|||
},
|
||||
"home": {
|
||||
"welcomeNight": "Доброй ночи, {username}!",
|
||||
"welcomeNightOwl": "Привет, ночная сова {username}",
|
||||
"welcomeNightBurning": "Работаешь допоздна, {username}?",
|
||||
"welcomeNightQuiet": "Тихие часы, {username}",
|
||||
"welcomeNightLate": "Поздно, {username}",
|
||||
"welcomeMorning": "Доброе утро, {username}!",
|
||||
"welcomeMorningHey": "Привет, {username}, готов?",
|
||||
"welcomeMorningFresh": "Свежий старт, {username}",
|
||||
"welcomeMorningCoffee": "Кофе и задачи, {username}?",
|
||||
"welcomeMorningRise": "Проснись и планируй, {username}",
|
||||
"welcomeMorningBack": "С возвращением, {username}",
|
||||
"welcomeMondayFresh": "Свежая неделя, {username}",
|
||||
"welcomeTuesday": "Счастливого вторника, {username}",
|
||||
"welcomeWednesdayMid": "Уже середина недели, {username}",
|
||||
"welcomeThursday": "Почти готово, {username}",
|
||||
"welcomeFridayPush": "Пятница, {username}?",
|
||||
"welcomeSaturday": "Режим выходных, {username}",
|
||||
"welcomeSundaySession": "Воскресный сеанс, {username}?",
|
||||
"welcomeDay": "Привет, {username}!",
|
||||
"welcomeDayFocus": "Давайте сосредоточимся, {username}",
|
||||
"welcomeDayKeepGoing": "Так держать, {username}",
|
||||
"welcomeDayWhatsNext": "Что дальше, {username}?",
|
||||
"welcomeDayGood": "Добрый день, {username}",
|
||||
"welcomeEvening": "Добрый вечер, {username}!",
|
||||
"welcomeEveningWind": "Заканчиваешь, {username}?",
|
||||
"welcomeEveningReturns": "{username} возвращается",
|
||||
"welcomeEveningOneMore": "Ещё одна вещь, {username}?",
|
||||
"lastViewed": "Последние просмотренные",
|
||||
"addToHomeScreen": "Добавьте это приложение на домашний экран для быстрого доступа и удобной работы.",
|
||||
"goToOverview": "Перейти к обзору",
|
||||
|
|
@ -80,11 +57,6 @@
|
|||
"openIdTotpSubmit": "Продолжить",
|
||||
"oauthMissingParams": "Отсутствуют необходимые параметры OAuth: {params}",
|
||||
"oauthRedirectedToApp": "Вы были перенаправлены в приложение. Теперь вы можете закрыть эту вкладку.",
|
||||
"desktopTryDemo": "Попробовать демо-версию",
|
||||
"desktopCustomServer": "Пользовательский URL сервера",
|
||||
"desktopCustomServerDescription": "Введите URL сервера Vikunja, чтобы начать.",
|
||||
"desktopWaitingForAuth": "Ожидание аутентификации…",
|
||||
"desktopOAuthError": "Ошибка аутентификации: {error}",
|
||||
"logout": "Выйти",
|
||||
"emailInvalid": "Введите корректный email адрес.",
|
||||
"usernameRequired": "Введите имя пользователя.",
|
||||
|
|
@ -103,19 +75,6 @@
|
|||
"registrationFailed": "Произошла ошибка при регистрации. Проверьте введённые данные и повторите попытку."
|
||||
},
|
||||
"settings": {
|
||||
"bots": {
|
||||
"title": "Боты",
|
||||
"description": "Боты — это пользователи, которые принадлежат вам и которые имеют доступ только к API. Их можно добавить в проекты, назначить задачи, и аутентификация выполняется с помощью токенов API. Боты не могут использовать обычный интерфейс.",
|
||||
"namePlaceholder": "Мой помощник",
|
||||
"create": "Создать бота",
|
||||
"enable": "Включить",
|
||||
"badge": "Бот",
|
||||
"delete": {
|
||||
"header": "Удалить бота",
|
||||
"text1": "Удалить бота «{username}»?",
|
||||
"text2": "Это необратимо. Любые токены API, принадлежащие этому боту, будут аннулированы."
|
||||
}
|
||||
},
|
||||
"title": "Настройки",
|
||||
"newPasswordTitle": "Изменить пароль",
|
||||
"newPassword": "Новый пароль",
|
||||
|
|
@ -141,11 +100,6 @@
|
|||
"weekStart": "Первый день недели",
|
||||
"weekStartSunday": "Воскресенье",
|
||||
"weekStartMonday": "Понедельник",
|
||||
"weekStartTuesday": "Вторник",
|
||||
"weekStartWednesday": "Среда",
|
||||
"weekStartThursday": "Четверг",
|
||||
"weekStartFriday": "Пятница",
|
||||
"weekStartSaturday": "Суббота",
|
||||
"language": "Язык",
|
||||
"defaultProject": "Проект по умолчанию",
|
||||
"defaultView": "Представление по умолчанию",
|
||||
|
|
@ -179,13 +133,7 @@
|
|||
"taskAndNotifications": "Проекты и задачи",
|
||||
"privacy": "Конфиденциальность",
|
||||
"localization": "Локализация",
|
||||
"appearance": "Внешний вид и поведение",
|
||||
"desktop": "Настольное приложение"
|
||||
},
|
||||
"desktop": {
|
||||
"quickEntryShortcut": "Ярлык быстрого входа",
|
||||
"shortcutRecorderPlaceholder": "Нажмите, чтобы задать ярлык",
|
||||
"shortcutRecorderRecording": "Нажмите комбинацию клавиш…"
|
||||
"appearance": "Внешний вид и поведение"
|
||||
},
|
||||
"totp": {
|
||||
"title": "Двухфакторная аутентификация",
|
||||
|
|
@ -215,13 +163,6 @@
|
|||
"usernameIs": "Имя пользователя для CalDAV: {0}",
|
||||
"apiTokenHint": "Вы также можете использовать токен API с разрешением CalDAV. Создайте его в {link}."
|
||||
},
|
||||
"feeds": {
|
||||
"title": "Atom-лента",
|
||||
"howTo": "Вы можете подписаться на уведомления Vikunja в любом приложении для чтения новостей, поддерживающем Atom-ленты. Используйте следующий URL:",
|
||||
"usernameIs": "Имя пользователя для доступа к ленте: {0}",
|
||||
"apiTokenHint": "Для аутентификации используйте токен API с разрешением {scope}. Создайте его на странице {link}.",
|
||||
"tokenTitle": "Atom-лента"
|
||||
},
|
||||
"avatar": {
|
||||
"title": "Аватар",
|
||||
"initials": "Инициалы",
|
||||
|
|
@ -344,7 +285,6 @@
|
|||
"shared": "Общие проекты",
|
||||
"noDescriptionAvailable": "Описание проекта отсутствует.",
|
||||
"inboxTitle": "Входящие",
|
||||
"myOpenTasksFilterTitle": "Мои открытые задачи",
|
||||
"favorite": "Отметить проект как избранный",
|
||||
"unfavorite": "Удалить проект из избранного",
|
||||
"openSettingsMenu": "Открыть настройки проекта",
|
||||
|
|
@ -389,7 +329,6 @@
|
|||
"title": "Создание копии проекта",
|
||||
"label": "Создать копию",
|
||||
"text": "Выберите родительский проект, в который поместить копию проекта:",
|
||||
"shares": "Скопировать настройки доступа (пользователей, групп и ссылок для обмена)",
|
||||
"success": "Копия проекта создана."
|
||||
},
|
||||
"edit": {
|
||||
|
|
@ -486,8 +425,7 @@
|
|||
"partialDatesStart": "Только дата начала (без окончания)",
|
||||
"partialDatesEnd": "Только дата окончания (без начала)",
|
||||
"expandGroup": "Развернуть группу: {task}",
|
||||
"collapseGroup": "Свернуть группу: {task}",
|
||||
"toggleRelationArrows": "Переключить стрелки связи"
|
||||
"collapseGroup": "Свернуть группу: {task}"
|
||||
},
|
||||
"table": {
|
||||
"title": "Таблица",
|
||||
|
|
@ -516,8 +454,7 @@
|
|||
"bucketTitleSavedSuccess": "Название колонки сохранено.",
|
||||
"bucketLimitSavedSuccess": "Лимит колонки сохранён.",
|
||||
"collapse": "Свернуть эту колонку",
|
||||
"bucketLimitReached": "Вы достигли лимита колонки. Удалите какие-нибудь задачи или увеличьте лимит, чтобы добавить новые задачи.",
|
||||
"bucketOptions": "Настройки колонки"
|
||||
"bucketLimitReached": "Вы достигли лимита колонки. Удалите какие-нибудь задачи или увеличьте лимит, чтобы добавить новые задачи."
|
||||
},
|
||||
"pseudo": {
|
||||
"favorites": {
|
||||
|
|
@ -740,9 +677,7 @@
|
|||
"upcoming": "Предстоящие задачи",
|
||||
"settings": "Настройки",
|
||||
"imprint": "Отпечаток",
|
||||
"privacy": "Политика конфиденциальности",
|
||||
"closeSidebar": "Закрыть боковую панель",
|
||||
"home": "Главная страница Vikunja"
|
||||
"privacy": "Политика конфиденциальности"
|
||||
},
|
||||
"misc": {
|
||||
"loading": "Загрузка…",
|
||||
|
|
@ -774,17 +709,9 @@
|
|||
"createdBy": "Создатель {0}",
|
||||
"actions": "Действия",
|
||||
"cannotBeUndone": "Это действие отменить нельзя!",
|
||||
"avatarOfUser": "Изображение профиля {user}",
|
||||
"closeBanner": "Закрыть баннер",
|
||||
"closeDialog": "Закрыть диалог",
|
||||
"closeQuickActions": "Закрыть быстрые действия",
|
||||
"skipToContent": "Перейти к основному содержимому",
|
||||
"dateRange": "Диапазон",
|
||||
"notSet": "Не задано",
|
||||
"user": "Пользователь"
|
||||
"avatarOfUser": "Изображение профиля {user}"
|
||||
},
|
||||
"input": {
|
||||
"projectColor": "Цвет проекта",
|
||||
"resetColor": "Сбросить цвет",
|
||||
"datepicker": {
|
||||
"today": "Сегодня",
|
||||
|
|
@ -857,7 +784,6 @@
|
|||
"date": "Дата",
|
||||
"ranges": {
|
||||
"today": "Сегодня",
|
||||
"tomorrow": "Завтра",
|
||||
"thisWeek": "Эта неделя",
|
||||
"restOfThisWeek": "Остаток этой недели",
|
||||
"nextWeek": "Следующая неделя",
|
||||
|
|
@ -965,8 +891,6 @@
|
|||
"belongsToProject": "Задача принадлежит проекту «{project}»",
|
||||
"back": "Вернуться к проекту",
|
||||
"due": "Истекает {at}",
|
||||
"closeTaskDetail": "Закрыть детали задачи",
|
||||
"title": "Детали задачи",
|
||||
"scrollToBottom": "Прокрутить до конца страницы",
|
||||
"organization": "Организация",
|
||||
"management": "Управление",
|
||||
|
|
@ -1060,10 +984,7 @@
|
|||
"addedSuccess": "Комментарий добавлен.",
|
||||
"permalink": "Скопировать постоянную ссылку на комментарий",
|
||||
"sortNewestFirst": "Сначала новые",
|
||||
"sortOldestFirst": "Сначала старые",
|
||||
"reply": "Ответить",
|
||||
"jumpToOriginal": "Перейти к исходному комментарию",
|
||||
"deletedComment": "удалённый комментарий"
|
||||
"sortOldestFirst": "Сначала старые"
|
||||
},
|
||||
"mention": {
|
||||
"noUsersFound": "Пользователи не найдены"
|
||||
|
|
@ -1327,11 +1248,9 @@
|
|||
"none": "Уведомлений нет. Хорошего дня!",
|
||||
"explainer": "Здесь появятся уведомления, когда что-нибудь произойдёт с проектами или задачами, на которые вы подписаны.",
|
||||
"markAllRead": "Отметить всё как прочитанное",
|
||||
"markAllReadSuccess": "Все уведомления отмечены как прочитанные.",
|
||||
"subscribeFeed": "Подписаться на уведомления через Atom-ленту"
|
||||
"markAllReadSuccess": "Все уведомления отмечены как прочитанные."
|
||||
},
|
||||
"quickActions": {
|
||||
"notLoggedIn": "Сначала войдите в главное окно Vikunja.",
|
||||
"commands": "Команды",
|
||||
"placeholder": "Введите команду или поисковый запрос…",
|
||||
"hint": "Используйте {project}, чтобы ограничить поиск проектом. Комбинируйте {project} и {label} (метки) с поисковым запросом для поиска задачи с этими метками или на этом проекте. Используйте {assignee} для поиска команд.",
|
||||
|
|
@ -1458,66 +1377,5 @@
|
|||
"weeks": "неделя|недели|недель",
|
||||
"years": "год|года|лет"
|
||||
}
|
||||
},
|
||||
"admin": {
|
||||
"title": "Администрирование",
|
||||
"labels": {
|
||||
"users": "Пользователи",
|
||||
"tasks": "Задачи"
|
||||
},
|
||||
"overview": {
|
||||
"shares": "Общий доступ",
|
||||
"linkSharesShort": "ссылка",
|
||||
"teamSharesShort": "группа",
|
||||
"userSharesShort": "пользователь",
|
||||
"version": "Версия",
|
||||
"license": "Лицензия",
|
||||
"licenseValidUntil": "Истекает",
|
||||
"licenseExpiresIn": "через {days} дней",
|
||||
"licenseLastVerified": "Последняя проверка",
|
||||
"licenseNever": "никогда",
|
||||
"licenseLastCheckFailed": "последняя проверка не удалась",
|
||||
"licenseFeatures": "Возможности",
|
||||
"licenseInstance": "ID экземпляра",
|
||||
"licenseManage": "Управление"
|
||||
},
|
||||
"searchUsersPlaceholder": "Поиск по имени пользователя или электронной почте…",
|
||||
"users": {
|
||||
"status": "Статус",
|
||||
"details": "Детали",
|
||||
"detailsTitle": "Пользователь: {username}",
|
||||
"issuer": "Издатель",
|
||||
"issuerLocal": "Локальный",
|
||||
"issuerUrl": "URL издателя",
|
||||
"subject": "Тема",
|
||||
"statusActive": "Активен",
|
||||
"statusEmailConfirmation": "Нужно подтвердить почту",
|
||||
"statusDisabled": "Отключен",
|
||||
"statusLocked": "Заблокирован",
|
||||
"isAdminLabel": "Администратор",
|
||||
"addUser": "Добавить пользователя",
|
||||
"createTitle": "Создать пользователя",
|
||||
"nameLabel": "Имя",
|
||||
"skipEmailConfirm": "Пропустить подтверждение по электронной почте",
|
||||
"createSubmit": "Создать пользователя",
|
||||
"saveButton": "Сохранить изменения",
|
||||
"createdSuccess": "Пользователь {username} создан.",
|
||||
"updatedSuccess": "Пользователь {username} обновлён.",
|
||||
"deletedSuccess": "Пользователь {username} удалён.",
|
||||
"deleteScheduledSuccess": "Пользователь {username} получит подтверждение по электронной почте для запланированного удаления.",
|
||||
"confirmDeleteTitle": "Удалить пользователя?",
|
||||
"confirmDeleteIntro": "Как следует удалить пользователя {username}?",
|
||||
"deleteModeScheduled": "Запланировать удаление",
|
||||
"deleteModeScheduledHelp": "Запланированное удаление отправляет пользователю письмо с подтверждением, как если бы пользователь сам запросил удаление аккаунта.",
|
||||
"deleteModeNow": "Удалить сейчас",
|
||||
"deleteModeNowHelp": "Удаление сейчас удаляет пользователя и все его данные сразу. Это не может быть отменено."
|
||||
},
|
||||
"projects": {
|
||||
"ownerLabel": "Владелец",
|
||||
"reassignOwner": "Переназначить владельца",
|
||||
"reassignTitle": "Переназначить {title}",
|
||||
"reassignedSuccess": "Владелец проекта переназначен.",
|
||||
"newOwnerLabel": "Новый владелец"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -172,7 +172,6 @@
|
|||
"yyyy/mm/dd": "YYYY/MM/DD"
|
||||
},
|
||||
"timeFormat": "Формат часу",
|
||||
"timeTrackingDefaultStart": "Початковий час для автоматичного заповнення обліку часу",
|
||||
"timeFormatOptions": {
|
||||
"12h": "12-годинний (AM/PM)",
|
||||
"24h": "24-годинний (HH:mm)"
|
||||
|
|
@ -393,7 +392,6 @@
|
|||
"title": "Дублювати цей проєкт",
|
||||
"label": "Дублювати",
|
||||
"text": "Оберіть батьківський проєкт, який повинен складатися з дубльованих проєктів:",
|
||||
"shares": "Скопіювати налаштування спільного доступу (користувачів, команди та посилання) до копії проєкту",
|
||||
"success": "Проєкт дубльовано."
|
||||
},
|
||||
"edit": {
|
||||
|
|
@ -783,10 +781,7 @@
|
|||
"closeDialog": "Закрити діалог",
|
||||
"closeQuickActions": "Закрити швидкі дії",
|
||||
"skipToContent": "Перейти до основного вмісту",
|
||||
"sortBy": "Сортувати за",
|
||||
"dateRange": "Діапазон дат",
|
||||
"notSet": "Не встановлено",
|
||||
"user": "Користувач"
|
||||
"sortBy": "Сортувати за"
|
||||
},
|
||||
"input": {
|
||||
"projectColor": "Колір проєкту",
|
||||
|
|
@ -989,14 +984,13 @@
|
|||
"assign": "Доручити",
|
||||
"label": "Позначки",
|
||||
"priority": "Встановити пріоритет",
|
||||
"dueDate": "Встановити термін виконання",
|
||||
"dueDate": "Встановити термін",
|
||||
"startDate": "Почати",
|
||||
"endDate": "Встановити дату завершення",
|
||||
"reminders": "Нагадування",
|
||||
"repeatAfter": "Повторювати",
|
||||
"percentDone": "Встановити прогрес",
|
||||
"attachments": "Вкласти",
|
||||
"timeTracking": "Відстежити час",
|
||||
"relatedTasks": "Пов'язати",
|
||||
"moveProject": "Перемістити",
|
||||
"duplicate": "Дублювати",
|
||||
|
|
@ -1152,7 +1146,6 @@
|
|||
"repeat": {
|
||||
"everyDay": "Щодня",
|
||||
"everyWeek": "Щотижня",
|
||||
"every30d": "Кожні 30 днів",
|
||||
"mode": "Спосіб",
|
||||
"monthly": "Щомісяця",
|
||||
"fromCurrentDate": "З дня закінчення",
|
||||
|
|
@ -1466,32 +1459,6 @@
|
|||
"frontendVersion": "Версія інтерфейсу: {version}",
|
||||
"apiVersion": "API версія: {version}"
|
||||
},
|
||||
"timeTracking": {
|
||||
"title": "Відстеження часу",
|
||||
"stop": "Зупинити таймер",
|
||||
"logTime": "Записати час",
|
||||
"editEntry": "Редагувати запис",
|
||||
"form": {
|
||||
"task": "Завдання",
|
||||
"taskSearch": "Знайти завдання…",
|
||||
"commentPlaceholder": "Над чим ви працювали?",
|
||||
"save": "Зберегти запис",
|
||||
"startTimer": "Запустити таймер",
|
||||
"update": "Оновити запис",
|
||||
"smartFill": "Заповнити з останнього запису"
|
||||
},
|
||||
"list": {
|
||||
"emptyTask": "Для цього завдання ще немає записів обліку часу.",
|
||||
"emptyFiltered": "Немає записів обліку часу для вибраних фільтрів.",
|
||||
"total": "Загалом",
|
||||
"time": "Час",
|
||||
"duration": "Тривалість"
|
||||
},
|
||||
"browse": {
|
||||
"selectRange": "Обрати діапазон",
|
||||
"userSearch": "Знайти користувача…"
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
"units": {
|
||||
"seconds": "секунда|секунд(и)",
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ export const DAYJS_LOCALE_MAPPING = {
|
|||
'ja-jp': 'ja',
|
||||
'hu-hu': 'hu',
|
||||
'ar-sa': 'ar-sa',
|
||||
'fa-ir': 'fa',
|
||||
'sl-si': 'sl',
|
||||
'pt-br': 'pt',
|
||||
'hr-hr': 'hr',
|
||||
|
|
@ -56,7 +55,6 @@ export const DAYJS_LANGUAGE_IMPORTS = {
|
|||
'ja-jp': () => import('dayjs/locale/ja'),
|
||||
'hu-hu': () => import('dayjs/locale/hu'),
|
||||
'ar-sa': () => import('dayjs/locale/ar-sa'),
|
||||
'fa-ir': () => import('dayjs/locale/fa'),
|
||||
'sl-si': () => import('dayjs/locale/sl'),
|
||||
'pt-br': () => import('dayjs/locale/pt-br'),
|
||||
'hr-hr': () => import('dayjs/locale/hr'),
|
||||
|
|
|
|||
|
|
@ -5,5 +5,4 @@ export interface IProjectDuplicate extends IAbstract {
|
|||
projectId: number
|
||||
duplicatedProject: IProject | null
|
||||
parentProjectId: IProject['id']
|
||||
duplicateShares: boolean
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ export default class ProjectDuplicateModel extends AbstractModel<IProjectDuplica
|
|||
projectId = 0
|
||||
duplicatedProject: IProject | null = null
|
||||
parentProjectId = 0
|
||||
duplicateShares = false
|
||||
|
||||
constructor(data : Partial<IProjectDuplicate>) {
|
||||
super()
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import {getProjectViewId} from '@/helpers/projectView'
|
|||
import {parseDateOrString} from '@/helpers/time/parseDateOrString'
|
||||
import {getNextWeekDate} from '@/helpers/time/getNextWeekDate'
|
||||
import {LINK_SHARE_HASH_PREFIX} from '@/constants/linkShareHash'
|
||||
import {REDIRECT_HASH_PREFIX} from '@/constants/redirectHash'
|
||||
import {AUTH_ROUTE_NAMES} from '@/constants/authRouteNames'
|
||||
import {PRO_FEATURE} from '@/constants/proFeatures'
|
||||
|
||||
|
|
@ -31,7 +30,7 @@ const router = createRouter({
|
|||
}
|
||||
|
||||
// Scroll to anchor should still work
|
||||
if (to.hash && !to.hash.startsWith(LINK_SHARE_HASH_PREFIX) && !to.hash.startsWith(REDIRECT_HASH_PREFIX)) {
|
||||
if (to.hash && !to.hash.startsWith(LINK_SHARE_HASH_PREFIX)) {
|
||||
return {el: to.hash}
|
||||
}
|
||||
|
||||
|
|
@ -473,22 +472,10 @@ const router = createRouter({
|
|||
})
|
||||
|
||||
export async function getAuthForRoute(to: RouteLocation, authStore) {
|
||||
// vue-router already decoded to.hash once, so slicing off the prefix yields the original
|
||||
// fullPath (e.g. /oauth/authorize?...) losslessly — no extra decodeURIComponent needed.
|
||||
const redirectDest = to.name === 'user.login' && to.hash.startsWith(REDIRECT_HASH_PREFIX)
|
||||
? to.hash.slice(REDIRECT_HASH_PREFIX.length)
|
||||
: ''
|
||||
|
||||
if (authStore.authUser || authStore.authLinkShare) {
|
||||
// An already-signed-in browser that opens a copied /login#redirect=<oauth.authorize> URL
|
||||
// must run the OAuth flow with its existing session instead of short-circuiting to home.
|
||||
// The destination has no redirect hash, so the second guard pass just early-returns (#2654).
|
||||
if (redirectDest) {
|
||||
return redirectDest
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Check if password reset token is in query params
|
||||
const resetToken = to.query.userPasswordReset as string | undefined
|
||||
|
||||
|
|
@ -512,35 +499,15 @@ export async function getAuthForRoute(to: RouteLocation, authStore) {
|
|||
}
|
||||
}
|
||||
|
||||
// Keep the destination in the address bar (not just per-browser localStorage) so a native
|
||||
// client's /oauth/authorize URL stays copyable into another browser. Hash, not query, so the
|
||||
// embedded OAuth params never reach access logs (#2654). Pass fullPath raw: vue-router encodes
|
||||
// the hash itself, so an extra encodeURIComponent here would be double-encoded in the URL.
|
||||
if (to.name === 'oauth.authorize') {
|
||||
return {
|
||||
name: 'user.login',
|
||||
hash: REDIRECT_HASH_PREFIX + to.fullPath,
|
||||
}
|
||||
}
|
||||
|
||||
// Fold the hash destination into localStorage: it's the only bridge that survives the
|
||||
// external OIDC round-trip out of the SPA, so redirectIfSaved() works after any auth method.
|
||||
// vue-router already decoded to.hash once, so it equals the fullPath we wrote above as-is.
|
||||
if (to.hash.startsWith(REDIRECT_HASH_PREFIX)) {
|
||||
const destination = to.hash.slice(REDIRECT_HASH_PREFIX.length)
|
||||
const resolved = router.resolve(destination)
|
||||
saveLastVisited(resolved.name as string, resolved.params, resolved.query)
|
||||
}
|
||||
|
||||
// Check if the route the user wants to go to is a route which needs authentication. We use this to
|
||||
// redirect the user after successful login.
|
||||
const isValidUserAppRoute = !AUTH_ROUTE_NAMES.has(to.name as string) &&
|
||||
localStorage.getItem('emailConfirmToken') === null
|
||||
|
||||
|
||||
if (isValidUserAppRoute) {
|
||||
saveLastVisited(to.name as string, to.params, to.query)
|
||||
}
|
||||
|
||||
|
||||
if (isValidUserAppRoute) {
|
||||
return {name: 'user.login'}
|
||||
}
|
||||
|
|
@ -598,25 +565,12 @@ router.beforeEach(async (to, from) => {
|
|||
|
||||
const newRoute = await getAuthForRoute(to, authStore)
|
||||
if(newRoute) {
|
||||
// A string target (the decoded redirect destination for an authed browser) already
|
||||
// carries its own query/path and no redirect hash, so navigate to it verbatim — don't
|
||||
// re-attach to.hash or it would re-enter the redirect loop.
|
||||
if (typeof newRoute === 'string') {
|
||||
return newRoute
|
||||
}
|
||||
return {
|
||||
hash: to.hash,
|
||||
...newRoute,
|
||||
hash: to.hash,
|
||||
}
|
||||
}
|
||||
|
||||
// to.fullPath keeps the redirect hash url-encoded while to.hash is decoded, so the endsWith
|
||||
// check below never matches and would re-append the hash forever. The hash is already on the
|
||||
// URL here, so skip the re-attach (#2654).
|
||||
if (to.hash.startsWith(REDIRECT_HASH_PREFIX)) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if(!to.fullPath.endsWith(to.hash)) {
|
||||
return to.fullPath + to.hash
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,139 +0,0 @@
|
|||
import {describe, it, expect, beforeEach, vi} from 'vitest'
|
||||
import {setActivePinia, createPinia} from 'pinia'
|
||||
|
||||
import {useAuthStore} from './auth'
|
||||
import {AUTH_TYPES} from '@/modelTypes/IUser'
|
||||
|
||||
const {refreshTokenMock, routerPushMock, getTokenMock} = vi.hoisted(() => ({
|
||||
refreshTokenMock: vi.fn(),
|
||||
routerPushMock: vi.fn(),
|
||||
getTokenMock: vi.fn(() => null as string | null),
|
||||
}))
|
||||
|
||||
vi.mock('@/helpers/auth', () => ({
|
||||
refreshToken: refreshTokenMock,
|
||||
getToken: getTokenMock,
|
||||
saveToken: vi.fn(),
|
||||
removeToken: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/router', () => ({
|
||||
default: {push: routerPushMock},
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useWebSocket', () => ({
|
||||
useWebSocket: () => ({disconnect: vi.fn(), connect: vi.fn()}),
|
||||
}))
|
||||
|
||||
function fakeHttp() {
|
||||
return {
|
||||
post: vi.fn().mockResolvedValue({data: {}}),
|
||||
get: vi.fn().mockResolvedValue({data: {}}),
|
||||
request: vi.fn().mockResolvedValue({data: {}}),
|
||||
interceptors: {
|
||||
request: {use: vi.fn()},
|
||||
response: {use: vi.fn()},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
vi.mock('@/helpers/fetcher', () => ({
|
||||
HTTPFactory: () => fakeHttp(),
|
||||
AuthenticatedHTTPFactory: () => fakeHttp(),
|
||||
getApiBaseUrl: () => 'http://localhost/api/v1/',
|
||||
}))
|
||||
|
||||
vi.mock('@/helpers/redirectToProvider', () => ({
|
||||
getRedirectUrlFromCurrentFrontendPath: vi.fn(),
|
||||
redirectToProvider: vi.fn(),
|
||||
redirectToProviderOnLogout: vi.fn(),
|
||||
}))
|
||||
|
||||
// A refresh failure that looks like a real network/HTTP error so renewToken's
|
||||
// "is this a genuine logout?" check (it inspects the error cause's status) fires.
|
||||
function refreshError() {
|
||||
return new Error('Error renewing token: ', {
|
||||
cause: {response: {status: 401}},
|
||||
})
|
||||
}
|
||||
|
||||
// A JWT carrying a not-yet-expired user session, so the checkAuth() call that
|
||||
// renewToken() runs after a successful refresh treats the session as live.
|
||||
function freshUserJwt() {
|
||||
const payload = {
|
||||
id: 1,
|
||||
type: AUTH_TYPES.USER,
|
||||
exp: Math.floor(Date.now() / 1000) + 3600,
|
||||
}
|
||||
const encoded = btoa(JSON.stringify(payload))
|
||||
return `header.${encoded}.signature`
|
||||
}
|
||||
|
||||
describe('auth store renewToken retry (issue #2863)', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
refreshTokenMock.mockReset()
|
||||
routerPushMock.mockReset()
|
||||
getTokenMock.mockReset().mockReturnValue(null)
|
||||
})
|
||||
|
||||
function setupExpiredUserSession(store: ReturnType<typeof useAuthStore>) {
|
||||
store.setAuthenticated(true)
|
||||
// Expired exp so renewToken treats a refresh failure as a real logout.
|
||||
store.setUser({
|
||||
id: 1,
|
||||
type: AUTH_TYPES.USER,
|
||||
exp: Math.floor(Date.now() / 1000) - 60,
|
||||
} as never, false)
|
||||
}
|
||||
|
||||
it('does NOT log out when the first refresh fails but the retry succeeds', async () => {
|
||||
const store = useAuthStore()
|
||||
setupExpiredUserSession(store)
|
||||
|
||||
// The retry "succeeds" only if it actually leaves a usable token behind:
|
||||
// renewToken() runs checkAuth() afterwards, which reads getToken(). Start
|
||||
// with no token, then hand back a fresh JWT once the refresh resolves.
|
||||
getTokenMock.mockReturnValue(null)
|
||||
refreshTokenMock
|
||||
.mockRejectedValueOnce(refreshError())
|
||||
.mockImplementationOnce(async () => {
|
||||
getTokenMock.mockReturnValue(freshUserJwt())
|
||||
})
|
||||
|
||||
await store.renewToken()
|
||||
|
||||
// Two refresh attempts: the initial one and the single retry.
|
||||
expect(refreshTokenMock).toHaveBeenCalledTimes(2)
|
||||
// The retry recovered the session: the user is still authenticated...
|
||||
expect(store.authenticated).toBe(true)
|
||||
// ...and was not bounced to login.
|
||||
expect(routerPushMock).not.toHaveBeenCalledWith({name: 'user.login'})
|
||||
})
|
||||
|
||||
it('logs out when BOTH the refresh and its retry fail', async () => {
|
||||
const store = useAuthStore()
|
||||
setupExpiredUserSession(store)
|
||||
|
||||
refreshTokenMock
|
||||
.mockRejectedValueOnce(refreshError())
|
||||
.mockRejectedValueOnce(refreshError())
|
||||
|
||||
await store.renewToken()
|
||||
|
||||
expect(refreshTokenMock).toHaveBeenCalledTimes(2)
|
||||
expect(routerPushMock).toHaveBeenCalledWith({name: 'user.login'})
|
||||
})
|
||||
|
||||
it('retries exactly once (no infinite loop) when the session is genuinely dead', async () => {
|
||||
const store = useAuthStore()
|
||||
setupExpiredUserSession(store)
|
||||
|
||||
refreshTokenMock.mockRejectedValue(refreshError())
|
||||
|
||||
await store.renewToken()
|
||||
|
||||
// Initial attempt + exactly one retry — never more.
|
||||
expect(refreshTokenMock).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
|
|
@ -28,11 +28,6 @@ import {TIME_FORMAT} from '@/constants/timeFormat'
|
|||
import {RELATION_KIND} from '@/types/IRelationKind'
|
||||
import type {IProvider} from '@/types/IProvider'
|
||||
|
||||
// Set on explicit logout so the login page won't immediately bounce the user
|
||||
// back to the OIDC provider. Lives in sessionStorage so it survives the
|
||||
// round-trip to the IdP within the tab and isn't wiped by localStorage.clear().
|
||||
export const JUST_LOGGED_OUT_KEY = 'justLoggedOut'
|
||||
|
||||
function redirectToSpecifiedProvider() {
|
||||
|
||||
const {auth} = useConfigStore()
|
||||
|
|
@ -60,17 +55,6 @@ function redirectToSpecifiedProvider() {
|
|||
}
|
||||
}
|
||||
|
||||
// A race-loser's refresh fails but the rotated cookie is already valid, so a
|
||||
// second attempt succeeds — recovering what would otherwise be a spurious
|
||||
// logout. Exactly one retry: a genuinely dead session still logs out, no loop.
|
||||
async function refreshTokenWithRetry(persist: boolean): Promise<void> {
|
||||
try {
|
||||
await refreshToken(persist)
|
||||
} catch {
|
||||
await refreshToken(persist)
|
||||
}
|
||||
}
|
||||
|
||||
function getLoggedInVia(): string | null {
|
||||
return localStorage.getItem('loggedInViaProvider')
|
||||
}
|
||||
|
|
@ -368,7 +352,7 @@ export const useAuthStore = defineStore('auth', () => {
|
|||
// refresh before giving up. This lets users who reopen the app
|
||||
// after the short JWT TTL seamlessly resume their session.
|
||||
try {
|
||||
await refreshTokenWithRetry(true)
|
||||
await refreshToken(true)
|
||||
const freshJwt = getToken()
|
||||
if (freshJwt) {
|
||||
const b64 = freshJwt.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')
|
||||
|
|
@ -528,7 +512,7 @@ export const useAuthStore = defineStore('auth', () => {
|
|||
saveToken(response.data.token, false)
|
||||
} else {
|
||||
// User sessions renew via the refresh-token cookie.
|
||||
await refreshTokenWithRetry(true)
|
||||
await refreshToken(true)
|
||||
}
|
||||
await checkAuth()
|
||||
} catch (e) {
|
||||
|
|
@ -549,11 +533,9 @@ export const useAuthStore = defineStore('auth', () => {
|
|||
|
||||
// Revoke the server session so the refresh token can't be reused.
|
||||
// Best-effort: if the network call fails, still clean up locally.
|
||||
let oidcLogoutUrl = ''
|
||||
try {
|
||||
const HTTP = AuthenticatedHTTPFactory()
|
||||
const {data} = await HTTP.post('user/logout')
|
||||
oidcLogoutUrl = data?.oidc_logout_url ?? ''
|
||||
await HTTP.post('user/logout')
|
||||
} catch (_e) {
|
||||
// Ignore — session will expire naturally
|
||||
}
|
||||
|
|
@ -562,25 +544,14 @@ export const useAuthStore = defineStore('auth', () => {
|
|||
const loggedInVia = getLoggedInVia()
|
||||
window.localStorage.clear() // Clear all settings and history we might have saved in local storage.
|
||||
lastUserInfoRefresh.value = null
|
||||
|
||||
sessionStorage.setItem(JUST_LOGGED_OUT_KEY, 'true')
|
||||
|
||||
// Redirect to the OIDC provider to end its session too. Prefer the
|
||||
// server-built RP-Initiated Logout URL, falling back to the static one.
|
||||
// These full-page redirects return the user to the login page, so we
|
||||
// must not router.push there first — that would consume
|
||||
// JUST_LOGGED_OUT_KEY before the round-trip lands.
|
||||
if (oidcLogoutUrl) {
|
||||
window.location.href = oidcLogoutUrl
|
||||
return
|
||||
}
|
||||
const fullProvider: IProvider|undefined = configStore.auth.openidConnect.providers?.find((p: IProvider) => p.key === loggedInVia)
|
||||
if (fullProvider && redirectToProviderOnLogout(fullProvider)) {
|
||||
return
|
||||
}
|
||||
|
||||
await router.push({name: 'user.login'})
|
||||
await checkAuth()
|
||||
|
||||
// if configured, redirect to OIDC Provider on logout
|
||||
const fullProvider: IProvider|undefined = configStore.auth.openidConnect.providers?.find((p: IProvider) => p.key === loggedInVia)
|
||||
if (fullProvider) {
|
||||
redirectToProviderOnLogout(fullProvider)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ export interface ConfigState {
|
|||
publicTeamsEnabled: boolean,
|
||||
allowIconChanges: boolean,
|
||||
enabledProFeatures: string[],
|
||||
concurrentWrites: boolean,
|
||||
}
|
||||
|
||||
export const useConfigStore = defineStore('config', () => {
|
||||
|
|
@ -89,7 +88,6 @@ export const useConfigStore = defineStore('config', () => {
|
|||
publicTeamsEnabled: false,
|
||||
allowIconChanges: true,
|
||||
enabledProFeatures: [],
|
||||
concurrentWrites: false,
|
||||
})
|
||||
|
||||
const migratorsEnabled = computed(() => state.availableMigrators?.length > 0)
|
||||
|
|
|
|||
|
|
@ -380,11 +380,10 @@ export function useProject(projectId: MaybeRefOrGetter<IProject['id']>) {
|
|||
success({message: t('project.edit.success')})
|
||||
}
|
||||
|
||||
async function duplicateProject(parentProjectId: IProject['id'], duplicateShares: boolean = false) {
|
||||
async function duplicateProject(parentProjectId: IProject['id']) {
|
||||
const projectDuplicate = new ProjectDuplicateModel({
|
||||
projectId: Number(toValue(projectId)),
|
||||
parentProjectId,
|
||||
duplicateShares,
|
||||
})
|
||||
|
||||
const duplicate = await projectDuplicateService.create(projectDuplicate)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import {describe, expect, it} from 'vitest'
|
||||
import {buildDefaultRemindersForQuickAdd, runWrites} from './tasks'
|
||||
import {buildDefaultRemindersForQuickAdd} from './tasks'
|
||||
import {REMINDER_PERIOD_RELATIVE_TO_TYPES} from '@/types/IReminderPeriodRelativeTo'
|
||||
import type {ITaskReminder} from '@/modelTypes/ITaskReminder'
|
||||
|
||||
|
|
@ -42,39 +42,3 @@ describe('buildDefaultRemindersForQuickAdd', () => {
|
|||
expect(result[0].relativeTo).toBe(REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE)
|
||||
})
|
||||
})
|
||||
|
||||
describe('runWrites', () => {
|
||||
function deferredWrite() {
|
||||
const inFlight: string[] = []
|
||||
let maxConcurrent = 0
|
||||
const completed: string[] = []
|
||||
const write = async (item: string) => {
|
||||
inFlight.push(item)
|
||||
maxConcurrent = Math.max(maxConcurrent, inFlight.length)
|
||||
await Promise.resolve()
|
||||
inFlight.splice(inFlight.indexOf(item), 1)
|
||||
completed.push(item)
|
||||
}
|
||||
return {write, completed, getMaxConcurrent: () => maxConcurrent}
|
||||
}
|
||||
|
||||
it('runs all writes in parallel when concurrent', async () => {
|
||||
const {write, completed, getMaxConcurrent} = deferredWrite()
|
||||
await runWrites(['a', 'b', 'c'], write, true)
|
||||
expect(completed).toHaveLength(3)
|
||||
expect(getMaxConcurrent()).toBeGreaterThan(1)
|
||||
})
|
||||
|
||||
it('runs writes one at a time when not concurrent', async () => {
|
||||
const {write, completed, getMaxConcurrent} = deferredWrite()
|
||||
await runWrites(['a', 'b', 'c'], write, false)
|
||||
expect(completed).toEqual(['a', 'b', 'c'])
|
||||
expect(getMaxConcurrent()).toBe(1)
|
||||
})
|
||||
|
||||
it('does nothing for an empty list', async () => {
|
||||
const {write, completed} = deferredWrite()
|
||||
await runWrites([], write, false)
|
||||
expect(completed).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ import type {IProject} from '@/modelTypes/IProject'
|
|||
import {REMINDER_PERIOD_RELATIVE_TO_TYPES} from '@/types/IReminderPeriodRelativeTo'
|
||||
|
||||
import {setModuleLoading} from '@/stores/helper'
|
||||
import {useConfigStore} from '@/stores/config'
|
||||
import {useLabelStore} from '@/stores/labels'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useKanbanStore} from '@/stores/kanban'
|
||||
|
|
@ -60,22 +59,6 @@ export function buildDefaultRemindersForQuickAdd(
|
|||
}))
|
||||
}
|
||||
|
||||
// runWrites applies a write to each item. SQLite deadlocks on concurrent writes
|
||||
// (read-then-write upgrade conflict), so callers pass concurrent=false to serialize.
|
||||
export async function runWrites<T>(
|
||||
items: readonly T[],
|
||||
write: (item: T) => Promise<unknown>,
|
||||
concurrent: boolean,
|
||||
): Promise<void> {
|
||||
if (concurrent) {
|
||||
await Promise.all(items.map(item => write(item)))
|
||||
return
|
||||
}
|
||||
for (const item of items) {
|
||||
await write(item)
|
||||
}
|
||||
}
|
||||
|
||||
// IDEA: maybe use a small fuzzy search here to prevent errors
|
||||
function findPropertyByValue(object, key, value, fuzzy = false) {
|
||||
return Object.values(object).find(l => {
|
||||
|
|
@ -148,7 +131,6 @@ export const useTaskStore = defineStore('task', () => {
|
|||
const labelStore = useLabelStore()
|
||||
const projectStore = useProjectStore()
|
||||
const authStore = useAuthStore()
|
||||
const configStore = useConfigStore()
|
||||
|
||||
const tasks = ref<{ [id: ITask['id']]: ITask }>({}) // TODO: or is this ITask[]
|
||||
const isLoading = ref(false)
|
||||
|
|
@ -413,7 +395,10 @@ export const useTaskStore = defineStore('task', () => {
|
|||
}
|
||||
|
||||
const labels = await ensureLabelsExist(parsedLabels)
|
||||
await runWrites(labels, l => addLabelToTask(task, l), configStore.concurrentWrites)
|
||||
const labelAddsToWaitFor = labels.map(async l => addLabelToTask(task, l))
|
||||
|
||||
// This waits until all labels are created and added to the task
|
||||
await Promise.all(labelAddsToWaitFor)
|
||||
return task
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,10 +4,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.is-pulled-end {
|
||||
.is-pulled-right {
|
||||
float: right !important;
|
||||
}
|
||||
|
||||
[dir="rtl"] .is-pulled-end {
|
||||
float: left !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
>
|
||||
<XButton
|
||||
:to="{name:'labels.create'}"
|
||||
class="is-pulled-end"
|
||||
class="is-pulled-right"
|
||||
icon="plus"
|
||||
>
|
||||
{{ $t('label.create.header') }}
|
||||
|
|
|
|||
|
|
@ -8,12 +8,6 @@
|
|||
>
|
||||
<p>{{ $t('project.duplicate.text') }}</p>
|
||||
<ProjectSearch v-model="parentProject" />
|
||||
<FancyCheckbox
|
||||
v-model="duplicateShares"
|
||||
class="mbs-2"
|
||||
>
|
||||
{{ $t('project.duplicate.shares') }}
|
||||
</FancyCheckbox>
|
||||
</CreateEdit>
|
||||
</template>
|
||||
|
||||
|
|
@ -24,7 +18,6 @@ import {useI18n} from 'vue-i18n'
|
|||
|
||||
import CreateEdit from '@/components/misc/CreateEdit.vue'
|
||||
import ProjectSearch from '@/components/tasks/partials/ProjectSearch.vue'
|
||||
import FancyCheckbox from '@/components/input/FancyCheckbox.vue'
|
||||
|
||||
import {success} from '@/message'
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
|
|
@ -40,7 +33,6 @@ const projectStore = useProjectStore()
|
|||
const {project, isLoading, duplicateProject} = useProject(route.params.projectId)
|
||||
|
||||
const parentProject = ref<IProject | null>(null)
|
||||
const duplicateShares = ref(true)
|
||||
const isDuplicating = ref(false)
|
||||
|
||||
const loadingModel = computed({
|
||||
|
|
@ -61,7 +53,7 @@ async function duplicate() {
|
|||
isDuplicating.value = true
|
||||
|
||||
try {
|
||||
await duplicateProject(parentProject.value?.id ?? 0, duplicateShares.value)
|
||||
await duplicateProject(parentProject.value?.id ?? 0)
|
||||
success({message: t('project.duplicate.success')})
|
||||
} finally {
|
||||
isDuplicating.value = false
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
>
|
||||
<XButton
|
||||
:to="{name:'teams.create'}"
|
||||
class="is-pulled-end"
|
||||
class="is-pulled-right"
|
||||
icon="plus"
|
||||
>
|
||||
{{ $t('team.create.title') }}
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ import {redirectToProvider} from '@/helpers/redirectToProvider'
|
|||
import {useRedirectToLastVisited} from '@/composables/useRedirectToLastVisited'
|
||||
import {isDesktopApp} from '@/helpers/desktopAuth'
|
||||
|
||||
import {useAuthStore, JUST_LOGGED_OUT_KEY} from '@/stores/auth'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useConfigStore} from '@/stores/config'
|
||||
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
|
|
@ -181,25 +181,6 @@ onBeforeMount(() => {
|
|||
// route before the submit() handler gets a chance to use it.
|
||||
if (authenticated.value) {
|
||||
router.push({name: 'home'})
|
||||
return
|
||||
}
|
||||
|
||||
// Don't auto-redirect right after an explicit logout, otherwise we'd
|
||||
// immediately re-authenticate the user we just logged out.
|
||||
if (sessionStorage.getItem(JUST_LOGGED_OUT_KEY)) {
|
||||
sessionStorage.removeItem(JUST_LOGGED_OUT_KEY)
|
||||
return
|
||||
}
|
||||
|
||||
// When the login page offers nothing but a single OIDC provider, skip it
|
||||
// and send the user straight there.
|
||||
if (
|
||||
!localAuthEnabled.value &&
|
||||
!ldapAuthEnabled.value &&
|
||||
hasOpenIdProviders.value &&
|
||||
openidConnect.value.providers.length === 1
|
||||
) {
|
||||
redirectToProvider(openidConnect.value.providers[0])
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,124 +0,0 @@
|
|||
import {test, expect} from '../../support/fixtures'
|
||||
import {ProjectFactory} from '../../factories/project'
|
||||
import {ProjectViewFactory} from '../../factories/project_view'
|
||||
import {BucketFactory} from '../../factories/bucket'
|
||||
import {TaskFactory} from '../../factories/task'
|
||||
import {TaskBucketFactory} from '../../factories/task_buckets'
|
||||
|
||||
// Regression test for #2940: in the Kanban task popup the description editor is
|
||||
// rendered inside a native <dialog> opened via showModal() (browser top-layer).
|
||||
// The link prompt used to be appended to document.body, so it was painted behind
|
||||
// the dialog and unfocusable through its focus trap, making "set link" a no-op.
|
||||
test.describe('Editor link prompt inside the Kanban task popup', () => {
|
||||
test('creates a link in the description when opened as the Kanban popup', async ({authenticatedPage: page}) => {
|
||||
const projects = await ProjectFactory.create(1)
|
||||
const views = await ProjectViewFactory.create(1, {
|
||||
id: 1,
|
||||
project_id: projects[0].id,
|
||||
view_kind: 3,
|
||||
bucket_configuration_mode: 1,
|
||||
})
|
||||
const buckets = await BucketFactory.create(1, {
|
||||
project_view_id: views[0].id,
|
||||
})
|
||||
const tasks = await TaskFactory.create(1, {
|
||||
project_id: projects[0].id,
|
||||
description: 'link me',
|
||||
index: 1,
|
||||
})
|
||||
await TaskBucketFactory.create(1, {
|
||||
task_id: tasks[0].id,
|
||||
bucket_id: buckets[0].id,
|
||||
project_view_id: views[0].id,
|
||||
})
|
||||
|
||||
await page.goto(`/projects/${projects[0].id}/${views[0].id}`)
|
||||
|
||||
const card = page.locator('.kanban .bucket .tasks .task').filter({hasText: tasks[0].title})
|
||||
await expect(card).toBeVisible()
|
||||
await card.click()
|
||||
|
||||
// The task popup must be a native <dialog> in the top layer.
|
||||
const dialog = page.locator('dialog[open]')
|
||||
await expect(dialog).toBeVisible()
|
||||
await expect(dialog.locator('.task-view')).toBeVisible()
|
||||
|
||||
const editButton = dialog.locator('.details.content.description .tiptap button.done-edit').filter({hasText: 'Edit'})
|
||||
await expect(editButton).toBeVisible({timeout: 10000})
|
||||
await editButton.click()
|
||||
|
||||
const description = dialog.locator('.details.content.description')
|
||||
const editor = description.locator('[contenteditable="true"]').first()
|
||||
await expect(editor).toBeVisible({timeout: 10000})
|
||||
await editor.click()
|
||||
await page.keyboard.press('ControlOrMeta+a')
|
||||
|
||||
await description.locator('.editor-toolbar__button').filter({hasText: 'Link'}).click()
|
||||
|
||||
const urlInput = dialog.locator('input.input[placeholder="URL"]')
|
||||
await expect(urlInput).toBeVisible()
|
||||
await urlInput.fill('https://vikunja.io')
|
||||
await urlInput.press('Enter')
|
||||
|
||||
const link = editor.locator('a[href="https://vikunja.io"]')
|
||||
await expect(link).toBeVisible()
|
||||
await expect(link).toHaveText('link me')
|
||||
})
|
||||
|
||||
// The link prompt is a sub-modal of the task <dialog>: pressing Escape while
|
||||
// it is open must cancel only the prompt and leave the task dialog open,
|
||||
// instead of falling through to the native <dialog>'s Escape-to-close.
|
||||
test('Escape cancels the link prompt without closing the task dialog', async ({authenticatedPage: page}) => {
|
||||
const projects = await ProjectFactory.create(1)
|
||||
const views = await ProjectViewFactory.create(1, {
|
||||
id: 1,
|
||||
project_id: projects[0].id,
|
||||
view_kind: 3,
|
||||
bucket_configuration_mode: 1,
|
||||
})
|
||||
const buckets = await BucketFactory.create(1, {
|
||||
project_view_id: views[0].id,
|
||||
})
|
||||
const tasks = await TaskFactory.create(1, {
|
||||
project_id: projects[0].id,
|
||||
description: 'link me',
|
||||
index: 1,
|
||||
})
|
||||
await TaskBucketFactory.create(1, {
|
||||
task_id: tasks[0].id,
|
||||
bucket_id: buckets[0].id,
|
||||
project_view_id: views[0].id,
|
||||
})
|
||||
|
||||
await page.goto(`/projects/${projects[0].id}/${views[0].id}`)
|
||||
|
||||
const card = page.locator('.kanban .bucket .tasks .task').filter({hasText: tasks[0].title})
|
||||
await expect(card).toBeVisible()
|
||||
await card.click()
|
||||
|
||||
const dialog = page.locator('dialog[open]')
|
||||
await expect(dialog).toBeVisible()
|
||||
await expect(dialog.locator('.task-view')).toBeVisible()
|
||||
|
||||
const editButton = dialog.locator('.details.content.description .tiptap button.done-edit').filter({hasText: 'Edit'})
|
||||
await expect(editButton).toBeVisible({timeout: 10000})
|
||||
await editButton.click()
|
||||
|
||||
const description = dialog.locator('.details.content.description')
|
||||
const editor = description.locator('[contenteditable="true"]').first()
|
||||
await expect(editor).toBeVisible({timeout: 10000})
|
||||
await editor.click()
|
||||
await page.keyboard.press('ControlOrMeta+a')
|
||||
|
||||
await description.locator('.editor-toolbar__button').filter({hasText: 'Link'}).click()
|
||||
|
||||
const urlInput = dialog.locator('input.input[placeholder="URL"]')
|
||||
await expect(urlInput).toBeVisible()
|
||||
await urlInput.press('Escape')
|
||||
|
||||
// The prompt is gone, but the task dialog stays open.
|
||||
await expect(urlInput).toBeHidden()
|
||||
await expect(dialog).toBeVisible()
|
||||
await expect(dialog.locator('.task-view')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
import {type Page} from '@playwright/test'
|
||||
import {test, expect} from '../../support/fixtures'
|
||||
import {TaskFactory} from '../../factories/task'
|
||||
import {createProjects} from './prepareProjects'
|
||||
|
||||
async function selectSortInList(page: Page, optionLabel: string) {
|
||||
await page.locator('.filter-container').getByRole('button', {name: 'Sort', exact: true}).click()
|
||||
await page.getByLabel('Sort by').selectOption({label: optionLabel})
|
||||
await page.getByRole('button', {name: 'Apply sort'}).click()
|
||||
}
|
||||
|
||||
async function navigateViaSidebar(page: Page, projectTitle: string) {
|
||||
await page.locator('.menu-list .list-menu-link', {
|
||||
has: page.locator('.project-menu-title', {hasText: new RegExp(`^${projectTitle}$`)}),
|
||||
}).first().click()
|
||||
}
|
||||
|
||||
test.describe('Sort persistence across sidebar navigation (#2753)', () => {
|
||||
test('List view: sort persists after navigating to another project and back', async ({authenticatedPage: page}) => {
|
||||
const projects = await createProjects(2)
|
||||
const [projectA, projectB] = projects
|
||||
await TaskFactory.create(3, {
|
||||
id: '{increment}',
|
||||
project_id: projectA.id,
|
||||
title: 'Task {increment}',
|
||||
})
|
||||
|
||||
const listViewA = projectA.views[0].id
|
||||
await page.goto(`/projects/${projectA.id}/${listViewA}`)
|
||||
await expect(page).not.toHaveURL(/sort=/)
|
||||
|
||||
await selectSortInList(page, 'Due date (Earliest first)')
|
||||
await expect(page).toHaveURL(/sort=due_date:asc/)
|
||||
|
||||
await navigateViaSidebar(page, projectB.title)
|
||||
await expect(page).toHaveURL(new RegExp(`/projects/${projectB.id}/`))
|
||||
|
||||
await navigateViaSidebar(page, projectA.title)
|
||||
await expect(page).toHaveURL(new RegExp(`/projects/${projectA.id}/`))
|
||||
await expect(page).toHaveURL(/sort=due_date:asc/)
|
||||
})
|
||||
|
||||
test('List view: explicit URL sort wins over stored sort', async ({authenticatedPage: page}) => {
|
||||
const projects = await createProjects(1)
|
||||
const listView = projects[0].views[0].id
|
||||
|
||||
// Seed the store with one sort by visiting with it set.
|
||||
await page.goto(`/projects/${projects[0].id}/${listView}?sort=due_date:asc`)
|
||||
await expect(page).toHaveURL(/sort=due_date:asc/)
|
||||
|
||||
// Visit a URL that explicitly sets a different sort — that should win.
|
||||
await page.goto(`/projects/${projects[0].id}/${listView}?sort=priority:desc`)
|
||||
await expect(page).toHaveURL(/sort=priority:desc/)
|
||||
})
|
||||
})
|
||||
|
|
@ -32,20 +32,10 @@ test.describe('OAuth 2.0 Authorization Flow', () => {
|
|||
})
|
||||
|
||||
// Navigate to the OAuth authorize frontend route.
|
||||
// The user is not logged in, so the router guard redirects to /login while
|
||||
// carrying the authorize destination in a copyable #redirect= hash (not a
|
||||
// query param, to keep the OAuth params out of access logs).
|
||||
// The user is not logged in, so the router guard saves the route
|
||||
// and redirects to /login.
|
||||
await page.goto(`/oauth/authorize?${authorizeParams}`)
|
||||
await expect(page).toHaveURL(/\/login#redirect=/)
|
||||
|
||||
// The decoded #redirect= destination must carry the full authorize URL, including the
|
||||
// OAuth params — checking only for the path would pass even if the query were dropped.
|
||||
const redirectHash = decodeURIComponent(new URL(page.url()).hash)
|
||||
expect(redirectHash).toContain('/oauth/authorize')
|
||||
expect(redirectHash).toContain('response_type=code')
|
||||
expect(redirectHash).toContain('client_id=vikunja')
|
||||
expect(redirectHash).toContain(`code_challenge=${codeChallenge}`)
|
||||
expect(redirectHash).toContain(`state=${state}`)
|
||||
await expect(page).toHaveURL(/\/login/)
|
||||
|
||||
// Register the response listener BEFORE clicking Login, because after
|
||||
// login redirectIfSaved() navigates back to /oauth/authorize and the
|
||||
|
|
@ -87,70 +77,4 @@ test.describe('OAuth 2.0 Authorization Flow', () => {
|
|||
expect(tokenBody.token_type).toBe('bearer')
|
||||
expect(tokenBody.expires_in).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
// The primary #2654 scenario: the native client opened a different default browser that is
|
||||
// already signed in to Vikunja. Opening the copied /login#redirect=<oauth.authorize> URL must
|
||||
// run the OAuth flow with the existing session instead of short-circuiting to home.
|
||||
test('Already-authenticated browser opening the copied login redirect runs the authorize flow', async ({authenticatedPage, apiContext, currentUser}) => {
|
||||
const page = authenticatedPage
|
||||
|
||||
const codeVerifier = randomBytes(32).toString('base64url')
|
||||
const codeChallenge = createHash('sha256').update(codeVerifier).digest('base64url')
|
||||
const state = randomBytes(16).toString('base64url')
|
||||
|
||||
const authorizeParams = new URLSearchParams({
|
||||
response_type: 'code',
|
||||
client_id: 'vikunja',
|
||||
redirect_uri: 'vikunja-flutter://callback',
|
||||
code_challenge: codeChallenge,
|
||||
code_challenge_method: 'S256',
|
||||
state,
|
||||
})
|
||||
|
||||
// The component POSTs as soon as it mounts with the existing session, so register the
|
||||
// listener before navigating.
|
||||
const authorizeResponsePromise = page.waitForResponse(
|
||||
response => response.url().includes('/api/v1/oauth/authorize') && response.request().method() === 'POST',
|
||||
{timeout: 15000},
|
||||
)
|
||||
|
||||
// Open the copyable login URL exactly as it would be pasted from another browser
|
||||
// (#redirect= is REDIRECT_HASH_PREFIX from @/constants/redirectHash, inlined here because
|
||||
// the e2e runner has no @ alias).
|
||||
const redirectDestination = `/oauth/authorize?${authorizeParams}`
|
||||
await page.goto(`/login#redirect=${encodeURIComponent(redirectDestination)}`)
|
||||
|
||||
// The authed guard must send us straight to /oauth/authorize, not home.
|
||||
await expect(page).toHaveURL(/\/oauth\/authorize/)
|
||||
const landed = new URL(page.url())
|
||||
expect(landed.pathname).toBe('/oauth/authorize')
|
||||
expect(landed.searchParams.get('response_type')).toBe('code')
|
||||
expect(landed.searchParams.get('client_id')).toBe('vikunja')
|
||||
expect(landed.searchParams.get('code_challenge')).toBe(codeChallenge)
|
||||
expect(landed.searchParams.get('state')).toBe(state)
|
||||
|
||||
// The PKCE flow completes with the existing session — no second login.
|
||||
const authorizeResponse = await authorizeResponsePromise
|
||||
const authorizeBody = await authorizeResponse.json()
|
||||
expect(authorizeBody.code).toBeTruthy()
|
||||
expect(authorizeBody.redirect_uri).toBe('vikunja-flutter://callback')
|
||||
expect(authorizeBody.state).toBe(state)
|
||||
|
||||
const tokenResponse = await apiContext.post('oauth/token', {
|
||||
data: {
|
||||
grant_type: 'authorization_code',
|
||||
code: authorizeBody.code,
|
||||
client_id: 'vikunja',
|
||||
redirect_uri: 'vikunja-flutter://callback',
|
||||
code_verifier: codeVerifier,
|
||||
},
|
||||
})
|
||||
|
||||
expect(tokenResponse.ok()).toBe(true)
|
||||
const tokenBody = await tokenResponse.json()
|
||||
expect(tokenBody.access_token).toBeTruthy()
|
||||
expect(tokenBody.refresh_token).toBeTruthy()
|
||||
expect(tokenBody.token_type).toBe('bearer')
|
||||
expect(tokenBody.expires_in).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
123
go.mod
123
go.mod
|
|
@ -16,58 +16,56 @@
|
|||
|
||||
module code.vikunja.io/api
|
||||
|
||||
go 1.26.4
|
||||
go 1.25.7
|
||||
|
||||
require (
|
||||
code.dny.dev/ssrf v0.2.0
|
||||
dario.cat/mergo v1.0.2
|
||||
github.com/JohannesKaufmann/dom v0.3.1
|
||||
github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.2
|
||||
github.com/ThreeDotsLabs/watermill v1.5.2
|
||||
github.com/ThreeDotsLabs/watermill v1.5.1
|
||||
github.com/adlio/trello v1.12.0
|
||||
github.com/arran4/golang-ical v0.3.5
|
||||
github.com/arran4/golang-ical v0.3.2
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
|
||||
github.com/aws/aws-sdk-go-v2 v1.42.0
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.26
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.25
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.104.1
|
||||
github.com/aws/smithy-go v1.27.3
|
||||
github.com/bbrks/go-blurhash v1.2.0
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.10
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.10
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3
|
||||
github.com/aws/smithy-go v1.24.2
|
||||
github.com/bbrks/go-blurhash v1.1.1
|
||||
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500
|
||||
github.com/coder/websocket v1.8.15
|
||||
github.com/coreos/go-oidc/v3 v3.19.0
|
||||
github.com/coder/websocket v1.8.14
|
||||
github.com/coreos/go-oidc/v3 v3.17.0
|
||||
github.com/d4l3k/messagediff v1.2.1
|
||||
github.com/danielgtaylor/huma/v2 v2.38.0
|
||||
github.com/danielgtaylor/huma/v2 v2.37.3
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/dustinkirkland/golang-petname v0.0.0-20240422154211-76c06c4bde6b
|
||||
github.com/fatih/color v1.19.0
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/gabriel-vasile/mimetype v1.4.13
|
||||
github.com/ganigeorgiev/fexpr v0.5.0
|
||||
github.com/getsentry/sentry-go v0.41.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.13
|
||||
github.com/go-sql-driver/mysql v1.10.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.12
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.19.0
|
||||
github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/feeds v1.2.0
|
||||
github.com/hashicorp/go-version v1.9.0
|
||||
github.com/hashicorp/go-version v1.8.0
|
||||
github.com/hhsnopek/etag v0.0.0-20171206181245-aea95f647346
|
||||
github.com/huandu/go-clone/generic v1.7.3
|
||||
github.com/iancoleman/strcase v0.3.0
|
||||
github.com/jaswdr/faker/v2 v2.9.1
|
||||
github.com/jinzhu/copier v0.4.0
|
||||
github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6
|
||||
github.com/labstack/echo-jwt/v5 v5.0.1
|
||||
github.com/labstack/echo/v5 v5.2.1
|
||||
github.com/lib/pq v1.12.3
|
||||
github.com/magefile/mage v1.17.2
|
||||
github.com/mattn/go-sqlite3 v1.14.47
|
||||
github.com/labstack/echo-jwt/v5 v5.0.0
|
||||
github.com/labstack/echo/v5 v5.0.3
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/magefile/mage v1.15.0
|
||||
github.com/mattn/go-sqlite3 v1.14.33
|
||||
github.com/microcosm-cc/bluemonday v1.0.27
|
||||
github.com/olekukonko/tablewriter v1.1.4
|
||||
github.com/olekukonko/tablewriter v1.1.3
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/redis/go-redis/v9 v9.21.0
|
||||
github.com/redis/go-redis/v9 v9.17.3
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/samedi/caldav-go v3.0.0+incompatible
|
||||
github.com/schollz/progressbar/v3 v3.19.0
|
||||
|
|
@ -78,43 +76,43 @@ require (
|
|||
github.com/tkuchiki/go-timezone v0.2.3
|
||||
github.com/traefik/yaegi v0.16.1
|
||||
github.com/ulule/limiter/v3 v3.11.2
|
||||
github.com/wneessen/go-mail v0.7.3
|
||||
github.com/yuin/goldmark v1.8.2
|
||||
golang.org/x/crypto v0.53.0
|
||||
github.com/wneessen/go-mail v0.7.2
|
||||
github.com/yuin/goldmark v1.7.16
|
||||
golang.org/x/crypto v0.48.0
|
||||
golang.org/x/image v0.38.0
|
||||
golang.org/x/net v0.55.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/sync v0.21.0
|
||||
golang.org/x/sys v0.46.0
|
||||
golang.org/x/term v0.44.0
|
||||
golang.org/x/text v0.38.0
|
||||
golang.org/x/net v0.50.0
|
||||
golang.org/x/oauth2 v0.34.0
|
||||
golang.org/x/sync v0.20.0
|
||||
golang.org/x/sys v0.41.0
|
||||
golang.org/x/term v0.40.0
|
||||
golang.org/x/text v0.35.0
|
||||
gopkg.in/d4l3k/messagediff.v1 v1.2.1
|
||||
mvdan.cc/xurls/v2 v2.6.0
|
||||
src.techknowlogick.com/xormigrate v1.7.1
|
||||
xorm.io/builder v0.3.13
|
||||
xorm.io/xorm v1.4.1
|
||||
xorm.io/xorm v1.3.11
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
filippo.io/edwards25519 v1.1.1 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/Azure/go-ntlmssp v0.1.1 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.30 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.2.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.31.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.43.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/beevik/etree v1.1.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
|
|
@ -136,7 +134,7 @@ require (
|
|||
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.1 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||
github.com/go-chi/chi/v5 v5.2.5 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
||||
|
|
@ -147,7 +145,7 @@ require (
|
|||
github.com/go-openapi/spec v0.20.4 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/goccy/go-json v0.10.6 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
|
|
@ -158,8 +156,8 @@ require (
|
|||
github.com/lithammer/shortuuid/v3 v3.0.7 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.22 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.21 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.20 // indirect
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/moby/api v1.53.0 // indirect
|
||||
|
|
@ -168,18 +166,18 @@ require (
|
|||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
||||
github.com/olekukonko/errors v1.2.0 // indirect
|
||||
github.com/olekukonko/ll v0.1.6 // indirect
|
||||
github.com/olekukonko/errors v1.1.0 // indirect
|
||||
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 // indirect
|
||||
github.com/onsi/ginkgo v1.16.4 // indirect
|
||||
github.com/onsi/gomega v1.16.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.67.5 // indirect
|
||||
github.com/prometheus/procfs v0.20.1 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||
|
|
@ -199,13 +197,12 @@ require (
|
|||
go.opentelemetry.io/otel v1.41.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.41.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.41.0 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9 // indirect
|
||||
golang.org/x/mod v0.36.0 // indirect
|
||||
golang.org/x/mod v0.33.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.45.0 // indirect
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
|
|
|||
179
go.sum
179
go.sum
|
|
@ -4,8 +4,6 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
|||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw=
|
||||
filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
|
||||
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
|
||||
gitee.com/travelliu/dm v1.8.11192/go.mod h1:DHTzyhCrM843x9VdKVbZ+GKXGRbKM2sJ4LxihRxShkE=
|
||||
|
|
@ -17,10 +15,6 @@ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg6
|
|||
github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw=
|
||||
github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/JohannesKaufmann/dom v0.3.1 h1:J16l9JAHWgkFPR3VIPbQ1gvS0cWab6laK1q7PFL3qh0=
|
||||
github.com/JohannesKaufmann/dom v0.3.1/go.mod h1:BZPkf8ZeYrBgABjwJn9iiKt8aiCtkxpHkevms+Yp2DE=
|
||||
github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.2 h1:XFJZFWESIWlUEHHjzBuv8RvrtCWnSGlimEX17ysSDb8=
|
||||
github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.2/go.mod h1:BHWO8lJzttJLqwuV8Rb1B3OG2OSzLbssZDI1FRg2eAA=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
|
|
@ -30,116 +24,56 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt
|
|||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=
|
||||
github.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=
|
||||
github.com/ThreeDotsLabs/watermill v1.5.2 h1:0ES33Eq1jEsP/pWvtE4n8bE0bs+9Jq7boT7wGBCVY6Q=
|
||||
github.com/ThreeDotsLabs/watermill v1.5.2/go.mod h1:i9/968UriGphWfEbfMuYSD1qFbYRjb0mE0r+rV0FPp4=
|
||||
github.com/adlio/trello v1.12.0 h1:JqOE2GFHQ9YtEviRRRSnicSxPbt4WFOxhqXzjMOw8lw=
|
||||
github.com/adlio/trello v1.12.0/go.mod h1:I4Lti4jf2KxjTNgTqs5W3lLuE78QZZdYbbPnQQGwjOo=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/arran4/golang-ical v0.3.2 h1:MGNjcXJFSuCXmYX/RpZhR2HDCYoFuK8vTPFLEdFC3JY=
|
||||
github.com/arran4/golang-ical v0.3.2/go.mod h1:xblDGxxIUMWwFZk9dlECUlc1iXNV65LJZOTHLVwu8bo=
|
||||
github.com/arran4/golang-ical v0.3.5 h1:bbz6ld4dC+MmCKiFfOd6SkmIGnhNMBACZ485ULh7p9A=
|
||||
github.com/arran4/golang-ical v0.3.5/go.mod h1:OnguFgjN0Hmx8jzpmWcC+AkHio94ujmLHKoaef7xQh8=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
|
||||
github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/bymhA=
|
||||
github.com/aws/aws-sdk-go-v2 v1.42.0/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 h1:p1BBrg/Hhp6uK7zpejeI8QFXHJeC/mynzi04Sl03k9g=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13/go.mod h1:8cIfkE9MDhkRZGpQ22aV6/lkYeYSozpz16Smrs5x4Ls=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.25 h1:ACCejvStYoilgwrfegSt5ZntCbPrk52qfwyNcnl3omM=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.25/go.mod h1:LJyU8sDRbXUxFn8xMJIGP+v9QYYwveNLI8a/giAOiAs=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.26 h1:JI+W5B3jUA8UBz2ggbICGd9UCR6/+SB21G8EFl0SFTQ=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.26/go.mod h1:RLE2Ls/wRstvdSz1GPrIWNnXcKZ/znDdWyMuiQxdBoY=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.24 h1:2hQqYCV9yqyePQ9o6dCrZc/zO8U3TwPr9mIKlZnPu/I=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.24/go.mod h1:IDwpACtwqHLISdzfwUUNq4P9DsB/h5BLg4FwJPNfqFY=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.25 h1:TzPVjfUZ1hsKafvYE+DIzKXIik2KufQxsPHanlkttbo=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.25/go.mod h1:K4hw0buguVvtC74HnVfTRr0LzQQHAWPqJbBU9QGk2Pg=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 h1:r6qZHbT+wxgWO/e9vYNUEtg7lv5+UN3pRqKhLXvnArg=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29/go.mod h1:QRnaRcTVGKPGRy8w78HMQtKUGRYcnMZAANATkeVA6Mo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 h1:f3vKqSo13fhTYb+JEcXwXefZQE26I1FB5eTSniU67ko=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29/go.mod h1:MzoLFUArKGpGD+ukmPiTPG1X5x4o6M2kq4v2dr1FiEc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 h1:RdwIf/CuUsvJX3RgJagbOyotl/cxoLY4xviKuE7p2GY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29/go.mod h1:71wt8W2EgswdZy9Mf9KNnzxZ3TiZlv4caKghPktDOkA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 h1:VTGy885W5DKBxWRUJbym9hytNaYzsyaPkCHGRRMAOhU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30/go.mod h1:AS0HycUvJRFvTt613AYDOgO2jzw+00cVSMny8XB3yMY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 h1:ZD2+BSw9vFsNlKYIasSNt3uDbjqqXIBcM13UJv/Lx2k=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12/go.mod h1:Ms4zlcVBbXbiP7EVLhl+lgjvA/a7YphqQ3Ih3174EmI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22 h1:V51LGlOq/1VsDsHUdoklAQi7rMmx4qQubvFYAlP2254=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22/go.mod h1:4Pzhyz8hJOm2bepgl+NjvRx8vlUFAIIvJnZ/MkcNPpU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 h1:DRebniUGZ2MqiiIVmQJ04vIXr918hubdHMnarSLEWyU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29/go.mod h1:LfRkPCD8YHDM2E5eTkos2UpwYeZnBcVarTa8L59bJHA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29 h1:hiME6pBzC7OTl9LMtlyTWBuEl1f4QBcUmFDKC7MLXtc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29/go.mod h1:G7RP+uhagpKtKhd1BM9N6JQqjCcGEU47K5lBVZQyRQw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.30 h1:4HbXxyipSYxexU0juMIpdS05dilL6dbB2VQHxxN2vGU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.30/go.mod h1:G7RP+uhagpKtKhd1BM9N6JQqjCcGEU47K5lBVZQyRQw=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3 h1:HwxWTbTrIHm5qY+CAEur0s/figc3qwvLWsNkF4RPToo=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.104.0 h1:ta8csKy5vN91F3i5gGR85lFV0srBqySEji7Jroes6rE=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.104.0/go.mod h1:77ZAgynvx1txMvDG8gGWoWkO1augYDxkp9JElWFgjQU=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.104.1 h1:yb03KevaOAG5e8suo79Af74vjIQvoeKmjl79WQchLrs=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.104.1/go.mod h1:mreYODw0Y4yv7xeczvqC6vciwFao8lPE9k1l1ulfY6E=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.2.0 h1:3nXpRcFwRCW8n7HgO2QGy0Dc20eQNfBuUemGQhpF8m8=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.2.0/go.mod h1:LxYujSTLPRlp2vTtcUO/+1ilrew8ytt6SvQyOgejzFQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.2.1 h1:BeJmkm5YOZs6lGRGcNoIuLSoTTtGLLCEqlSiRKYodfM=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.2.1/go.mod h1:LxYujSTLPRlp2vTtcUO/+1ilrew8ytt6SvQyOgejzFQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 h1:ey1XLTYXb9PcLt4535632o5kCGXNXEhNb620Dqwuylo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.31.3/go.mod h1:Lk7PlmoTYryQmyBG0EXqj5BcUbj3whXdU2s3yGI3EAc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.31.4 h1:i465b/3c7xJd++pobNIDOggouekCuiWOnB0goQJy+94=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.31.4/go.mod h1:Lk7PlmoTYryQmyBG0EXqj5BcUbj3whXdU2s3yGI3EAc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 h1:yLr03zQE/5Eu5l3QU0Si+xMbLMbSDF2YXsigqXngs6g=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6/go.mod h1:Q5N6icH+KJZDLh+ESNwzdv6cZ6vLFF/egy3IOxWhmz4=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.7 h1:xbmJAnBbyYPkTzoCNCF/bpJ6ymQHRdXX1vquYfDIGYk=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.7/go.mod h1:Q5N6icH+KJZDLh+ESNwzdv6cZ6vLFF/egy3IOxWhmz4=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 h1:VrIhKRCSK1umelSgB9RghvA9RTUYeQffyAS5ApXehNI=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.43.3/go.mod h1:r8wkDOuLaaMFqFiYAb8dGY2A3gJCOujMc6CFOVC4Zhc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.43.4 h1:Np0vmL7op0Zs5xGacYMMX3v5O5pvZ46xhb5LwDgPj8M=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.43.4/go.mod h1:r8wkDOuLaaMFqFiYAb8dGY2A3gJCOujMc6CFOVC4Zhc=
|
||||
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
|
||||
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/aws/smithy-go v1.27.1 h1:4T340VFndXtADGF52gYa1POyL7s9E4Z1OeZ1hCscIw8=
|
||||
github.com/aws/smithy-go v1.27.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/aws/smithy-go v1.27.3 h1:F3Zb497UhhskkfpJmfkXswyo+t0sh9OTBnIHjogWbVY=
|
||||
github.com/aws/smithy-go v1.27.3/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/bbrks/go-blurhash v1.1.1 h1:uoXOxRPDca9zHYabUTwvS4KnY++KKUbwFo+Yxb8ME4M=
|
||||
github.com/bbrks/go-blurhash v1.1.1/go.mod h1:lkAsdyXp+EhARcUo85yS2G1o+Sh43I2ebF5togC4bAY=
|
||||
github.com/bbrks/go-blurhash v1.2.0 h1:99w0YT50b/B7uoZyM79Nqy+UemMOh8fO/ONyyxmr9MU=
|
||||
github.com/bbrks/go-blurhash v1.2.0/go.mod h1:r4N4/ViVMa2h6Ex6e1aoCWMTkykYWS/VXvYMCrbkRpw=
|
||||
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
|
||||
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
|
|
@ -169,16 +103,12 @@ github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJ
|
|||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
|
||||
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
|
||||
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
|
||||
github.com/coreos/go-oidc/v3 v3.19.0 h1:F/xyOi3x1UnG1U27YVnM1N6bHiL1K2upi6U/0qr8r+I=
|
||||
github.com/coreos/go-oidc/v3 v3.19.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
|
|
@ -192,8 +122,6 @@ github.com/d4l3k/messagediff v1.2.1 h1:ZcAIMYsUg0EAp9X+tt8/enBE/Q8Yd5kzPynLyKptt
|
|||
github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
|
||||
github.com/danielgtaylor/huma/v2 v2.37.3 h1:6Av0Vj45Vk5lDxRVfoO2iPlEdvCvwLc7pl5nbqGOkYM=
|
||||
github.com/danielgtaylor/huma/v2 v2.37.3/go.mod h1:OeHHtCEAaNiuVbAVdYu4IQ0UOmnb4x3yMUOShNlZ53g=
|
||||
github.com/danielgtaylor/huma/v2 v2.38.0 h1:fb0WZCatnaiHLphMQDDWDjygNxfMkX/ENma3QsRl7vY=
|
||||
github.com/danielgtaylor/huma/v2 v2.38.0/go.mod h1:k9hwjlgWFt1t2jsmQGlsgXAG2FBTZa4kkjV581qAtfo=
|
||||
github.com/danielgtaylor/mexpr v1.9.1 h1:nA9bsGRmNlJeVCPFgGf7WhrLuKag/+iWfOaJ03iKFPI=
|
||||
github.com/danielgtaylor/mexpr v1.9.1/go.mod h1:kAivYNRnBeE/IJinqBvVFvLrX54xX//9zFYwADo4Bc8=
|
||||
github.com/danielgtaylor/shorthand/v2 v2.2.0 h1:hVsemdRq6v3JocP6YRTfu9rOoghZI9PFmkngdKqzAVQ=
|
||||
|
|
@ -224,8 +152,6 @@ github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjT
|
|||
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
|
|
@ -236,8 +162,6 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
|
|||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=
|
||||
github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/ganigeorgiev/fexpr v0.5.0 h1:XA9JxtTE/Xm+g/JFI6RfZEHSiQlk+1glLvRK1Lpv/Tk=
|
||||
|
|
@ -255,8 +179,6 @@ github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9
|
|||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
|
||||
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
|
||||
github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ=
|
||||
github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
|
|
@ -279,8 +201,6 @@ github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ
|
|||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
|
||||
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.19.0 h1:/Y0bars250zggm+1A2PvwaJQsJel7/tS4D/Hhwt66Bc=
|
||||
|
|
@ -293,15 +213,11 @@ github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGF
|
|||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
|
|
@ -343,8 +259,6 @@ github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/C
|
|||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
|
||||
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA=
|
||||
github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hhsnopek/etag v0.0.0-20171206181245-aea95f647346 h1:Odeq5rB6OZSkib5gqTG+EM1iF0bUVjYYd33XB1ULv00=
|
||||
github.com/hhsnopek/etag v0.0.0-20171206181245-aea95f647346/go.mod h1:4ggHM2qnyyZjenBb7RpwVzIj+JMsu9kHCVxMjB30hGs=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
|
|
@ -442,14 +356,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
|||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/labstack/echo-jwt/v5 v5.0.0 h1:uPp+FpkI/PKpMPPygtnK3RQOpg5a2wlM04UgfpWLVyI=
|
||||
github.com/labstack/echo-jwt/v5 v5.0.0/go.mod h1:RYF2ojWXbaY09QQ5J9vVtPUtkyI5UztS0gJotmCRz/U=
|
||||
github.com/labstack/echo-jwt/v5 v5.0.1 h1:uIpCHCiDPN3jA8Jb47i4EViToUl1uypMiPvVAAgKpIw=
|
||||
github.com/labstack/echo-jwt/v5 v5.0.1/go.mod h1:kcHmJPzrVSEJa1FRheVoi9EJrBLLUqr1ntlil6uPe1Q=
|
||||
github.com/labstack/echo/v5 v5.0.3 h1:Jql8sDtCYXrhh2Mbs6jKwjR6r7X8FSQQmch+w6QS7kc=
|
||||
github.com/labstack/echo/v5 v5.0.3/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo=
|
||||
github.com/labstack/echo/v5 v5.0.4 h1:ll3I/O8BifjMztj9dD1vx/peZQv8cR2CTUdQK6QxGGc=
|
||||
github.com/labstack/echo/v5 v5.0.4/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo=
|
||||
github.com/labstack/echo/v5 v5.2.1 h1:TzpIksY6zLMzV0T0ycYbvTEoj9w6o6AcL5twg182VTY=
|
||||
github.com/labstack/echo/v5 v5.2.1/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo=
|
||||
github.com/laurent22/ical-go v0.1.1-0.20181107184520-7e5d6ade8eef h1:RZnRnSID1skF35j/15KJ6hKZkdIC/teQClJK5wP5LU4=
|
||||
github.com/laurent22/ical-go v0.1.1-0.20181107184520-7e5d6ade8eef/go.mod h1:4LATl0uhhtytR6p9n1AlktDyIz4u2iUnWEdI3L/hXiw=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
|
|
@ -459,14 +367,10 @@ github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
|||
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
|
||||
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=
|
||||
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
||||
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/magefile/mage v1.17.2 h1:fyXVu1eadI8Ap1HCCNgEhJ5McIWiYhLR8uol64ZZc40=
|
||||
github.com/magefile/mage v1.17.2/go.mod h1:Yj51kqllmsgFpvvSzgrZPK9WtluG3kUhFaBUVLo4feA=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
|
|
@ -482,18 +386,14 @@ github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
|
|||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
||||
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
|
||||
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
|
||||
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
|
||||
github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.47 h1:jOBI62gS7nKeZv+as1oGEy0+1qISgXwH/QBlR6KbfIo=
|
||||
github.com/mattn/go-sqlite3 v1.14.47/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
|
||||
|
|
@ -524,16 +424,10 @@ github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj
|
|||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
|
||||
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
|
||||
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||
github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=
|
||||
github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 h1:jrYnow5+hy3WRDCBypUFvVKNSPPCdqgSXIE9eJDD8LM=
|
||||
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew=
|
||||
github.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA=
|
||||
github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88=
|
||||
github.com/olekukonko/tablewriter v1.1.3 h1:VSHhghXxrP0JHl+0NnKid7WoEmd9/urKRJLysb70nnA=
|
||||
github.com/olekukonko/tablewriter v1.1.3/go.mod h1:9VU0knjhmMkXjnMKrZ3+L2JhhtsQ/L38BbL3CRNE8tM=
|
||||
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
|
||||
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
|
|
@ -550,8 +444,6 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw
|
|||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
|
||||
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
|
||||
|
|
@ -569,16 +461,10 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
|
|||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/redis/go-redis/v9 v9.17.3 h1:fN29NdNrE17KttK5Ndf20buqfDZwGNgoUr9qjl1DQx4=
|
||||
github.com/redis/go-redis/v9 v9.17.3/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||
github.com/redis/go-redis/v9 v9.21.0 h1:FPBE4hhbAke+TLmcY3WkpbDffJEomdqPn3HYiqAtL9E=
|
||||
github.com/redis/go-redis/v9 v9.21.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
|
|
@ -600,10 +486,6 @@ github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeH
|
|||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc=
|
||||
github.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
|
||||
github.com/sebdah/goldie/v2 v2.8.0 h1:dZb9wR8q5++oplmEiJT+U/5KyotVD+HNGCAc5gNr8rc=
|
||||
github.com/sebdah/goldie/v2 v2.8.0/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
|
||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
|
|
@ -660,16 +542,14 @@ github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
|
|||
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
|
||||
github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=
|
||||
github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
|
||||
github.com/wneessen/go-mail v0.7.3 h1:g3DravXC5SMlVdboFrQA8Jx95A8sOzoBeS5F+vzNRK0=
|
||||
github.com/wneessen/go-mail v0.7.3/go.mod h1:QGhBX0yNbc1J+Mkjcu7z2rpj4B4l+BmDY8gYznPC9sk=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4 h1:0sw0nJM544SpsihWx1bkXdYLQDlzRflMgFJQ4Yih9ts=
|
||||
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4/go.mod h1:+ccdNT0xMY1dtc5XBxumbYfOUhmduiGudqaDgD2rVRE=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
|
||||
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
|
|
@ -690,8 +570,6 @@ go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
|||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
|
|
@ -703,8 +581,6 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
|||
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
|
|
@ -720,10 +596,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
|
|||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9 h1:yZNXmy+j/JpX19vZkVktWqAo7Gny4PBWYYK3zskGpx4=
|
||||
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
|
|
@ -734,10 +608,8 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG
|
|||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
|
|
@ -752,20 +624,16 @@ golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qx
|
|||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
|
@ -793,18 +661,15 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
||||
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
||||
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
|
|
@ -812,10 +677,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
|
|
@ -831,10 +694,8 @@ golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapK
|
|||
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
|
@ -938,5 +799,3 @@ xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
|
|||
xorm.io/xorm v1.3.3/go.mod h1:qFJGFoVYbbIdnz2vaL5OxSQ2raleMpyRRalnq3n9OJo=
|
||||
xorm.io/xorm v1.3.11 h1:i4tlVUASogb0ZZFJHA7dZqoRU2pUpUsutnNdaOlFyMI=
|
||||
xorm.io/xorm v1.3.11/go.mod h1:cs0ePc8O4a0jD78cNvD+0VFwhqotTvLQZv372QsDw7Q=
|
||||
xorm.io/xorm v1.4.1 h1:m7QlNd0eBGb31IV4Q/ow0Du83rtdC1CiwlvJZGvYde8=
|
||||
xorm.io/xorm v1.4.1/go.mod h1:cs0ePc8O4a0jD78cNvD+0VFwhqotTvLQZv372QsDw7Q=
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
[tools]
|
||||
node = "24.18.0" # keep in sync with frontend/.nvmrc
|
||||
pnpm = "10.34.4" # keep in sync with frontend/package.json#packageManager
|
||||
go = "1.26.4" # keep in sync with go.mod
|
||||
node = "24.13.0" # keep in sync with frontend/.nvmrc
|
||||
pnpm = "10.28.1" # keep in sync with frontend/package.json#packageManager
|
||||
go = "1.25.7" # keep in sync with go.mod
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package audit persists an audit trail of authentication, authorization and
|
||||
// data lifecycle events as JSONL, with optional forwarding to stdout, syslog
|
||||
// or webhook sinks.
|
||||
//
|
||||
// Events opt in via RegisterEventForAudit, which subscribes one audit
|
||||
// listener per event on the existing watermill bus; the event→Entry mapping
|
||||
// is a closure passed at registration. The catalog of audited events lives in
|
||||
// registerEventsForAuditLogging in pkg/models/listeners.go.
|
||||
//
|
||||
// Entries reference actors and targets by opaque ID only — deleting a user
|
||||
// row orphans their audit references, which satisfies GDPR erasure without
|
||||
// log redaction.
|
||||
//
|
||||
// Audit logging is gated twice: registration on the audit.enabled config key,
|
||||
// and each write on the licensed audit_logs feature. The license is checked
|
||||
// per event because it can change at runtime; enabled-but-unlicensed means
|
||||
// listeners run and write nothing.
|
||||
//
|
||||
// Request attribution (IP, user agent, request id) flows from an Echo
|
||||
// middleware through the request context onto message metadata — see
|
||||
// pkg/events.RequestMeta. Events dispatched outside a request get
|
||||
// source type "system" instead.
|
||||
//
|
||||
// The local file is the source of truth: a failed file write is returned to
|
||||
// the router for retry, while forwarder failures are only logged so a dead
|
||||
// sink cannot poison-queue every event. Tamper evidence comes from filesystem
|
||||
// permissions (the file is created 0600) plus shipping entries to an external
|
||||
// sink, not from hash chains or signatures. Rotation is size-based with
|
||||
// age-based cleanup of rotated files; retention is the operator's concern.
|
||||
package audit
|
||||
|
|
@ -14,33 +14,6 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package audit persists an audit trail of authentication, authorization and
|
||||
// data lifecycle events as JSONL.
|
||||
//
|
||||
// Events opt in via RegisterEventForAudit, which subscribes one audit
|
||||
// listener per event on the existing watermill bus; the event→Entry mapping
|
||||
// is a closure passed at registration. The catalog of audited events lives in
|
||||
// registerEventsForAuditLogging in pkg/models/listeners.go.
|
||||
//
|
||||
// Entries reference actors and targets by opaque ID only — deleting a user
|
||||
// row orphans their audit references, which satisfies GDPR erasure without
|
||||
// log redaction.
|
||||
//
|
||||
// Audit logging is gated twice: registration on the audit.enabled config key,
|
||||
// and each write on the licensed audit_logs feature. The license is checked
|
||||
// per event because it can change at runtime; enabled-but-unlicensed means
|
||||
// listeners run and write nothing.
|
||||
//
|
||||
// Request attribution (IP, user agent, request id) flows from an Echo
|
||||
// middleware through the request context onto message metadata — see
|
||||
// pkg/events.RequestMeta. Events dispatched outside a request get
|
||||
// source type "system" instead.
|
||||
//
|
||||
// A failed file write is returned to the router for retry. Tamper evidence
|
||||
// comes from filesystem permissions (the file is created 0600) plus shipping
|
||||
// the file to an external system, not from hash chains or signatures.
|
||||
// Rotation is size-based with age-based cleanup of rotated files; retention
|
||||
// is the operator's concern.
|
||||
package audit
|
||||
|
||||
import "time"
|
||||
|
|
|
|||
|
|
@ -45,15 +45,27 @@ func RegisterEventForAudit[T any, PT interface {
|
|||
events.Event
|
||||
}](toEntry func(PT) *Entry) {
|
||||
name := PT(new(T)).Name()
|
||||
RegisterEventNameForAudit(name, func(payload []byte) (*Entry, error) {
|
||||
e := PT(new(T)) // fresh instance per message — handlers run concurrently
|
||||
if err := json.Unmarshal(payload, e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return toEntry(e), nil
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterEventNameForAudit is the untyped variant for events which cannot be
|
||||
// unmarshaled into their Go struct directly (e.g. interface-typed Doer
|
||||
// fields); the mapping decodes the raw payload itself.
|
||||
func RegisterEventNameForAudit(name string, toEntry func(payload []byte) (*Entry, error)) {
|
||||
events.RegisterListener(name, &auditListener{handle: func(msg *message.Message) error {
|
||||
if !license.IsFeatureEnabled(license.FeatureAuditLogs) {
|
||||
return nil // license is runtime-mutable — checked per event, not at registration
|
||||
}
|
||||
e := PT(new(T)) // fresh instance per message — handlers run concurrently
|
||||
if err := json.Unmarshal(msg.Payload, e); err != nil {
|
||||
entry, err := toEntry(msg.Payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entry := toEntry(e)
|
||||
if entry == nil {
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Package sinks contains the forwarding targets for audit log entries.
|
||||
package sinks
|
||||
|
||||
// Sink forwards a single audit entry, passed as its serialized JSON line
|
||||
// without a trailing newline. Implementations must be safe for concurrent use.
|
||||
type Sink interface {
|
||||
Write(line []byte) error
|
||||
}
|
||||
|
|
@ -14,34 +14,31 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package richtext
|
||||
package sinks
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// TestMain bootstraps a test DB with user fixtures so the mention-resolution
|
||||
// tests can look up real users. The pure converter tests don't touch the DB.
|
||||
func TestMain(m *testing.M) {
|
||||
log.InitLogger()
|
||||
|
||||
x, err := db.CreateTestEngine()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := x.Sync2(user.GetTables()...); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := db.InitTestFixtures("users"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
os.Exit(m.Run())
|
||||
// Stdout writes each entry as one line to standard output.
|
||||
type Stdout struct {
|
||||
mu sync.Mutex
|
||||
// out exists so tests can capture the output.
|
||||
out io.Writer
|
||||
}
|
||||
|
||||
func NewStdout() *Stdout {
|
||||
return &Stdout{out: os.Stdout}
|
||||
}
|
||||
|
||||
func (s *Stdout) Write(line []byte) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if _, err := s.out.Write(line); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := s.out.Write([]byte{'\n'})
|
||||
return err
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package sinks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Hand-rolled RFC 5424 instead of log/syslog: the stdlib package only emits
|
||||
// the older RFC 3164 format and does not build on Windows.
|
||||
type Syslog struct {
|
||||
network string
|
||||
address string
|
||||
facility int
|
||||
hostname string
|
||||
procid string
|
||||
|
||||
mu sync.Mutex
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
var syslogFacilities = map[string]int{
|
||||
"kern": 0, "user": 1, "mail": 2, "daemon": 3, "auth": 4, "syslog": 5,
|
||||
"lpr": 6, "news": 7, "uucp": 8, "cron": 9, "authpriv": 10, "ftp": 11,
|
||||
"local0": 16, "local1": 17, "local2": 18, "local3": 19,
|
||||
"local4": 20, "local5": 21, "local6": 22, "local7": 23,
|
||||
}
|
||||
|
||||
// NewSyslog creates a syslog sink. The address has the form
|
||||
// udp://host:port or tcp://host:port; the scheme defaults to udp.
|
||||
func NewSyslog(address, facility string) (*Syslog, error) {
|
||||
if address == "" {
|
||||
return nil, fmt.Errorf("syslog forwarder requires an address")
|
||||
}
|
||||
if !strings.Contains(address, "://") {
|
||||
address = "udp://" + address
|
||||
}
|
||||
u, err := url.Parse(address)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid syslog address %q: %w", address, err)
|
||||
}
|
||||
if u.Scheme != "udp" && u.Scheme != "tcp" {
|
||||
return nil, fmt.Errorf("unsupported syslog scheme %q, must be udp or tcp", u.Scheme)
|
||||
}
|
||||
|
||||
if facility == "" {
|
||||
facility = "local0"
|
||||
}
|
||||
facilityCode, ok := syslogFacilities[strings.ToLower(facility)]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown syslog facility %q", facility)
|
||||
}
|
||||
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil || hostname == "" {
|
||||
hostname = "-"
|
||||
}
|
||||
|
||||
return &Syslog{
|
||||
network: u.Scheme,
|
||||
address: u.Host,
|
||||
facility: facilityCode,
|
||||
hostname: hostname,
|
||||
procid: fmt.Sprintf("%d", os.Getpid()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Syslog) Write(line []byte) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.conn == nil {
|
||||
dialer := &net.Dialer{Timeout: 10 * time.Second}
|
||||
conn, err := dialer.DialContext(context.Background(), s.network, s.address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not connect to syslog at %s://%s: %w", s.network, s.address, err)
|
||||
}
|
||||
s.conn = conn
|
||||
}
|
||||
|
||||
pri := s.facility*8 + 6 // severity: informational
|
||||
frame := fmt.Sprintf("<%d>1 %s %s vikunja %s audit - %s",
|
||||
pri, time.Now().UTC().Format(time.RFC3339Nano), s.hostname, s.procid, line)
|
||||
if s.network == "tcp" {
|
||||
frame += "\n" // RFC 6587 non-transparent framing
|
||||
}
|
||||
|
||||
if _, err := s.conn.Write([]byte(frame)); err != nil {
|
||||
// Drop the connection so the next write redials.
|
||||
_ = s.conn.Close()
|
||||
s.conn = nil
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package sinks
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
)
|
||||
|
||||
// Webhook POSTs each entry as a JSON body to a fixed URL.
|
||||
type Webhook struct {
|
||||
url string
|
||||
headers map[string]string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewWebhook(url string, headers map[string]string) (*Webhook, error) {
|
||||
if url == "" {
|
||||
return nil, fmt.Errorf("webhook forwarder requires a url")
|
||||
}
|
||||
return &Webhook{
|
||||
url: url,
|
||||
headers: headers,
|
||||
client: utils.NewSSRFSafeHTTPClient(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (w *Webhook) Write(line []byte) error {
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, w.url, bytes.NewReader(line))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", "Vikunja/audit")
|
||||
for key, value := range w.headers {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
|
||||
resp, err := w.client.Do(req) // #nosec G704 -- URL is the operator-configured sink target; the SSRF-safe client enforces IP restrictions
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return fmt.Errorf("audit webhook %s returned status %d", w.url, resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -18,7 +18,6 @@ package audit
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
|
@ -26,6 +25,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/audit/sinks"
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
|
||||
|
|
@ -41,9 +41,10 @@ var (
|
|||
maxSizeBytes int64
|
||||
maxAge time.Duration
|
||||
lastSync time.Time
|
||||
forwarders []sinks.Sink
|
||||
)
|
||||
|
||||
// Init opens the audit log file.
|
||||
// Init opens the audit log file and sets up the configured forwarders.
|
||||
// Safe to call again to re-read the config (used by tests).
|
||||
func Init() error {
|
||||
mu.Lock()
|
||||
|
|
@ -65,6 +66,13 @@ func Init() error {
|
|||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
forwarders, err = buildForwarders(config.AuditForwarders.Get())
|
||||
if err != nil {
|
||||
closeLocked()
|
||||
return err
|
||||
}
|
||||
|
||||
initialized = true
|
||||
return nil
|
||||
}
|
||||
|
|
@ -82,6 +90,7 @@ func closeLocked() {
|
|||
_ = logFile.Close()
|
||||
logFile = nil
|
||||
}
|
||||
forwarders = nil
|
||||
initialized = false
|
||||
}
|
||||
|
||||
|
|
@ -100,8 +109,74 @@ func openLogFileLocked() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// WriteAuditEvent writes one entry to the local audit log. A failed write is
|
||||
// returned so the event router retries it.
|
||||
func buildForwarders(raw any) (built []sinks.Sink, err error) {
|
||||
if raw == nil {
|
||||
return nil, nil
|
||||
}
|
||||
rawList, ok := raw.([]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("audit.forwarders must be a list, got %T", raw)
|
||||
}
|
||||
|
||||
for i, rawEntry := range rawList {
|
||||
entry, ok := toStringMap(rawEntry)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("audit.forwarders[%d] must be a map", i)
|
||||
}
|
||||
|
||||
var sink sinks.Sink
|
||||
typ, _ := entry["type"].(string)
|
||||
switch typ {
|
||||
case "stdout":
|
||||
sink = sinks.NewStdout()
|
||||
case "syslog":
|
||||
address, _ := entry["address"].(string)
|
||||
facility, _ := entry["facility"].(string)
|
||||
sink, err = sinks.NewSyslog(address, facility)
|
||||
case "webhook":
|
||||
targetURL, _ := entry["url"].(string)
|
||||
headers := map[string]string{}
|
||||
if rawHeaders, ok := toStringMap(entry["headers"]); ok {
|
||||
for key, value := range rawHeaders {
|
||||
headers[key], _ = value.(string)
|
||||
}
|
||||
}
|
||||
sink, err = sinks.NewWebhook(targetURL, headers)
|
||||
default:
|
||||
return nil, fmt.Errorf("audit.forwarders[%d] has unknown type %q", i, typ)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("audit.forwarders[%d]: %w", i, err)
|
||||
}
|
||||
built = append(built, sink)
|
||||
}
|
||||
return built, nil
|
||||
}
|
||||
|
||||
// toStringMap normalizes the two map shapes viper produces depending on the
|
||||
// config source (file vs. programmatic Set).
|
||||
func toStringMap(raw any) (map[string]any, bool) {
|
||||
switch m := raw.(type) {
|
||||
case map[string]any:
|
||||
return m, true
|
||||
case map[any]any:
|
||||
out := make(map[string]any, len(m))
|
||||
for key, value := range m {
|
||||
keyStr, ok := key.(string)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
out[keyStr] = value
|
||||
}
|
||||
return out, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// WriteAuditEvent writes one entry to the local audit log and forwards it to
|
||||
// all configured sinks. The local write is the source of truth — its failure
|
||||
// is returned so the event router retries; forwarder failures are only
|
||||
// logged, since a dead sink must not poison-queue every event.
|
||||
func WriteAuditEvent(entry *Entry) error {
|
||||
if entry.EventID == "" {
|
||||
id, err := uuid.NewV7()
|
||||
|
|
@ -133,27 +208,24 @@ func WriteAuditEvent(entry *Entry) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// A failed rotation can leave us without an open file — retry the open
|
||||
// here so writes self-heal via the router's retries instead of panicking.
|
||||
if logFile == nil {
|
||||
if err := openLogFileLocked(); err != nil {
|
||||
mu.Unlock()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
written, err := logFile.Write(append(line, '\n'))
|
||||
currentSize += int64(written)
|
||||
if err == nil && time.Since(lastSync) > time.Second {
|
||||
err = logFile.Sync()
|
||||
lastSync = time.Now()
|
||||
}
|
||||
currentForwarders := forwarders
|
||||
mu.Unlock()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not write audit entry: %w", err)
|
||||
}
|
||||
|
||||
for _, forwarder := range currentForwarders {
|
||||
if ferr := forwarder.Write(line); ferr != nil {
|
||||
log.Errorf("Could not forward audit entry %s: %s", entry.EventID, ferr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -169,9 +241,7 @@ func rotateIfNeededLocked(addition int64) error {
|
|||
rotatedPath := rotatedFileName(logfilePath, time.Now().UTC())
|
||||
if err := os.Rename(logfilePath, rotatedPath); err != nil {
|
||||
// Reopen the original so logging continues even if rotation failed.
|
||||
if openErr := openLogFileLocked(); openErr != nil {
|
||||
return errors.Join(fmt.Errorf("could not rotate audit log: %w", err), openErr)
|
||||
}
|
||||
_ = openLogFileLocked()
|
||||
return fmt.Errorf("could not rotate audit log: %w", err)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,9 +21,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/richtext"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
)
|
||||
|
|
@ -181,18 +179,8 @@ DURATION:PT` + formatDuration(t.Duration)
|
|||
DTEND:` + makeCalDavTimeFromTimeStamp(t.End)
|
||||
}
|
||||
if t.Description != "" {
|
||||
// CalDAV clients show plain text, so emit markdown. On the near-impossible
|
||||
// conversion error, log it and keep the stored value (GetContent can't
|
||||
// return an error) rather than drop the description.
|
||||
description, err := richtext.HTMLToMarkdown(t.Description)
|
||||
if err != nil {
|
||||
log.Errorf("[CALDAV] Failed to convert description to markdown for task %q: %v", t.UID, err)
|
||||
description = t.Description
|
||||
}
|
||||
if description != "" {
|
||||
caldavtodos += `
|
||||
DESCRIPTION:` + escapeICalText(description)
|
||||
}
|
||||
caldavtodos += `
|
||||
DESCRIPTION:` + escapeICalText(t.Description)
|
||||
}
|
||||
if t.Completed.Unix() > 0 {
|
||||
caldavtodos += `
|
||||
|
|
|
|||
|
|
@ -47,11 +47,12 @@ func TestParseTodos(t *testing.T) {
|
|||
},
|
||||
todos: []*Todo{
|
||||
{
|
||||
Summary: "Todo #1",
|
||||
Description: `<p>Lorem Ipsum</p><p>Dolor sit amet</p>`,
|
||||
UID: "randommduid",
|
||||
Timestamp: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
Color: "affffe",
|
||||
Summary: "Todo #1",
|
||||
Description: `Lorem Ipsum
|
||||
Dolor sit amet`,
|
||||
UID: "randommduid",
|
||||
Timestamp: time.Unix(1543626724, 0).In(config.GetTimeZone()),
|
||||
Color: "affffe",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -72,7 +73,7 @@ X-APPLE-CALENDAR-COLOR:#affffeFF
|
|||
X-OUTLOOK-COLOR:#affffeFF
|
||||
X-FUNAMBOL-COLOR:#affffeFF
|
||||
COLOR:#affffeFF
|
||||
DESCRIPTION:Lorem Ipsum\n\nDolor sit amet
|
||||
DESCRIPTION:Lorem Ipsum\nDolor sit amet
|
||||
LAST-MODIFIED:00010101T000000Z
|
||||
END:VTODO
|
||||
END:VCALENDAR`,
|
||||
|
|
@ -437,33 +438,6 @@ END:VCALENDAR`,
|
|||
}
|
||||
}
|
||||
|
||||
func TestParseTodosRichTextDescription(t *testing.T) {
|
||||
cfg := &Config{Name: "test", ProdID: "Vikunja"}
|
||||
ts := time.Unix(1543626724, 0).In(config.GetTimeZone())
|
||||
|
||||
t.Run("rich html serializes as markdown", func(t *testing.T) {
|
||||
out := ParseTodos(cfg, []*Todo{{
|
||||
Summary: "Todo",
|
||||
UID: "uid",
|
||||
Timestamp: ts,
|
||||
Description: `<p>Hello <strong>bold</strong> and <mention-user data-id="user1" data-label="User One">@User One</mention-user></p>` +
|
||||
`<ul data-type="taskList"><li data-checked="true" data-type="taskItem"><label><input type="checkbox" checked="checked"><span></span></label><div><p>done</p></div></li></ul>`,
|
||||
}})
|
||||
// iCal escapes the markdown's newlines as "\n".
|
||||
assert.Contains(t, out, `DESCRIPTION:Hello **bold** and @user1\n\n- [x] done`)
|
||||
})
|
||||
|
||||
t.Run("empty html omits the description line", func(t *testing.T) {
|
||||
out := ParseTodos(cfg, []*Todo{{Summary: "Todo", UID: "uid", Timestamp: ts, Description: "<p></p>"}})
|
||||
assert.NotContains(t, out, "DESCRIPTION:")
|
||||
})
|
||||
|
||||
t.Run("plain text description is unaffected", func(t *testing.T) {
|
||||
out := ParseTodos(cfg, []*Todo{{Summary: "Todo", UID: "uid", Timestamp: ts, Description: "just plain text"}})
|
||||
assert.Contains(t, out, "DESCRIPTION:just plain text")
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetCaldavColor(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
|
|||
|
|
@ -92,15 +92,14 @@ const (
|
|||
AuthLdapVerifyTLS Key = `auth.ldap.verifytls`
|
||||
AuthLdapBindDN Key = `auth.ldap.binddn`
|
||||
// #nosec G101
|
||||
AuthLdapBindPassword Key = `auth.ldap.bindpassword`
|
||||
AuthLdapGroupSyncEnabled Key = `auth.ldap.groupsyncenabled`
|
||||
AuthLdapGroupSyncFilter Key = `auth.ldap.groupsyncfilter`
|
||||
AuthLdapGroupSyncUseServiceAccount Key = `auth.ldap.groupsyncuseserviceaccount`
|
||||
AuthLdapAvatarSyncAttribute Key = `auth.ldap.avatarsyncattribute`
|
||||
AuthLdapAttributeUsername Key = `auth.ldap.attribute.username`
|
||||
AuthLdapAttributeEmail Key = `auth.ldap.attribute.email`
|
||||
AuthLdapAttributeDisplayname Key = `auth.ldap.attribute.displayname`
|
||||
AuthLdapAttributeMemberID Key = `auth.ldap.attribute.memberid`
|
||||
AuthLdapBindPassword Key = `auth.ldap.bindpassword`
|
||||
AuthLdapGroupSyncEnabled Key = `auth.ldap.groupsyncenabled`
|
||||
AuthLdapGroupSyncFilter Key = `auth.ldap.groupsyncfilter`
|
||||
AuthLdapAvatarSyncAttribute Key = `auth.ldap.avatarsyncattribute`
|
||||
AuthLdapAttributeUsername Key = `auth.ldap.attribute.username`
|
||||
AuthLdapAttributeEmail Key = `auth.ldap.attribute.email`
|
||||
AuthLdapAttributeDisplayname Key = `auth.ldap.attribute.displayname`
|
||||
AuthLdapAttributeMemberID Key = `auth.ldap.attribute.memberid`
|
||||
|
||||
LegalImprintURL Key = `legal.imprinturl`
|
||||
LegalPrivacyURL Key = `legal.privacyurl`
|
||||
|
|
@ -225,6 +224,7 @@ const (
|
|||
AuditLogfile Key = `audit.logfile`
|
||||
AuditRotationMaxSizeMB Key = `audit.rotation.maxsizemb`
|
||||
AuditRotationMaxAge Key = `audit.rotation.maxage`
|
||||
AuditForwarders Key = `audit.forwarders`
|
||||
|
||||
OutgoingRequestsAllowNonRoutableIPs Key = `outgoingrequests.allownonroutableips`
|
||||
OutgoingRequestsProxyURL Key = `outgoingrequests.proxyurl`
|
||||
|
|
@ -390,7 +390,6 @@ func InitDefaultConfig() {
|
|||
AuthLdapVerifyTLS.setDefault(true)
|
||||
AuthLdapGroupSyncEnabled.setDefault(false)
|
||||
AuthLdapGroupSyncFilter.setDefault("(&(objectclass=*)(|(objectclass=group)(objectclass=groupOfNames)))")
|
||||
AuthLdapGroupSyncUseServiceAccount.setDefault(false)
|
||||
AuthLdapAttributeUsername.setDefault("uid")
|
||||
AuthLdapAttributeEmail.setDefault("mail")
|
||||
AuthLdapAttributeDisplayname.setDefault("displayName")
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ func RestoreAndTruncate(table string, contents []map[string]interface{}) (err er
|
|||
return err
|
||||
}
|
||||
} else {
|
||||
if _, err := x.Query("TRUNCATE TABLE " + x.Quote(table)); err != nil {
|
||||
if _, err := x.Query("TRUNCATE TABLE ?", table); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
@ -148,7 +148,7 @@ func TruncateAllTables() error {
|
|||
return err
|
||||
}
|
||||
} else {
|
||||
if _, err := x.Query("TRUNCATE TABLE " + x.Quote(name)); err != nil {
|
||||
if _, err := x.Query("TRUNCATE TABLE ?", name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,41 +41,3 @@
|
|||
created_by_id: 3
|
||||
created: 2024-01-01 00:00:00
|
||||
updated: 2024-01-01 00:00:00
|
||||
# Webhooks 6-8 are user-level (project_id null, user_id set) and back the v2
|
||||
# user-webhook tests. #6/#7 belong to user6; #6 carries credentials so masking
|
||||
# can be asserted. #8 belongs to user1 so the owner-isolation check (user6 must
|
||||
# not see or mutate another user's webhook) has a target.
|
||||
#
|
||||
# Event choice matters because the pkg/e2etests user-webhook suite shares these
|
||||
# fixtures and dispatches real events. The WebhookListener fans a fired event out
|
||||
# to ALL of the event-user's webhooks, asynchronously; a user-level fixture
|
||||
# subscribed to a user-directed event the suite dispatches for its owner fires a
|
||||
# real (failing) delivery to example.com, and that in-flight write then races the
|
||||
# next test's fixture reload ("database table is locked: webhooks"). The suite
|
||||
# dispatches user-directed events only for user1, so #6/#7 are owned by user6, and
|
||||
# #8 (owned by user1) subscribes to task.updated — a project-only event the
|
||||
# listener never matches for user webhooks. None of the three can fire there.
|
||||
- id: 6
|
||||
target_url: "https://example.com/user-webhook-fixture"
|
||||
events: '["task.reminder.fired"]'
|
||||
user_id: 6
|
||||
secret: "uwh-secret-fixture"
|
||||
basic_auth_user: "uwh-basicauth-user"
|
||||
basic_auth_password: "uwh-basicauth-pass"
|
||||
created_by_id: 6
|
||||
created: 2024-01-01 00:00:00
|
||||
updated: 2024-01-01 00:00:00
|
||||
- id: 7
|
||||
target_url: "https://example.com/user-webhook-second"
|
||||
events: '["task.reminder.fired"]'
|
||||
user_id: 6
|
||||
created_by_id: 6
|
||||
created: 2024-01-01 00:00:00
|
||||
updated: 2024-01-01 00:00:00
|
||||
- id: 8
|
||||
target_url: "https://example.com/user-webhook-other"
|
||||
events: '["task.updated"]'
|
||||
user_id: 1
|
||||
created_by_id: 1
|
||||
created: 2024-01-01 00:00:00
|
||||
updated: 2024-01-01 00:00:00
|
||||
|
|
|
|||
|
|
@ -76,18 +76,6 @@ func ClearDispatchedEvents() {
|
|||
dispatchedTestEvents = nil
|
||||
}
|
||||
|
||||
// GetDispatchedEvents returns all dispatched test events matching the given name, letting tests
|
||||
// assert on the event payload (not just that it was dispatched).
|
||||
func GetDispatchedEvents(eventName string) []Event {
|
||||
var events []Event
|
||||
for _, testEvent := range dispatchedTestEvents {
|
||||
if testEvent.Name() == eventName {
|
||||
events = append(events, testEvent)
|
||||
}
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
// CountDispatchedEvents counts how many events of a specific type have been dispatched.
|
||||
func CountDispatchedEvents(eventName string) int {
|
||||
count := 0
|
||||
|
|
|
|||
|
|
@ -1,127 +0,0 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
type taskPosition20260617153629 struct {
|
||||
TaskID int64 `xorm:"bigint not null index"`
|
||||
ProjectViewID int64 `xorm:"bigint not null index"`
|
||||
Position float64 `xorm:"double not null"`
|
||||
}
|
||||
|
||||
func (taskPosition20260617153629) TableName() string {
|
||||
return "task_positions"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20260617153629",
|
||||
Description: "deduplicate task positions and add a unique index on task_id + project_view_id",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
|
||||
s := tx.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
err := s.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// First remove all duplicate entries. A task may only ever have a
|
||||
// single position per view; rapid task creation could race and
|
||||
// insert more than one row before this constraint existed.
|
||||
duplicates := []taskPosition20260617153629{}
|
||||
err = s.
|
||||
Select("task_id, project_view_id").
|
||||
GroupBy("task_id, project_view_id").
|
||||
Having("count(*) > 1").
|
||||
Find(&duplicates)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
// Keep the lowest position of each group so the result is
|
||||
// deterministic across databases.
|
||||
kept := []taskPosition20260617153629{}
|
||||
for _, dup := range duplicates {
|
||||
row := taskPosition20260617153629{}
|
||||
has, err := s.
|
||||
Where("task_id = ? AND project_view_id = ?", dup.TaskID, dup.ProjectViewID).
|
||||
OrderBy("position ASC").
|
||||
Get(&row)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return err
|
||||
}
|
||||
if !has {
|
||||
// The pair was just reported as duplicated by the GroupBy above,
|
||||
// so a row must exist. If it doesn't, fail instead of continuing —
|
||||
// the delete loop below would otherwise drop every row for the pair
|
||||
// without re-inserting one.
|
||||
_ = s.Rollback()
|
||||
return fmt.Errorf("no task_positions row found for task %d and project view %d while deduplicating positions", dup.TaskID, dup.ProjectViewID)
|
||||
}
|
||||
kept = append(kept, row)
|
||||
}
|
||||
|
||||
for _, dup := range duplicates {
|
||||
_, err = s.
|
||||
Where("task_id = ? AND project_view_id = ?", dup.TaskID, dup.ProjectViewID).
|
||||
Delete(&taskPosition20260617153629{})
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, position := range kept {
|
||||
_, err = s.Insert(&position)
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = s.Commit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Then create the unique index
|
||||
var query string
|
||||
switch tx.Dialect().URI().DBType {
|
||||
case schemas.MYSQL:
|
||||
query = "CREATE UNIQUE INDEX UQE_task_positions_task_project_view ON task_positions (task_id, project_view_id)"
|
||||
default:
|
||||
query = "CREATE UNIQUE INDEX IF NOT EXISTS UQE_task_positions_task_project_view ON task_positions (task_id, project_view_id)"
|
||||
}
|
||||
_, err = tx.Exec(query)
|
||||
return err
|
||||
},
|
||||
Rollback: func(_ *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// Mirrors models.Session; adds the two columns RP-Initiated Logout needs.
|
||||
type sessionOIDCLogout20260619155410 struct {
|
||||
ID string `xorm:"varchar(36) not null unique pk"`
|
||||
UserID int64 `xorm:"bigint not null index"`
|
||||
TokenHash string `xorm:"varchar(64) not null unique index"`
|
||||
DeviceInfo string `xorm:"text"`
|
||||
IPAddress string `xorm:"varchar(100)"`
|
||||
IsLongSession bool `xorm:"not null default false"`
|
||||
OIDCIDToken string `xorm:"text"`
|
||||
OIDCProviderKey string `xorm:"varchar(250)"`
|
||||
LastActive time.Time `xorm:"not null"`
|
||||
Created time.Time `xorm:"created not null"`
|
||||
}
|
||||
|
||||
func (sessionOIDCLogout20260619155410) TableName() string {
|
||||
return "sessions"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20260619155410",
|
||||
Description: "Add oidc_id_token and oidc_provider_key columns to sessions for RP-Initiated Logout",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
return tx.Sync(sessionOIDCLogout20260619155410{})
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/license"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type ShareCounts struct {
|
||||
LinkShares int64 `json:"link_shares" readOnly:"true" doc:"Number of link shares across all projects."`
|
||||
TeamShares int64 `json:"team_shares" readOnly:"true" doc:"Number of team-project shares."`
|
||||
UserShares int64 `json:"user_shares" readOnly:"true" doc:"Number of user-project shares."`
|
||||
}
|
||||
|
||||
type Overview struct {
|
||||
Users int64 `json:"users" readOnly:"true" doc:"Total number of user accounts."`
|
||||
Projects int64 `json:"projects" readOnly:"true" doc:"Total number of projects."`
|
||||
Tasks int64 `json:"tasks" readOnly:"true" doc:"Total number of tasks."`
|
||||
Teams int64 `json:"teams" readOnly:"true" doc:"Total number of teams."`
|
||||
Shares ShareCounts `json:"shares" readOnly:"true" doc:"Aggregate share counts."`
|
||||
License license.Info `json:"license" readOnly:"true" doc:"Snapshot of the instance license state."`
|
||||
}
|
||||
|
||||
// BuildOverview returns aggregate instance counts plus the current license snapshot.
|
||||
func BuildOverview(s *xorm.Session) (*Overview, error) {
|
||||
users, err := s.Table("users").Count()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
projects, err := s.Table("projects").Count()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tasks, err := s.Table("tasks").Count()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
teams, err := s.Table("teams").Count()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
linkShares, err := s.Table("link_shares").Count()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
teamShares, err := s.Table("team_projects").Count()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userShares, err := s.Table("users_projects").Count()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Overview{
|
||||
Users: users,
|
||||
Projects: projects,
|
||||
Tasks: tasks,
|
||||
Teams: teams,
|
||||
Shares: ShareCounts{
|
||||
LinkShares: linkShares,
|
||||
TeamShares: teamShares,
|
||||
UserShares: userShares,
|
||||
},
|
||||
License: license.CurrentInfo(),
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// loadAdminTargetUser fetches a user by ID for the admin actions, returning
|
||||
// ErrUserDoesNotExist for an invalid ID or a missing row.
|
||||
func loadAdminTargetUser(s *xorm.Session, id int64) (*user.User, error) {
|
||||
if id < 1 {
|
||||
return nil, user.ErrUserDoesNotExist{UserID: id}
|
||||
}
|
||||
target := &user.User{ID: id}
|
||||
has, err := s.Get(target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
return nil, user.ErrUserDoesNotExist{UserID: id}
|
||||
}
|
||||
return target, nil
|
||||
}
|
||||
|
||||
// SetUserAdminFlag sets a user's instance-admin flag. Demoting the last
|
||||
// reachable admin is refused via GuardLastAdmin. It does not commit; the caller
|
||||
// owns the transaction.
|
||||
func SetUserAdminFlag(s *xorm.Session, id int64, isAdmin bool) (*user.User, error) {
|
||||
target, err := loadAdminTargetUser(s, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !isAdmin {
|
||||
if err := user.GuardLastAdmin(s, target); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
target.IsAdmin = isAdmin
|
||||
if _, err := s.ID(target.ID).Cols("is_admin").Update(target); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return target, nil
|
||||
}
|
||||
|
||||
// SetUserStatusAsAdmin sets a user's account status. Moving the last reachable
|
||||
// admin out of Active is refused via GuardLastAdmin (any non-Active status
|
||||
// blocks login, so it is equivalent to demotion). It does not commit; the caller
|
||||
// owns the transaction.
|
||||
func SetUserStatusAsAdmin(s *xorm.Session, id int64, status user.Status) (*user.User, error) {
|
||||
target, err := loadAdminTargetUser(s, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if target.IsAdmin && status != user.StatusActive {
|
||||
if err := user.GuardLastAdmin(s, target); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := user.SetUserStatus(s, target, status); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Reflect the change on the returned struct; GetUserByID refuses disabled accounts.
|
||||
target.Status = status
|
||||
return target, nil
|
||||
}
|
||||
|
||||
// DeleteUserAsAdmin removes a user. mode "now" deletes immediately; any other
|
||||
// value triggers the email-confirmation self-deletion flow. Deleting the last
|
||||
// reachable admin is refused via GuardLastAdmin. It does not commit; the caller
|
||||
// owns the transaction.
|
||||
func DeleteUserAsAdmin(s *xorm.Session, id int64, mode string) error {
|
||||
target, err := loadAdminTargetUser(s, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := user.GuardLastAdmin(s, target); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if mode == "now" {
|
||||
return DeleteUser(s, target)
|
||||
}
|
||||
return user.RequestDeletion(s, target)
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// CreateUserBody wraps user.APIUserPassword with admin-only fields.
|
||||
type CreateUserBody struct {
|
||||
// The full name of the new user. Optional.
|
||||
Name string `json:"name" doc:"The full name of the new user. Optional."`
|
||||
// The language of the new user. Must be a valid IETF BCP 47 language code and exist in Vikunja.
|
||||
Language string `json:"language" valid:"language" doc:"IETF BCP 47 language code; must exist in Vikunja."`
|
||||
user.APIUserPassword
|
||||
// Mark the new user as an instance admin.
|
||||
IsAdmin bool `json:"is_admin" doc:"Mark the new user as an instance admin."`
|
||||
// Activate the new user immediately without email confirmation.
|
||||
SkipEmailConfirm bool `json:"skip_email_confirm" doc:"Activate the new user immediately, skipping email confirmation."`
|
||||
}
|
||||
|
||||
// CreateUserAsAdmin provisions a new local account on behalf of an instance admin,
|
||||
// honouring the admin-only is_admin and skip_email_confirm fields and bypassing the
|
||||
// public-registration toggle. It commits s and returns the persisted user reloaded
|
||||
// so the status reflects what was actually stored.
|
||||
func CreateUserAsAdmin(s *xorm.Session, body *CreateUserBody) (*user.User, error) {
|
||||
newUser, err := RegisterUser(s, &user.User{
|
||||
Username: body.Username,
|
||||
Password: body.Password,
|
||||
Email: body.Email,
|
||||
Name: body.Name,
|
||||
Language: body.Language,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if body.IsAdmin {
|
||||
if _, err := s.ID(newUser.ID).Cols("is_admin").Update(&user.User{IsAdmin: true}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newUser.IsAdmin = true
|
||||
}
|
||||
|
||||
// Force Active when the admin asked to skip, or when no mailer exists to send the confirmation.
|
||||
if body.SkipEmailConfirm || !config.MailerEnabled.GetBool() {
|
||||
if err := user.SetUserStatus(s, newUser, user.StatusActive); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newUser.Status = user.StatusActive
|
||||
}
|
||||
|
||||
if err := s.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Reload on a fresh session so the returned status reflects what was actually
|
||||
// persisted (e.g. StatusEmailConfirmationRequired on mail-enabled instances).
|
||||
rs := db.NewSession()
|
||||
defer rs.Close()
|
||||
return user.GetUserByID(rs, newUser.ID)
|
||||
}
|
||||
|
|
@ -74,13 +74,6 @@ func stripAPIVersion(path string) string {
|
|||
return path
|
||||
}
|
||||
|
||||
// canonicalAPITokenGroup snake_cases a permission group name. The frontend
|
||||
// snake_cases request payloads, so a hyphenated group slug (e.g. from
|
||||
// /api/v2/time-entries) can't round-trip and fails validation on save.
|
||||
func canonicalAPITokenGroup(group string) string {
|
||||
return strings.ReplaceAll(group, "-", "_")
|
||||
}
|
||||
|
||||
func getRouteGroupName(path string) (finalName string, filteredParts []string) {
|
||||
parts := strings.Split(stripAPIVersion(path), "/")
|
||||
filteredParts = []string{}
|
||||
|
|
@ -89,7 +82,7 @@ func getRouteGroupName(path string) (finalName string, filteredParts []string) {
|
|||
continue
|
||||
}
|
||||
|
||||
filteredParts = append(filteredParts, canonicalAPITokenGroup(part))
|
||||
filteredParts = append(filteredParts, part)
|
||||
}
|
||||
|
||||
finalName = strings.Join(filteredParts, "_")
|
||||
|
|
@ -190,7 +183,7 @@ func isStandardCRUDRoute(routeGroupName string, routeParts []string, _ string) b
|
|||
"comments": true,
|
||||
"relations": true,
|
||||
"attachments": true,
|
||||
"time_entries": true,
|
||||
"time-entries": true,
|
||||
"projects_views": true,
|
||||
"projects_teams": true,
|
||||
"projects_users": true,
|
||||
|
|
@ -410,8 +403,7 @@ func CanDoAPIRoute(c *echo.Context, token *APIToken) (can bool) {
|
|||
}
|
||||
method := c.Request().Method
|
||||
|
||||
for rawGroup, perms := range token.APIPermissions {
|
||||
group := canonicalAPITokenGroup(rawGroup)
|
||||
for group, perms := range token.APIPermissions {
|
||||
tables := []APITokenRoute{apiTokenRoutes[group], apiTokenRoutesV2[group]}
|
||||
for _, routes := range tables {
|
||||
if routes == nil {
|
||||
|
|
@ -435,8 +427,7 @@ func CanDoAPIRoute(c *echo.Context, token *APIToken) (can bool) {
|
|||
// Two list endpoints share tasks.read_all but only one
|
||||
// survives collection, so allow either explicitly.
|
||||
if group == "tasks" && p == "read_all" && method == http.MethodGet &&
|
||||
(path == "/api/v1/tasks" || path == "/api/v1/projects/:project/tasks" ||
|
||||
path == "/api/v2/tasks" || path == "/api/v2/projects/:project/tasks") {
|
||||
(path == "/api/v1/tasks" || path == "/api/v1/projects/:project/tasks") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -456,9 +447,8 @@ func PermissionsAreValid(permissions APIPermissions) (err error) {
|
|||
// resources (no v1 counterpart) live solely in apiTokenRoutesV2, so
|
||||
// validating against the union lets tokens grant them. CanDoAPIRoute
|
||||
// already consults both tables when authorising.
|
||||
group := canonicalAPITokenGroup(key)
|
||||
v1Routes := apiTokenRoutes[group]
|
||||
v2Routes := apiTokenRoutesV2[group]
|
||||
v1Routes := apiTokenRoutes[key]
|
||||
v2Routes := apiTokenRoutesV2[key]
|
||||
if v1Routes == nil && v2Routes == nil {
|
||||
return &ErrInvalidAPITokenPermission{
|
||||
Group: key,
|
||||
|
|
|
|||
|
|
@ -121,9 +121,9 @@ func TestCollectRoutesV2(t *testing.T) {
|
|||
assert.Equal(t, "DELETE", labels["delete"].Method)
|
||||
}
|
||||
|
||||
// TestCollectRoutes_TimeEntriesV2 pins the v2-only time-entries resource to a
|
||||
// snake_case "time_entries" group (not the "other" catch-all, not a hyphenated
|
||||
// key the frontend's snake_case transform would mangle on save).
|
||||
// TestCollectRoutes_TimeEntriesV2 verifies the v2-only time-entries resource
|
||||
// lands under a clean "time-entries" group rather than the "other" catch-all,
|
||||
// so its scopes read sensibly for token clients.
|
||||
func TestCollectRoutes_TimeEntriesV2(t *testing.T) {
|
||||
apiTokenRoutes = make(map[string]APITokenRoute)
|
||||
apiTokenRoutesV2 = make(map[string]APITokenRoute)
|
||||
|
|
@ -137,11 +137,8 @@ func TestCollectRoutes_TimeEntriesV2(t *testing.T) {
|
|||
_, isOther := apiTokenRoutesV2["other"]
|
||||
assert.False(t, isOther, "time-entries CRUD must not fall into the 'other' bucket")
|
||||
|
||||
_, hyphenated := apiTokenRoutesV2["time-entries"]
|
||||
assert.False(t, hyphenated, "group key must be canonicalised to snake_case")
|
||||
|
||||
te, has := apiTokenRoutesV2["time_entries"]
|
||||
require.True(t, has, "time_entries group should exist in the v2 table")
|
||||
te, has := apiTokenRoutesV2["time-entries"]
|
||||
require.True(t, has, "time-entries group should exist in the v2 table")
|
||||
assert.Equal(t, "GET", te["read_all"].Method)
|
||||
assert.Equal(t, "/api/v2/time-entries", te["read_all"].Path)
|
||||
assert.Equal(t, "GET", te["read_one"].Method)
|
||||
|
|
@ -151,7 +148,7 @@ func TestCollectRoutes_TimeEntriesV2(t *testing.T) {
|
|||
}
|
||||
|
||||
// TestGetAPITokenRoutes_ExposesV2Only verifies the /routes payload merges
|
||||
// v2-only groups (time_entries has no v1 counterpart) so token clients can
|
||||
// v2-only groups (time-entries has no v1 counterpart) so token clients can
|
||||
// discover and grant them, without mutating the v1 table itself.
|
||||
func TestGetAPITokenRoutes_ExposesV2Only(t *testing.T) {
|
||||
apiTokenRoutes = make(map[string]APITokenRoute)
|
||||
|
|
@ -165,35 +162,14 @@ func TestGetAPITokenRoutes_ExposesV2Only(t *testing.T) {
|
|||
_, hasLabels := routes["labels"]
|
||||
assert.True(t, hasLabels, "v1 groups stay exposed")
|
||||
|
||||
te, hasTE := routes["time_entries"]
|
||||
require.True(t, hasTE, "v2-only time_entries must be exposed via /routes")
|
||||
te, hasTE := routes["time-entries"]
|
||||
require.True(t, hasTE, "v2-only time-entries must be exposed via /routes")
|
||||
assert.Equal(t, "GET", te["read_all"].Method)
|
||||
|
||||
_, v1HasTE := apiTokenRoutes["time_entries"]
|
||||
_, v1HasTE := apiTokenRoutes["time-entries"]
|
||||
assert.False(t, v1HasTE, "the merge must not mutate the v1 table")
|
||||
}
|
||||
|
||||
// TestCanDoAPIRoute_TimeEntriesHyphenLegacy proves a token stored under the old
|
||||
// hyphenated "time-entries" key still validates and authorises — no migration.
|
||||
func TestCanDoAPIRoute_TimeEntriesHyphenLegacy(t *testing.T) {
|
||||
apiTokenRoutes = make(map[string]APITokenRoute)
|
||||
apiTokenRoutesV2 = make(map[string]APITokenRoute)
|
||||
|
||||
CollectRoutesForAPITokenUsage(echo.RouteInfo{Method: "GET", Path: "/api/v2/time-entries"}, true)
|
||||
|
||||
for _, key := range []string{"time_entries", "time-entries"} {
|
||||
t.Run(key, func(t *testing.T) {
|
||||
perms := APIPermissions{key: []string{"read_all"}}
|
||||
require.NoError(t, PermissionsAreValid(perms), "%s must validate", key)
|
||||
|
||||
token := &APIToken{APIPermissions: perms}
|
||||
req := httptest.NewRequest("GET", "/api/v2/time-entries", nil)
|
||||
c := echo.New().NewContext(req, httptest.NewRecorder())
|
||||
assert.True(t, CanDoAPIRoute(c, token), "%s must authorise", key)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetRouteDetail_V2Verbs verifies the v2 verb mapping: POST→create,
|
||||
// PUT/PATCH→update. v1 inverts POST and PUT so we need a separate mapping
|
||||
// path.
|
||||
|
|
@ -270,40 +246,6 @@ func TestCanDoAPIRoute_V2PatchAliasesPut(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
// TestCanDoAPIRoute_V2TasksReadAll verifies that tasks.read_all authorises
|
||||
// both the global /api/v2/tasks and project-scoped /api/v2/projects/:project/tasks
|
||||
// endpoints. Both normalise to tasks.read_all via getRouteGroupName, but only
|
||||
// one RouteDetail survives in the map — the special case in CanDoAPIRoute must
|
||||
// accept either path.
|
||||
func TestCanDoAPIRoute_V2TasksReadAll(t *testing.T) {
|
||||
apiTokenRoutes = make(map[string]APITokenRoute)
|
||||
apiTokenRoutesV2 = make(map[string]APITokenRoute)
|
||||
apiTokenRoutes["caldav"] = APITokenRoute{
|
||||
"access": &RouteDetail{Path: "/dav/*", Method: "ANY"},
|
||||
}
|
||||
|
||||
CollectRoutesForAPITokenUsage(echo.RouteInfo{Method: "GET", Path: "/api/v2/tasks"}, true)
|
||||
CollectRoutesForAPITokenUsage(echo.RouteInfo{Method: "GET", Path: "/api/v2/projects/:project/tasks"}, true)
|
||||
|
||||
token := &APIToken{
|
||||
APIPermissions: APIPermissions{"tasks": []string{"read_all"}},
|
||||
}
|
||||
|
||||
e := echo.New()
|
||||
|
||||
t.Run("global /api/v2/tasks", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/v2/tasks", nil)
|
||||
c := e.NewContext(req, httptest.NewRecorder())
|
||||
assert.True(t, CanDoAPIRoute(c, token))
|
||||
})
|
||||
|
||||
t.Run("project-scoped /api/v2/projects/:project/tasks", func(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/api/v2/projects/:project/tasks", nil)
|
||||
c := e.NewContext(req, httptest.NewRecorder())
|
||||
assert.True(t, CanDoAPIRoute(c, token))
|
||||
})
|
||||
}
|
||||
|
||||
// End-to-end CanDoAPIRoute coverage for /api/v2 is provided by the Label
|
||||
// integration test in pkg/webtests/huma_label_test.go (see the token-auth
|
||||
// scenarios in that file) which exercises the full auth pipeline.
|
||||
|
|
|
|||
|
|
@ -535,34 +535,6 @@ func (err *ErrProjectViewDoesNotExist) HTTPError() web.HTTPError {
|
|||
}
|
||||
}
|
||||
|
||||
// ErrProjectHasNoBackground represents an error where a project has no background set.
|
||||
type ErrProjectHasNoBackground struct {
|
||||
ProjectID int64
|
||||
}
|
||||
|
||||
// IsErrProjectHasNoBackground checks if an error is ErrProjectHasNoBackground.
|
||||
func IsErrProjectHasNoBackground(err error) bool {
|
||||
_, ok := err.(*ErrProjectHasNoBackground)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err *ErrProjectHasNoBackground) Error() string {
|
||||
return fmt.Sprintf("Project has no background [ProjectID: %d]", err.ProjectID)
|
||||
}
|
||||
|
||||
// ErrCodeProjectHasNoBackground holds the unique world-error code of this error
|
||||
const ErrCodeProjectHasNoBackground = 3015
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err *ErrProjectHasNoBackground) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{
|
||||
HTTPCode: http.StatusNotFound,
|
||||
Code: ErrCodeProjectHasNoBackground,
|
||||
// Message kept verbatim from v1's inline handler error so the wire body is unchanged.
|
||||
Message: "Project background not found",
|
||||
}
|
||||
}
|
||||
|
||||
// ==============
|
||||
// Task errors
|
||||
// ==============
|
||||
|
|
@ -2624,32 +2596,3 @@ func (err ErrTimeEntryEndBeforeStart) HTTPError() web.HTTPError {
|
|||
Message: "A time entry's end time cannot be before its start time.",
|
||||
}
|
||||
}
|
||||
|
||||
// =================
|
||||
// User export errors
|
||||
// =================
|
||||
|
||||
// ErrUserDataExportDoesNotExist represents an error where a user has no ready data export to download.
|
||||
type ErrUserDataExportDoesNotExist struct{}
|
||||
|
||||
// IsErrUserDataExportDoesNotExist checks if an error is ErrUserDataExportDoesNotExist.
|
||||
func IsErrUserDataExportDoesNotExist(err error) bool {
|
||||
_, ok := err.(ErrUserDataExportDoesNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrUserDataExportDoesNotExist) Error() string {
|
||||
return "No user data export found"
|
||||
}
|
||||
|
||||
// ErrCodeUserDataExportDoesNotExist holds the unique world-error code of this error
|
||||
const ErrCodeUserDataExportDoesNotExist = 19001
|
||||
|
||||
// HTTPError holds the http error description
|
||||
func (err ErrUserDataExportDoesNotExist) HTTPError() web.HTTPError {
|
||||
return web.HTTPError{
|
||||
HTTPCode: http.StatusNotFound,
|
||||
Code: ErrCodeUserDataExportDoesNotExist,
|
||||
Message: "No user data export found.",
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package models
|
|||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/api/pkg/web"
|
||||
)
|
||||
|
||||
/////////////////
|
||||
|
|
@ -229,8 +230,8 @@ func (l *ProjectCreatedEvent) Name() string {
|
|||
|
||||
// ProjectUpdatedEvent represents an event where a project has been updated
|
||||
type ProjectUpdatedEvent struct {
|
||||
Project *Project `json:"project"`
|
||||
Doer *user.User `json:"doer"`
|
||||
Project *Project `json:"project"`
|
||||
Doer web.Auth `json:"doer"`
|
||||
}
|
||||
|
||||
// Name defines the name for ProjectUpdatedEvent
|
||||
|
|
@ -240,8 +241,8 @@ func (p *ProjectUpdatedEvent) Name() string {
|
|||
|
||||
// ProjectDeletedEvent represents an event where a project has been deleted
|
||||
type ProjectDeletedEvent struct {
|
||||
Project *Project `json:"project"`
|
||||
Doer *user.User `json:"doer"`
|
||||
Project *Project `json:"project"`
|
||||
Doer web.Auth `json:"doer"`
|
||||
}
|
||||
|
||||
// Name defines the name for ProjectDeletedEvent
|
||||
|
|
@ -257,7 +258,7 @@ func (p *ProjectDeletedEvent) Name() string {
|
|||
type ProjectSharedWithUserEvent struct {
|
||||
Project *Project `json:"project"`
|
||||
User *user.User `json:"user"`
|
||||
Doer *user.User `json:"doer"`
|
||||
Doer web.Auth `json:"doer"`
|
||||
}
|
||||
|
||||
// Name defines the name for ProjectSharedWithUserEvent
|
||||
|
|
@ -267,9 +268,9 @@ func (p *ProjectSharedWithUserEvent) Name() string {
|
|||
|
||||
// ProjectSharedWithTeamEvent represents an event where a project has been shared with a team
|
||||
type ProjectSharedWithTeamEvent struct {
|
||||
Project *Project `json:"project"`
|
||||
Team *Team `json:"team"`
|
||||
Doer *user.User `json:"doer"`
|
||||
Project *Project `json:"project"`
|
||||
Team *Team `json:"team"`
|
||||
Doer web.Auth `json:"doer"`
|
||||
}
|
||||
|
||||
// Name defines the name for ProjectSharedWithTeamEvent
|
||||
|
|
@ -307,8 +308,8 @@ func (t *TeamMemberRemovedEvent) Name() string {
|
|||
|
||||
// TeamCreatedEvent represents a TeamCreatedEvent event
|
||||
type TeamCreatedEvent struct {
|
||||
Team *Team `json:"team"`
|
||||
Doer *user.User `json:"doer"`
|
||||
Team *Team `json:"team"`
|
||||
Doer web.Auth `json:"doer"`
|
||||
}
|
||||
|
||||
// Name defines the name for TeamCreatedEvent
|
||||
|
|
@ -318,8 +319,8 @@ func (t *TeamCreatedEvent) Name() string {
|
|||
|
||||
// TeamDeletedEvent represents a TeamDeletedEvent event
|
||||
type TeamDeletedEvent struct {
|
||||
Team *Team `json:"team"`
|
||||
Doer *user.User `json:"doer"`
|
||||
Team *Team `json:"team"`
|
||||
Doer web.Auth `json:"doer"`
|
||||
}
|
||||
|
||||
// Name defines the name for TeamDeletedEvent
|
||||
|
|
|
|||
|
|
@ -404,64 +404,6 @@ func exportProjectBackgrounds(s *xorm.Session, u *user.User, wr *zip.Writer) (er
|
|||
return utils.WriteFilesToZip(backgroundFiles, wr)
|
||||
}
|
||||
|
||||
// GetUserDataExportFile loads the user's ready data export with its bytes open for
|
||||
// reading. It returns ErrUserDataExportDoesNotExist when the user never requested an
|
||||
// export or the underlying file is gone. The caller must close the returned reader.
|
||||
func GetUserDataExportFile(u *user.User) (*files.File, error) {
|
||||
if u.ExportFileID == 0 {
|
||||
return nil, ErrUserDataExportDoesNotExist{}
|
||||
}
|
||||
|
||||
exportFile := &files.File{ID: u.ExportFileID}
|
||||
if err := exportFile.LoadFileMetaByID(); err != nil {
|
||||
if files.IsErrFileDoesNotExist(err) {
|
||||
return nil, ErrUserDataExportDoesNotExist{}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if err := exportFile.LoadFileByID(); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, ErrUserDataExportDoesNotExist{}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return exportFile, nil
|
||||
}
|
||||
|
||||
// GetUserDataExportStatus returns metadata about the user's current data export, or
|
||||
// nil when none exists. The expiry mirrors the cleanup cron's 7-day retention.
|
||||
func GetUserDataExportStatus(u *user.User) (*UserExportStatus, error) {
|
||||
if u.ExportFileID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
exportFile := &files.File{ID: u.ExportFileID}
|
||||
if err := exportFile.LoadFileMetaByID(); err != nil {
|
||||
// A missing meta row means there is no export — mirror the download path
|
||||
// (404 there) instead of surfacing a 500.
|
||||
if files.IsErrFileDoesNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &UserExportStatus{
|
||||
ID: exportFile.ID,
|
||||
Size: exportFile.Size,
|
||||
Created: exportFile.Created,
|
||||
Expires: exportFile.Created.Add(7 * 24 * time.Hour),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UserExportStatus is the metadata returned for a user's current data export.
|
||||
type UserExportStatus struct {
|
||||
ID int64 `json:"id" readOnly:"true" doc:"The id of the export file."`
|
||||
Size uint64 `json:"size" readOnly:"true" doc:"The size of the export file in bytes."`
|
||||
Created time.Time `json:"created" readOnly:"true" doc:"When the export was created."`
|
||||
Expires time.Time `json:"expires" readOnly:"true" doc:"When the export will be automatically deleted (7 days after creation)."`
|
||||
}
|
||||
|
||||
func RegisterOldExportCleanupCron() {
|
||||
const logPrefix = "[User Export Cleanup Cron] "
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue