Compare commits

..

11 Commits

Author SHA1 Message Date
kolaente ab038ec6c4 feat(audit): forward audit entries to stdout, syslog and webhook sinks
Adds the audit.forwarders config list and a Sink interface with three
implementations: stdout, RFC 5424 syslog over UDP/TCP (hand-rolled —
stdlib log/syslog only emits RFC 3164 and does not build on Windows),
and webhook POSTs through the SSRF-safe HTTP client. Forwarders are
best-effort fan-out: the local file stays the source of truth and a
dead sink is logged instead of poison-queueing every event.
2026-06-10 22:22:20 +02:00
kolaente ae908be716 fix: dispatch pending events after user creation commits
The register handler, local/LDAP login and the OIDC callback all queue
the user.created event via DispatchOnCommit but never called
DispatchPending, so the event was silently dropped and its queue entry
leaked. Flush after commit and discard on rollback.
2026-06-10 22:20:59 +02:00
kolaente 126ea78dac refactor(events): pass context to DispatchPending directly
Every DispatchPending caller either has the request context in scope or
is genuinely request-less, so passing it as a parameter replaces the
stored-context mechanism on the pending queue and satisfies
contextcheck. Also fixes lint findings in the audit package.
2026-06-10 22:19:04 +02:00
kolaente 3fc5813888 docs(audit): add package documentation 2026-06-10 22:18:50 +02:00
kolaente 3d8c259242 test(audit): cover listener pipeline, license gating and rotation 2026-06-10 22:18:11 +02:00
kolaente 6ab03d3f87 feat(audit): register the audited event surface
One config-gated block in RegisterListeners maps every opted-in event
to its audit entry. Events with interface-typed doers are decoded via
a small doer ref that distinguishes link shares by their hash field.
2026-06-10 22:18:11 +02:00
kolaente de22af0048 feat(events): add auth boundary events
LoginSucceededEvent fires from NewUserAuthTokenResponse (the chokepoint
where local, LDAP and OIDC logins converge), LoginFailedEvent from
handleFailedPassword on every failed password check, LogoutEvent from
the logout handler, and APIToken issued/revoked/used events from the
token model and auth middleware. The token events carry IDs only since
the freshly created token struct holds the raw token string and the
poison queue logs message payloads.

None of these events have a listener yet — the audit registration adds
them. Dispatching to a topic without subscribers is a no-op.
2026-06-10 22:18:11 +02:00
kolaente 4ff8181a47 feat(audit): wire request-meta middleware and writer initialization 2026-06-10 22:18:11 +02:00
kolaente 939daaf1ab feat(audit): add audit logging package
Entry schema with constructor-enforced actor/target types, a generic
RegisterEventForAudit helper that maps opted-in events to entries on
the existing watermill bus (license-gated per event since licenses are
runtime-mutable), and a JSONL writer with size-based rotation,
age-based cleanup of rotated files and batched fsync.
2026-06-10 22:18:11 +02:00
kolaente a4bbd02d6a feat(config): add audit logging config keys 2026-06-10 22:17:42 +02:00
kolaente 5db25ab75c feat(events): carry request metadata onto dispatched event messages
Adds a RequestMeta context bridge so events dispatched during an HTTP
request can be attributed to it: a middleware stashes IP/UA/request-id
on the request context, the generic Do* handlers associate that context
with the transaction key, and DispatchPending/DispatchWithContext copy
the metadata onto the watermill message at publish time. Existing
dispatch call sites are unchanged.
2026-06-10 21:00:41 +02:00
295 changed files with 6566 additions and 20341 deletions

3
.envrc Normal file
View File

@ -0,0 +1,3 @@
source_url "https://raw.githubusercontent.com/cachix/devenv/95f329d49a8a5289d31e0982652f7058a189bfca/direnvrc" "sha256-d+8cBpDfDBj41inrADaJt+bDWhOktwslgoP5YiGJ1v0="
use devenv

View File

@ -79,7 +79,7 @@ runs:
} >> "$GITHUB_ENV" } >> "$GITHUB_ENV"
- name: Download Mage binary - name: Download Mage binary
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with: with:
name: mage_bin name: mage_bin
@ -89,7 +89,7 @@ runs:
- name: Download frontend dist (vikunja only) - name: Download frontend dist (vikunja only)
if: inputs.project == 'vikunja' if: inputs.project == 'vikunja'
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with: with:
name: frontend_dist name: frontend_dist
path: frontend/dist path: frontend/dist
@ -110,7 +110,7 @@ runs:
sudo mv upx-5.0.0-amd64_linux/upx /usr/local/bin sudo mv upx-5.0.0-amd64_linux/upx /usr/local/bin
- name: Setup xgo cache - name: Setup xgo cache
uses: useblacksmith/cache@c5fe29eb0efdf1cf4186b9f7fcbbcbc0cf025662 # v5.1.0 uses: useblacksmith/cache@71c7c918062ba3861252d84b07fe5ab2a6b467a6 # v5
with: with:
path: /home/runner/.xgo-cache path: /home/runner/.xgo-cache
key: xgo-${{ inputs.project }}-${{ hashFiles('**/go.sum') }} key: xgo-${{ inputs.project }}-${{ hashFiles('**/go.sum') }}
@ -133,7 +133,7 @@ runs:
cd build && mage release:build "$PROJECT" cd build && mage release:build "$PROJECT"
- name: GPG setup - name: GPG setup
uses: kolaente/action-gpg@eb0fd8f16fe9b499f060f659092c470cb9f76eb7 # main uses: kolaente/action-gpg@main
with: with:
gpg-passphrase: ${{ inputs.gpg-passphrase }} gpg-passphrase: ${{ inputs.gpg-passphrase }}
gpg-sign-key: ${{ inputs.gpg-sign-key }} gpg-sign-key: ${{ inputs.gpg-sign-key }}
@ -164,7 +164,7 @@ runs:
done done
- name: Upload zips to S3 - name: Upload zips to S3
uses: kolaente/s3-action@7f58dddd682b2f93a6c6799c9f68e7a38f2da558 # main uses: kolaente/s3-action@main
with: with:
s3-access-key-id: ${{ inputs.s3-access-key-id }} s3-access-key-id: ${{ inputs.s3-access-key-id }}
s3-secret-access-key: ${{ inputs.s3-secret-access-key }} s3-secret-access-key: ${{ inputs.s3-secret-access-key }}
@ -176,14 +176,14 @@ runs:
strip-path-prefix: ${{ env.DIST_PREFIX }}/zip/ strip-path-prefix: ${{ env.DIST_PREFIX }}/zip/
- name: Store binaries - name: Store binaries
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with: with:
name: ${{ env.ARTIFACT_BINARIES_NAME }} name: ${{ env.ARTIFACT_BINARIES_NAME }}
path: ./${{ env.DIST_PREFIX }}/binaries/* path: ./${{ env.DIST_PREFIX }}/binaries/*
- name: Store binary packages - name: Store binary packages
if: github.ref_type == 'tag' if: github.ref_type == 'tag'
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with: with:
name: ${{ env.ARTIFACT_ZIPS_NAME }} name: ${{ env.ARTIFACT_ZIPS_NAME }}
path: ./${{ env.DIST_PREFIX }}/zip/* path: ./${{ env.DIST_PREFIX }}/zip/*

View File

@ -91,12 +91,12 @@ runs:
echo "S3_TARGET_PATH=/${PROJECT}/${VERSION_OR_UNSTABLE}" >> "$GITHUB_ENV" echo "S3_TARGET_PATH=/${PROJECT}/${VERSION_OR_UNSTABLE}" >> "$GITHUB_ENV"
- name: Download project binaries - name: Download project binaries
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with: with:
name: ${{ env.BINARIES_ARTIFACT_NAME }} name: ${{ env.BINARIES_ARTIFACT_NAME }}
path: ${{ env.BINARIES_DOWNLOAD_PATH }} path: ${{ env.BINARIES_DOWNLOAD_PATH }}
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
with: with:
go-version: stable go-version: stable
@ -123,7 +123,7 @@ runs:
- name: GPG setup for archlinux signing - name: GPG setup for archlinux signing
if: inputs.packager == 'archlinux' if: inputs.packager == 'archlinux'
uses: kolaente/action-gpg@eb0fd8f16fe9b499f060f659092c470cb9f76eb7 # main uses: kolaente/action-gpg@main
with: with:
gpg-passphrase: ${{ inputs.gpg-passphrase }} gpg-passphrase: ${{ inputs.gpg-passphrase }}
gpg-sign-key: ${{ inputs.gpg-sign-key }} gpg-sign-key: ${{ inputs.gpg-sign-key }}
@ -163,7 +163,7 @@ runs:
run: mkdir -p "$PACKAGE_OUTPUT_DIR" run: mkdir -p "$PACKAGE_OUTPUT_DIR"
- name: Create package - name: Create package
uses: kolaente/action-gh-nfpm@08460c16ce3baaa48eaf94d51eea0e653b15d955 # master uses: kolaente/action-gh-nfpm@master
with: with:
packager: ${{ inputs.packager }} packager: ${{ inputs.packager }}
target: ${{ env.PACKAGE_OUTPUT_DIR }}/${{ env.PACKAGE_FILENAME }} target: ${{ env.PACKAGE_OUTPUT_DIR }}/${{ env.PACKAGE_FILENAME }}
@ -186,7 +186,7 @@ runs:
"$PACKAGE_OUTPUT_DIR/$PACKAGE_FILENAME" "$PACKAGE_OUTPUT_DIR/$PACKAGE_FILENAME"
- name: Upload to S3 - name: Upload to S3
uses: kolaente/s3-action@7f58dddd682b2f93a6c6799c9f68e7a38f2da558 # main uses: kolaente/s3-action@main
with: with:
s3-access-key-id: ${{ inputs.s3-access-key-id }} s3-access-key-id: ${{ inputs.s3-access-key-id }}
s3-secret-access-key: ${{ inputs.s3-secret-access-key }} s3-secret-access-key: ${{ inputs.s3-secret-access-key }}
@ -198,7 +198,7 @@ runs:
strip-path-prefix: ${{ env.PACKAGE_OUTPUT_DIR }}/ strip-path-prefix: ${{ env.PACKAGE_OUTPUT_DIR }}/
- name: Store OS package - name: Store OS package
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with: with:
name: ${{ env.ARTIFACT_NAME }} name: ${{ env.ARTIFACT_NAME }}
path: ${{ env.PACKAGE_OUTPUT_DIR }}/* path: ${{ env.PACKAGE_OUTPUT_DIR }}/*

View File

@ -16,11 +16,11 @@ runs:
echo "CYPRESS_INSTALL_BINARY=0" >> $GITHUB_ENV echo "CYPRESS_INSTALL_BINARY=0" >> $GITHUB_ENV
echo "PUPPETEER_SKIP_DOWNLOAD=true" >> $GITHUB_ENV echo "PUPPETEER_SKIP_DOWNLOAD=true" >> $GITHUB_ENV
echo "PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1" >> $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: with:
run_install: false run_install: false
package_json_file: frontend/package.json package_json_file: frontend/package.json
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
with: with:
node-version-file: frontend/.nvmrc node-version-file: frontend/.nvmrc
cache: 'pnpm' cache: 'pnpm'

View File

@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout (for prompt template) - name: Checkout (for prompt template)
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
sparse-checkout: | sparse-checkout: |
.github/workflows/auto-label.prompt.md .github/workflows/auto-label.prompt.md
@ -29,7 +29,7 @@ jobs:
- name: Render system prompt from live labels - name: Render system prompt from live labels
id: render id: render
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env: env:
PROMPT_TEMPLATE_PATH: .github/workflows/auto-label.prompt.md PROMPT_TEMPLATE_PATH: .github/workflows/auto-label.prompt.md
with: with:
@ -122,7 +122,7 @@ jobs:
- name: Classify with AI - name: Classify with AI
id: classify id: classify
uses: actions/ai-inference@a7805884c80886efc241e94a5351df715968a0ad # v2.1.1 uses: actions/ai-inference@e09e65981758de8b2fdab13c2bfb7c7d5493b0b6 # v2.0.7
with: with:
model: openai/gpt-4.1-mini model: openai/gpt-4.1-mini
# GPT-5 is a reasoning model: output tokens include reasoning, so budget generously. # 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 }} prompt-file: ${{ steps.prep.outputs.prompt_path }}
- name: Apply labels - name: Apply labels
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env: env:
AI_RESPONSE: ${{ steps.classify.outputs.response }} AI_RESPONSE: ${{ steps.classify.outputs.response }}
with: with:

View File

@ -9,19 +9,19 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with: with:
ssh-key: ${{ secrets.SSH_PRIVATE_KEY }} ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}
persist-credentials: true persist-credentials: true
- name: push source files - name: push source files
uses: crowdin/github-action@52aa776766211d83d975df51f3b9c53c2f8ba35f # v2.16.3 uses: crowdin/github-action@b4b468cffefb50bdd99dd83e5d2eaeb63c880380 # v2
with: with:
command: 'push' command: 'push'
env: env:
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: pull translations - name: pull translations
uses: crowdin/github-action@52aa776766211d83d975df51f3b9c53c2f8ba35f # v2.16.3 uses: crowdin/github-action@b4b468cffefb50bdd99dd83e5d2eaeb63c880380 # v2
with: with:
command: 'download' command: 'download'
command_args: '--export-only-approved --skip-untranslated-strings' command_args: '--export-only-approved --skip-untranslated-strings'
@ -29,7 +29,7 @@ jobs:
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
with: with:
node-version-file: frontend/.nvmrc node-version-file: frontend/.nvmrc
- name: Ensure file permissions - name: Ensure file permissions
@ -55,7 +55,7 @@ jobs:
git commit -m "chore(i18n): update translations via Crowdin" git commit -m "chore(i18n): update translations via Crowdin"
- name: Push changes - name: Push changes
if: steps.check_changes.outputs.changes_exist != '0' if: steps.check_changes.outputs.changes_exist != '0'
uses: ad-m/github-push-action@881a6320fdb16eb5318c5054f31c218aec2b324c # master uses: ad-m/github-push-action@master
with: with:
ssh: true ssh: true
branch: ${{ github.ref }} branch: ${{ github.ref }}

View File

@ -18,11 +18,11 @@ jobs:
directory: [frontend, desktop] directory: [frontend, desktop]
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Create Diff - name: Create Diff
uses: e18e/action-dependency-diff@8e9b8c1957ab066d36235a43f4c1ff1522e1bdbc # v1.6.1 uses: e18e/action-dependency-diff@v1
with: with:
working-directory: ${{ matrix.directory }} working-directory: ${{ matrix.directory }}
@ -33,11 +33,11 @@ jobs:
directory: [frontend, desktop] directory: [frontend, desktop]
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Check provenance downgrades - name: Check provenance downgrades
uses: danielroe/provenance-action@81568f71211c1839d6d3583c6a93037f5348c816 # main uses: danielroe/provenance-action@main
with: with:
workspace-path: ${{ matrix.directory }} workspace-path: ${{ matrix.directory }}
fail-on-provenance-change: true fail-on-provenance-change: true

View File

@ -10,14 +10,14 @@ jobs:
steps: steps:
- name: Generate GitHub App token - name: Generate GitHub App token
id: generate-token id: generate-token
uses: actions/create-github-app-token@fee1f7d63c2ff003460e3d139729b119787bc349 # v2.2.2 uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
with: with:
app-id: ${{ secrets.BOT_APP_ID }} app-id: ${{ secrets.BOT_APP_ID }}
private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }}
- name: Find closing PR or commit - name: Find closing PR or commit
id: find-closer id: find-closer
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with: with:
github-token: ${{ steps.generate-token.outputs.token }} github-token: ${{ steps.generate-token.outputs.token }}
script: | script: |
@ -82,7 +82,7 @@ jobs:
- name: Comment on issue - name: Comment on issue
if: steps.find-closer.outputs.closed_by_code == 'true' if: steps.find-closer.outputs.closed_by_code == 'true'
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with: with:
github-token: ${{ steps.generate-token.outputs.token }} github-token: ${{ steps.generate-token.outputs.token }}
script: | script: |

View File

@ -25,7 +25,7 @@ jobs:
docker-images: false docker-images: false
swap-storage: false swap-storage: false
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with: with:
# For pull_request_target, we need to explicitly fetch the PR ref from forks # 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. # 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 ref: refs/pull/${{ github.event.pull_request.number }}/head
- name: Git describe - name: Git describe
id: ghd id: ghd
uses: proudust/gh-describe@80412be8ce0e77d8afba6b340e34790bc772aa45 # v2.2.0 uses: proudust/gh-describe@v2
- name: Login to GHCR - name: Login to GHCR
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
with: with:
version: latest version: latest
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5
with: with:
images: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }} images: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}
tags: | tags: |
type=ref,event=pr type=ref,event=pr
type=sha,format=long type=sha,format=long
- name: Build and push PR image - name: Build and push PR image
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
with: with:
context: . context: .
platforms: linux/amd64 platforms: linux/amd64
@ -66,7 +66,7 @@ jobs:
build-args: | build-args: |
RELEASE_VERSION=${{ steps.ghd.outputs.describe }} RELEASE_VERSION=${{ steps.ghd.outputs.describe }}
- name: Comment on PR - name: Comment on PR
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env: env:
DOCKER_META_TAGS: ${{ steps.meta.outputs.tags }} DOCKER_META_TAGS: ${{ steps.meta.outputs.tags }}
with: with:

View File

@ -8,14 +8,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: prepare-build-mage name: prepare-build-mage
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Set up Go - name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
with: with:
go-version: stable go-version: stable
- name: Cache build mage - name: Cache build mage
id: cache-build-mage id: cache-build-mage
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5
with: with:
key: ${{ runner.os }}-build-mage-build-${{ hashFiles('build/magefile.go') }} key: ${{ runner.os }}-build-mage-build-${{ hashFiles('build/magefile.go') }}
path: | path: |
@ -33,7 +33,7 @@ jobs:
export PATH=$PATH:$GOPATH/bin export PATH=$PATH:$GOPATH/bin
mage -compile ./build-mage-static mage -compile ./build-mage-static
- name: Store build mage binary - name: Store build mage binary
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with: with:
name: build_mage_bin name: build_mage_bin
path: ./build/build-mage-static path: ./build/build-mage-static
@ -43,14 +43,14 @@ jobs:
steps: steps:
- name: Git describe - name: Git describe
id: ghd id: ghd
uses: proudust/gh-describe@80412be8ce0e77d8afba6b340e34790bc772aa45 # v2.2.0 uses: proudust/gh-describe@v2
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
with: with:
username: ${{ secrets.DOCKER_HUB_USERNAME }} username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }} password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- name: Login to GHCR - name: Login to GHCR
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@ -58,7 +58,7 @@ jobs:
- name: Docker meta version - name: Docker meta version
if: ${{ github.ref_type == 'tag' }} if: ${{ github.ref_type == 'tag' }}
id: meta id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5
with: with:
images: | images: |
vikunja/vikunja vikunja/vikunja
@ -70,7 +70,7 @@ jobs:
type=raw,value=latest type=raw,value=latest
- name: Build and push unstable - name: Build and push unstable
if: ${{ github.ref_type != 'tag' }} if: ${{ github.ref_type != 'tag' }}
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
with: with:
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8 platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
push: true push: true
@ -81,7 +81,7 @@ jobs:
RELEASE_VERSION=${{ steps.ghd.outputs.describe }} RELEASE_VERSION=${{ steps.ghd.outputs.describe }}
- name: Build and push version - name: Build and push version
if: ${{ github.ref_type == 'tag' }} if: ${{ github.ref_type == 'tag' }}
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
with: with:
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8 platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
push: true push: true
@ -93,10 +93,10 @@ jobs:
binaries: binaries:
runs-on: blacksmith-8vcpu-ubuntu-2204 runs-on: blacksmith-8vcpu-ubuntu-2204
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Git describe - name: Git describe
id: ghd id: ghd
uses: proudust/gh-describe@80412be8ce0e77d8afba6b340e34790bc772aa45 # v2.2.0 uses: proudust/gh-describe@v2
- uses: ./.github/actions/release-binaries - uses: ./.github/actions/release-binaries
with: with:
project: vikunja project: vikunja
@ -112,10 +112,10 @@ jobs:
veans-binaries: veans-binaries:
runs-on: blacksmith-8vcpu-ubuntu-2204 runs-on: blacksmith-8vcpu-ubuntu-2204
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Git describe - name: Git describe
id: ghd id: ghd
uses: proudust/gh-describe@80412be8ce0e77d8afba6b340e34790bc772aa45 # v2.2.0 uses: proudust/gh-describe@v2
- uses: ./.github/actions/release-binaries - uses: ./.github/actions/release-binaries
with: with:
project: veans project: veans
@ -147,10 +147,10 @@ jobs:
pkg: armv7 pkg: armv7
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Git describe - name: Git describe
id: ghd id: ghd
uses: proudust/gh-describe@80412be8ce0e77d8afba6b340e34790bc772aa45 # v2.2.0 uses: proudust/gh-describe@v2
- uses: ./.github/actions/release-os-package - uses: ./.github/actions/release-os-package
with: with:
project: vikunja project: vikunja
@ -186,10 +186,10 @@ jobs:
pkg: armv7 pkg: armv7
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Git describe - name: Git describe
id: ghd id: ghd
uses: proudust/gh-describe@80412be8ce0e77d8afba6b340e34790bc772aa45 # v2.2.0 uses: proudust/gh-describe@v2
- uses: ./.github/actions/release-os-package - uses: ./.github/actions/release-os-package
with: with:
project: veans project: veans
@ -235,19 +235,19 @@ jobs:
REPO_SUITE: ${{ github.ref_type == 'tag' && 'stable' || 'unstable' }} REPO_SUITE: ${{ github.ref_type == 'tag' && 'stable' || 'unstable' }}
RELEASE_VERSION: unstable RELEASE_VERSION: unstable
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Download build mage binary - name: Download build mage binary
# Statically compiled in test.yml's build-mage job so it runs inside # Statically compiled in test.yml's build-mage job so it runs inside
# ubuntu/fedora/archlinux containers without a Go toolchain. # ubuntu/fedora/archlinux containers without a Go toolchain.
if: matrix.format != 'apk' if: matrix.format != 'apk'
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with: with:
name: build_mage_bin name: build_mage_bin
path: build path: build
- name: Download all server OS packages - name: Download all server OS packages
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with: with:
pattern: vikunja_os_package_* pattern: vikunja_os_package_*
merge-multiple: true merge-multiple: true
@ -257,14 +257,14 @@ jobs:
# Merged into the same incoming dir so reprepro / createrepo_c / # Merged into the same incoming dir so reprepro / createrepo_c /
# repo-add / the apk loop pick them up alongside vikunja's packages # repo-add / the apk loop pick them up alongside vikunja's packages
# — same suite, same arch fan-out, no extra source entry for users. # — 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: with:
pattern: veans_os_package_* pattern: veans_os_package_*
merge-multiple: true merge-multiple: true
path: dist/repo-work/incoming path: dist/repo-work/incoming
- name: Download desktop packages (Linux) - name: Download desktop packages (Linux)
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with: with:
name: vikunja_desktop_packages_ubuntu-latest name: vikunja_desktop_packages_ubuntu-latest
path: dist/repo-work/incoming-desktop path: dist/repo-work/incoming-desktop
@ -309,7 +309,7 @@ jobs:
- name: GPG setup - name: GPG setup
if: matrix.format != 'apk' if: matrix.format != 'apk'
uses: kolaente/action-gpg@eb0fd8f16fe9b499f060f659092c470cb9f76eb7 # main uses: kolaente/action-gpg@main
with: with:
gpg-passphrase: "${{ secrets.RELEASE_GPG_PASSPHRASE }}" gpg-passphrase: "${{ secrets.RELEASE_GPG_PASSPHRASE }}"
gpg-sign-key: "${{ secrets.RELEASE_GPG_SIGN_KEY }}" 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 find dist/repo-output -type d -empty -delete 2>/dev/null || true
- name: Upload to R2 - name: Upload to R2
uses: kolaente/s3-action@7f58dddd682b2f93a6c6799c9f68e7a38f2da558 # main uses: kolaente/s3-action@main
with: with:
s3-access-key-id: ${{ secrets.S3_ACCESS_KEY }} s3-access-key-id: ${{ secrets.S3_ACCESS_KEY }}
s3-secret-access-key: ${{ secrets.S3_SECRET_KEY }} s3-secret-access-key: ${{ secrets.S3_SECRET_KEY }}
@ -398,12 +398,12 @@ jobs:
config-yaml: config-yaml:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Git describe - name: Git describe
id: ghd id: ghd
uses: proudust/gh-describe@80412be8ce0e77d8afba6b340e34790bc772aa45 # v2.2.0 uses: proudust/gh-describe@v2
- name: Download Mage Binary - name: Download Mage Binary
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with: with:
name: mage_bin name: mage_bin
- name: generate - name: generate
@ -411,7 +411,7 @@ jobs:
chmod +x ./mage-static chmod +x ./mage-static
./mage-static generate:config-yaml 1 ./mage-static generate:config-yaml 1
- name: Upload to S3 - name: Upload to S3
uses: kolaente/s3-action@7f58dddd682b2f93a6c6799c9f68e7a38f2da558 # main uses: kolaente/s3-action@main
with: with:
s3-access-key-id: ${{ secrets.S3_ACCESS_KEY }} s3-access-key-id: ${{ secrets.S3_ACCESS_KEY }}
s3-secret-access-key: ${{ secrets.S3_SECRET_KEY }} s3-secret-access-key: ${{ secrets.S3_SECRET_KEY }}
@ -431,16 +431,16 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Git describe - name: Git describe
id: ghd id: ghd
uses: proudust/gh-describe@80412be8ce0e77d8afba6b340e34790bc772aa45 # v2.2.0 uses: proudust/gh-describe@v2
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0 uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
with: with:
package_json_file: desktop/package.json package_json_file: desktop/package.json
- name: Setup Node - name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
with: with:
node-version-file: frontend/.nvmrc node-version-file: frontend/.nvmrc
cache: pnpm cache: pnpm
@ -451,7 +451,7 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get install --no-install-recommends -y libopenjp2-tools rpm libarchive-tools sudo apt-get install --no-install-recommends -y libopenjp2-tools rpm libarchive-tools
- name: get frontend - name: get frontend
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with: with:
name: frontend_dist name: frontend_dist
path: frontend/dist path: frontend/dist
@ -461,7 +461,7 @@ jobs:
pnpm install --frozen-lockfile --prefer-offline --fetch-timeout 100000 pnpm install --frozen-lockfile --prefer-offline --fetch-timeout 100000
node build.js "${{ steps.ghd.outputs.describe }}" ${{ github.ref_type == 'tag' }} node build.js "${{ steps.ghd.outputs.describe }}" ${{ github.ref_type == 'tag' }}
- name: Upload to S3 - name: Upload to S3
uses: kolaente/s3-action@7f58dddd682b2f93a6c6799c9f68e7a38f2da558 # main uses: kolaente/s3-action@main
with: with:
s3-access-key-id: ${{ secrets.S3_ACCESS_KEY }} s3-access-key-id: ${{ secrets.S3_ACCESS_KEY }}
s3-secret-access-key: ${{ secrets.S3_SECRET_KEY }} s3-secret-access-key: ${{ secrets.S3_SECRET_KEY }}
@ -473,7 +473,7 @@ jobs:
strip-path-prefix: desktop/dist/ strip-path-prefix: desktop/dist/
exclude: "desktop/dist/*.blockmap" exclude: "desktop/dist/*.blockmap"
- name: Store Desktop Package - name: Store Desktop Package
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with: with:
name: vikunja_desktop_packages_${{ matrix.os }} name: vikunja_desktop_packages_${{ matrix.os }}
path: | path: |
@ -486,16 +486,16 @@ jobs:
contents: write contents: write
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with: with:
ssh-key: ${{ secrets.SSH_PRIVATE_KEY }} ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}
persist-credentials: true persist-credentials: true
- name: Download Mage Binary - name: Download Mage Binary
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with: with:
name: mage_bin name: mage_bin
- name: Set up Go - name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
with: with:
go-version: stable go-version: stable
- name: generate - name: generate
@ -520,7 +520,7 @@ jobs:
git commit -am "[skip ci] Updated swagger docs" git commit -am "[skip ci] Updated swagger docs"
- name: Push changes - name: Push changes
if: steps.check_changes.outputs.changes_exist != '0' if: steps.check_changes.outputs.changes_exist != '0'
uses: ad-m/github-push-action@881a6320fdb16eb5318c5054f31c218aec2b324c # master uses: ad-m/github-push-action@master
with: with:
ssh: true ssh: true
branch: ${{ github.ref }} branch: ${{ github.ref }}
@ -539,44 +539,44 @@ jobs:
contents: write contents: write
steps: steps:
- name: Download Binaries - name: Download Binaries
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with: with:
name: vikunja_bin_packages name: vikunja_bin_packages
- name: Download OS Packages - name: Download OS Packages
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with: with:
pattern: vikunja_os_package_* pattern: vikunja_os_package_*
merge-multiple: true merge-multiple: true
- name: Download Veans Binaries - name: Download Veans Binaries
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with: with:
name: veans_bin_packages name: veans_bin_packages
- name: Download Veans OS Packages - name: Download Veans OS Packages
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with: with:
pattern: veans_os_package_* pattern: veans_os_package_*
merge-multiple: true merge-multiple: true
- name: Download Desktop Package Linux - name: Download Desktop Package Linux
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with: with:
name: vikunja_desktop_packages_ubuntu-latest name: vikunja_desktop_packages_ubuntu-latest
- name: Download Desktop Package MacOS - name: Download Desktop Package MacOS
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with: with:
name: vikunja_desktop_packages_macos-latest name: vikunja_desktop_packages_macos-latest
- name: Download Desktop Package Windows - name: Download Desktop Package Windows
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with: with:
name: vikunja_desktop_packages_windows-latest name: vikunja_desktop_packages_windows-latest
- name: Release - name: Release
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2 uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2
if: github.ref_type == 'tag' if: github.ref_type == 'tag'
with: with:
draft: true draft: true

View File

@ -12,7 +12,7 @@ jobs:
stale: stale:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 - uses: actions/stale@v9
with: with:
only-labels: 'waiting for reply' only-labels: 'waiting for reply'
days-before-issue-stale: 30 days-before-issue-stale: 30
@ -24,7 +24,6 @@ jobs:
questions. If you're still seeing this on a recent version, just questions. If you're still seeing this on a recent version, just
drop a comment with the requested info and we'll reopen. Thanks drop a comment with the requested info and we'll reopen. Thanks
for the report! for the report!
stale-pr-label: 'waiting for reply' days-before-pr-stale: -1
days-before-pr-stale: 30
days-before-pr-close: -1 days-before-pr-close: -1
operations-per-run: 100 operations-per-run: 100

View File

@ -8,26 +8,26 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: prepare-mage name: prepare-mage
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Set up Go - name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
with: with:
go-version: stable go-version: stable
- name: Cache Mage - name: Cache Mage
id: cache-mage id: cache-mage
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5
with: with:
key: ${{ runner.os }}-build-mage-${{ hashFiles('magefile.go') }} key: ${{ runner.os }}-build-mage-${{ hashFiles('magefile.go') }}
path: | path: |
./mage-static ./mage-static
- name: Compile Mage - name: Compile Mage
if: ${{ steps.cache-mage.outputs.cache-hit != 'true' }} if: ${{ steps.cache-mage.outputs.cache-hit != 'true' }}
uses: magefile/mage-action@6f50bbb8ea47d56e62dee92392788acbc8192d0b # v3.1.0 uses: magefile/mage-action@6f50bbb8ea47d56e62dee92392788acbc8192d0b # v3
with: with:
version: latest version: latest
args: -compile ./mage-static args: -compile ./mage-static
- name: Store Mage Binary - name: Store Mage Binary
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with: with:
name: mage_bin name: mage_bin
path: ./mage-static path: ./mage-static
@ -36,16 +36,16 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: mage needs: mage
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Download Mage Binary - name: Download Mage Binary
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with: with:
name: mage_bin name: mage_bin
- name: Git describe - name: Git describe
id: ghd id: ghd
uses: proudust/gh-describe@80412be8ce0e77d8afba6b340e34790bc772aa45 # v2.2.0 uses: proudust/gh-describe@v2
- name: Set up Go - name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
with: with:
go-version: stable go-version: stable
- name: Build - name: Build
@ -57,7 +57,7 @@ jobs:
chmod +x ./mage-static chmod +x ./mage-static
./mage-static build ./mage-static build
- name: Store Vikunja Binary - name: Store Vikunja Binary
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with: with:
name: vikunja_bin name: vikunja_bin
path: ./vikunja path: ./vikunja
@ -65,8 +65,8 @@ jobs:
api-lint: api-lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
with: with:
go-version: stable go-version: stable
- name: prepare frontend files - name: prepare frontend files
@ -74,19 +74,19 @@ jobs:
mkdir -p frontend/dist mkdir -p frontend/dist
touch frontend/dist/index.html touch frontend/dist/index.html
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0 uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9
with: with:
version: v2.10.1 version: v2.10.1
veans-lint: veans-lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
with: with:
go-version: stable go-version: stable
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0 uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9
with: with:
version: v2.10.1 version: v2.10.1
working-directory: veans working-directory: veans
@ -94,8 +94,8 @@ jobs:
veans-test: veans-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
with: with:
go-version: stable go-version: stable
- name: Install mage - name: Install mage
@ -115,9 +115,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: mage needs: mage
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Download Mage Binary - name: Download Mage Binary
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with: with:
name: mage_bin name: mage_bin
- name: Check - name: Check
@ -152,7 +152,7 @@ jobs:
ports: ports:
- 3306:3306 - 3306:3306
migration-smoke-db-postgres: migration-smoke-db-postgres:
image: postgres:18@sha256:4aabea78cf39b90e834caf3af7d602a18565f6fe2508705c8d01aa63245c2e20 image: postgres:18@sha256:5773fe724c49c42a7a9ca70202e11e1dff21fb7235b335a73f39297d200b73a2
env: env:
POSTGRES_PASSWORD: vikunjatest POSTGRES_PASSWORD: vikunjatest
POSTGRES_DB: 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 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 unzip vikunja-latest.zip vikunja-unstable-linux-amd64
- name: Download Vikunja Binary - name: Download Vikunja Binary
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with: with:
name: vikunja_bin name: vikunja_bin
- name: run migration - name: run migration
@ -254,13 +254,13 @@ jobs:
ports: ports:
- 389:389 - 389:389
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Download Mage Binary - name: Download Mage Binary
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with: with:
name: mage_bin name: mage_bin
- name: Set up Go - name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
with: with:
go-version: stable go-version: stable
- name: Configure Postgres for faster tests - name: Configure Postgres for faster tests
@ -300,13 +300,13 @@ jobs:
needs: needs:
- mage - mage
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Download Mage Binary - name: Download Mage Binary
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with: with:
name: mage_bin name: mage_bin
- name: Set up Go - name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
with: with:
go-version: stable go-version: stable
- name: test - name: test
@ -321,13 +321,13 @@ jobs:
needs: needs:
- mage - mage
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Download Mage Binary - name: Download Mage Binary
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with: with:
name: mage_bin name: mage_bin
- name: Set up Go - name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
with: with:
go-version: stable go-version: stable
- name: test - name: test
@ -351,13 +351,13 @@ jobs:
ports: ports:
- 9000:9000 - 9000:9000
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Download Mage Binary - name: Download Mage Binary
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with: with:
name: mage_bin name: mage_bin
- name: Set up Go - name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
with: with:
go-version: stable go-version: stable
- name: test S3 file storage integration - name: test S3 file storage integration
@ -382,7 +382,7 @@ jobs:
frontend-lint: frontend-lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- uses: ./.github/actions/setup-frontend - uses: ./.github/actions/setup-frontend
- name: Lint - name: Lint
working-directory: frontend working-directory: frontend
@ -391,7 +391,7 @@ jobs:
frontend-stylelint: frontend-stylelint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- uses: ./.github/actions/setup-frontend - uses: ./.github/actions/setup-frontend
- name: Lint styles - name: Lint styles
working-directory: frontend working-directory: frontend
@ -400,7 +400,7 @@ jobs:
frontend-typecheck: frontend-typecheck:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- uses: ./.github/actions/setup-frontend - uses: ./.github/actions/setup-frontend
- name: Typecheck - name: Typecheck
continue-on-error: true continue-on-error: true
@ -410,7 +410,7 @@ jobs:
test-frontend-unit: test-frontend-unit:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- uses: ./.github/actions/setup-frontend - uses: ./.github/actions/setup-frontend
- name: Run unit tests - name: Run unit tests
working-directory: frontend working-directory: frontend
@ -419,11 +419,11 @@ jobs:
frontend-build: frontend-build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- uses: ./.github/actions/setup-frontend - uses: ./.github/actions/setup-frontend
- name: Git describe - name: Git describe
id: ghd id: ghd
uses: proudust/gh-describe@80412be8ce0e77d8afba6b340e34790bc772aa45 # v2.2.0 uses: proudust/gh-describe@v2
- name: Inject frontend version - name: Inject frontend version
working-directory: frontend working-directory: frontend
run: | run: |
@ -432,7 +432,7 @@ jobs:
working-directory: frontend working-directory: frontend
run: pnpm build run: pnpm build
- name: Store Frontend - name: Store Frontend
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with: with:
name: frontend_dist name: frontend_dist
path: ./frontend/dist path: ./frontend/dist
@ -442,13 +442,13 @@ jobs:
needs: needs:
- api-build - api-build
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Download Vikunja Binary - name: Download Vikunja Binary
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with: with:
name: vikunja_bin name: vikunja_bin
- name: Set up Go - name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6
with: with:
go-version: stable go-version: stable
- name: Install mage - name: Install mage
@ -501,7 +501,7 @@ jobs:
(cd veans && mage test:e2e) (cd veans && mage test:e2e)
- name: Upload API log on failure - name: Upload API log on failure
if: failure() if: failure()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with: with:
name: veans-e2e-vikunja-log name: veans-e2e-vikunja-log
path: /tmp/vikunja.log path: /tmp/vikunja.log
@ -523,19 +523,19 @@ jobs:
ports: ports:
- 5556:5556 - 5556:5556
container: 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 options: --user 1001
steps: steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
- name: Download Vikunja Binary - name: Download Vikunja Binary
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with: with:
name: vikunja_bin name: vikunja_bin
- uses: ./.github/actions/setup-frontend - uses: ./.github/actions/setup-frontend
with: with:
install-e2e-binaries: false # Playwright browsers already in container install-e2e-binaries: false # Playwright browsers already in container
- name: Download Frontend - name: Download Frontend
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with: with:
name: frontend_dist name: frontend_dist
path: ./frontend/dist path: ./frontend/dist
@ -570,14 +570,14 @@ jobs:
VIKUNJA_AUTH_OPENID_PROVIDERS_DEX_CLIENTID: vikunja VIKUNJA_AUTH_OPENID_PROVIDERS_DEX_CLIENTID: vikunja
VIKUNJA_AUTH_OPENID_PROVIDERS_DEX_CLIENTSECRET: secret VIKUNJA_AUTH_OPENID_PROVIDERS_DEX_CLIENTSECRET: secret
- name: Upload Playwright Report - name: Upload Playwright Report
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
if: always() if: always()
with: with:
name: playwright-report-${{ matrix.shard }} name: playwright-report-${{ matrix.shard }}
path: frontend/playwright-report/ path: frontend/playwright-report/
retention-days: 30 retention-days: 30
- name: Upload Test Results - name: Upload Test Results
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
if: always() if: always()
with: with:
name: playwright-test-results-${{ matrix.shard }} name: playwright-test-results-${{ matrix.shard }}

View File

@ -145,13 +145,6 @@ linters:
- revive - revive
path: pkg/utils/* path: pkg/utils/*
text: 'var-naming: avoid meaningless package names' 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: - linters:
- revive - revive
text: 'var-naming: avoid package names that conflict with Go standard library package names' text: 'var-naming: avoid package names that conflict with Go standard library package names'

1
CRUSH.md Symbolic link
View File

@ -0,0 +1 @@
AGENTS.md

View File

@ -1,5 +1,5 @@
# syntax=docker/dockerfile:1@sha256:87999aa3d42bdc6bea60565083ee17e86d1f3339802f543c0d03998580f9cb89 # syntax=docker/dockerfile:1@sha256:b6afd42430b15f2d2a4c5a02b919e98a525b785b1aaff16747d2f623364e39b6
FROM --platform=$BUILDPLATFORM node:24.18.0-alpine@sha256:a0b9bf06e4e6193cf7a0f58816cc935ff8c2a908f81e6f1a95432d679c54fbfd AS frontendbuilder FROM --platform=$BUILDPLATFORM node:24.13.0-alpine@sha256:931d7d57f8c1fd0e2179dbff7cc7da4c9dd100998bc2b32afc85142d8efbc213 AS frontendbuilder
WORKDIR /build WORKDIR /build
@ -14,7 +14,7 @@ COPY frontend/ ./
ARG RELEASE_VERSION=dev ARG RELEASE_VERSION=dev
RUN echo "{\"VERSION\": \"${RELEASE_VERSION/-g/-}\"}" > src/version.json && pnpm run build 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 && \ RUN go install github.com/magefile/mage@latest && \
mv /go/bin/mage /usr/local/go/bin mv /go/bin/mage /usr/local/go/bin

View File

@ -1,5 +1,5 @@
module code.vikunja.io/build module code.vikunja.io/build
go 1.26.4 go 1.25.0
require github.com/magefile/mage v1.17.2 require github.com/magefile/mage v1.17.2

View File

@ -849,11 +849,6 @@
"default_value": "(&(objectclass=*)(|(objectclass=group)(objectclass=groupOfNames)))", "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`." "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", "key": "avatarsyncattribute",
"default_value": "", "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." "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```"
} }
] ]
}, },

View File

@ -100,15 +100,10 @@ app.on('second-instance', (_event, argv) => {
return return
} }
// Reveal the main window. It may be hidden in the tray (not just minimized), // Focus the main window
// 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.
if (mainWindow) { if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore() if (mainWindow.isMinimized()) mainWindow.restore()
mainWindow.show()
mainWindow.focus() mainWindow.focus()
} else if (serverPort) {
createMainWindow()
} }
// Find the deep link URL in argv // Find the deep link URL in argv
@ -241,11 +236,6 @@ function createMainWindow() {
mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
width: 1680, width: 1680,
height: 960, 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: { webPreferences: {
...BASE_WEB_PREFERENCES, ...BASE_WEB_PREFERENCES,
preload: path.join(__dirname, 'preload.js'), preload: path.join(__dirname, 'preload.js'),
@ -553,14 +543,3 @@ app.on('window-all-closed', () => {
app.quit() 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()
})
}

View File

@ -5,7 +5,7 @@
"main": "main.js", "main": "main.js",
"repository": "https://code.vikunja.io/desktop", "repository": "https://code.vikunja.io/desktop",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"packageManager": "pnpm@10.34.4", "packageManager": "pnpm@10.28.1",
"author": { "author": {
"email": "maintainers@vikunja.io", "email": "maintainers@vikunja.io",
"name": "Vikunja Team" "name": "Vikunja Team"
@ -61,9 +61,9 @@
} }
}, },
"devDependencies": { "devDependencies": {
"electron": "40.10.5", "electron": "40.10.3",
"electron-builder": "26.15.3", "electron-builder": "26.15.2",
"unzipper": "0.12.5" "unzipper": "0.12.3"
}, },
"dependencies": { "dependencies": {
"express": "5.2.1" "express": "5.2.1"
@ -73,16 +73,12 @@
"electron" "electron"
], ],
"overrides": { "overrides": {
"minimatch": "10.2.5", "minimatch": "^10.2.3",
"tar": "7.5.17", "tar": "^7.5.11",
"@tootallnate/once": "3.0.1", "@tootallnate/once": "^3.0.1",
"picomatch": "4.0.4", "picomatch": ">=4.0.4",
"tmp": "0.2.7", "tmp": ">=0.2.6",
"ip-address": "10.2.0", "ip-address": ">=10.1.1"
"form-data": "4.0.6",
"js-yaml": "5.2.0",
"undici@6": "6.27.0",
"undici@7": "7.28.0"
} }
} }
} }

View File

@ -5,16 +5,12 @@ settings:
excludeLinksFromLockfile: false excludeLinksFromLockfile: false
overrides: overrides:
minimatch: 10.2.5 minimatch: ^10.2.3
tar: 7.5.17 tar: ^7.5.11
'@tootallnate/once': 3.0.1 '@tootallnate/once': ^3.0.1
picomatch: 4.0.4 picomatch: '>=4.0.4'
tmp: 0.2.7 tmp: '>=0.2.6'
ip-address: 10.2.0 ip-address: '>=10.1.1'
form-data: 4.0.6
js-yaml: 5.2.0
undici@6: 6.27.0
undici@7: 7.28.0
importers: importers:
@ -25,14 +21,14 @@ importers:
version: 5.2.1 version: 5.2.1
devDependencies: devDependencies:
electron: electron:
specifier: 40.10.5 specifier: 40.10.3
version: 40.10.5 version: 40.10.3
electron-builder: electron-builder:
specifier: 26.15.3 specifier: 26.15.2
version: 26.15.3(electron-builder-squirrel-windows@24.13.3) version: 26.15.2(electron-builder-squirrel-windows@24.13.3)
unzipper: unzipper:
specifier: 0.12.5 specifier: 0.12.3
version: 0.12.5 version: 0.12.3
packages: packages:
@ -238,12 +234,12 @@ packages:
dmg-builder: 24.13.3 dmg-builder: 24.13.3
electron-builder-squirrel-windows: 24.13.3 electron-builder-squirrel-windows: 24.13.3
app-builder-lib@26.15.3: app-builder-lib@26.15.2:
resolution: {integrity: sha512-2VnyWkqsP5v5XbBhL3tD5Syx8iNPBYsoU7kY4S2fz7wg8Rj/nztWKCUzGKaFRTv0Xwf3/H058CR1Kvtd/3lRow==} resolution: {integrity: sha512-3mYfKOjr/ZY7gFESOcq8kylBMgGPpmlQYnpBVit4p6zIg0t/8bkWBILdMMtnjFyN2jllyBf225T8dLlz3D6oBQ==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
peerDependencies: peerDependencies:
dmg-builder: 26.15.3 dmg-builder: 26.15.2
electron-builder-squirrel-windows: 26.15.3 electron-builder-squirrel-windows: 26.15.2
archiver-utils@2.1.0: archiver-utils@2.1.0:
resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==}
@ -333,8 +329,8 @@ packages:
builder-util@24.13.1: builder-util@24.13.1:
resolution: {integrity: sha512-NhbCSIntruNDTOVI9fdXz0dihaqX2YuE1D6zZMrwiErzH4ELZHE6mdiB40wEgZNprDia+FghRFgKoAqMZRRjSA==} resolution: {integrity: sha512-NhbCSIntruNDTOVI9fdXz0dihaqX2YuE1D6zZMrwiErzH4ELZHE6mdiB40wEgZNprDia+FghRFgKoAqMZRRjSA==}
builder-util@26.15.3: builder-util@26.15.0:
resolution: {integrity: sha512-q2hn7Mbo2nFNkVekPiHFx6Nfo3hURmES3tfBn+k5Pqxl2RkmP3QGqZUhH/q9Pch/4G05NRhPjDlVj1O8q4Txvw==} resolution: {integrity: sha512-dUx+HxVbiNsNQ4mGe1PyoC/tBmsHwBNDLdBuqWCj+rhHFE9lHgrXiGYKAM1uNlznhAaUSyMlms84VeSSr3gOBA==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
bytes@3.1.2: bytes@3.1.2:
@ -487,8 +483,8 @@ packages:
dir-compare@4.2.0: dir-compare@4.2.0:
resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==} resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==}
dmg-builder@26.15.3: dmg-builder@26.15.2:
resolution: {integrity: sha512-O3zJUFUYHJKgzPqioHxfxzBzlSC1eXCSr79gMSBKBP5AgjjpmrydMsMLotEg9fAJF36vdUncb+4ndRNxoPdlSQ==} resolution: {integrity: sha512-fMkjRqKyPtsz4Kzu/qGP0BGjqzMCIgp+/7kw/u6YH6lvn/8hvL3c0TXhoFayBoYdpPCnEinnCHztd4bW7/jetA==}
dotenv-expand@11.0.6: dotenv-expand@11.0.6:
resolution: {integrity: sha512-8NHi73otpWsZGBSZwwknTXS5pqMOrk9+Ssrna8xCaxkzEpU9OTf9R5ArQGVw03//Zmk9MOwLPng9WwndvpAJ5g==} resolution: {integrity: sha512-8NHi73otpWsZGBSZwwknTXS5pqMOrk9+Ssrna8xCaxkzEpU9OTf9R5ArQGVw03//Zmk9MOwLPng9WwndvpAJ5g==}
@ -526,19 +522,19 @@ packages:
electron-builder-squirrel-windows@24.13.3: electron-builder-squirrel-windows@24.13.3:
resolution: {integrity: sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==} resolution: {integrity: sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==}
electron-builder@26.15.3: electron-builder@26.15.2:
resolution: {integrity: sha512-a1KM5heqS3gQCZzizXEI8RjJy3QVogULPdeSknt76uLDpBIW/HDGsMg/XgP0riP6PI9COsRvFITKKGDqA8fJxA==} resolution: {integrity: sha512-veKM9+dCljaC5A74Pwc0ZWQ9arOHREXWh9hUIf8NGg49ch7x+IB4QhbMzIrV5ONZIXM2OEkaxW11cAPjPtoi4A==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
hasBin: true hasBin: true
electron-publish@24.13.1: electron-publish@24.13.1:
resolution: {integrity: sha512-2ZgdEqJ8e9D17Hwp5LEq5mLQPjqU3lv/IALvgp+4W8VeNhryfGhYEQC/PgDPMrnWUp+l60Ou5SJLsu+k4mhQ8A==} resolution: {integrity: sha512-2ZgdEqJ8e9D17Hwp5LEq5mLQPjqU3lv/IALvgp+4W8VeNhryfGhYEQC/PgDPMrnWUp+l60Ou5SJLsu+k4mhQ8A==}
electron-publish@26.15.3: electron-publish@26.15.1:
resolution: {integrity: sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q==} resolution: {integrity: sha512-BMgMHOyexWn0UnOC+Afffw0DMrr0yfLp4U8YsLXwoJ3Da7LS7WUnz21teYZqO0gaApE1KgsjREWmbPqvF5JcPg==}
electron@40.10.5: electron@40.10.3:
resolution: {integrity: sha512-VzTIvwOYXZZufT9B83GDQogR1TFqREygRYhm0LE++QhGPjvBeg+W7siOP9K5+9rHMUnRuCX4YU/0ivLekN/UZQ==} resolution: {integrity: sha512-DdWRsHm4j5wH9TMcfnB2Dqx44G/6BgLKSG/oeRe9kS60pfqCUwzUkHk0ClwvZzBVXtJ1kcdkHVRrJsl1ooKp+g==}
engines: {node: '>= 22.12.0'} engines: {node: '>= 22.12.0'}
hasBin: true hasBin: true
@ -620,7 +616,7 @@ packages:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
peerDependencies: peerDependencies:
picomatch: 4.0.4 picomatch: '>=4.0.4'
peerDependenciesMeta: peerDependenciesMeta:
picomatch: picomatch:
optional: true optional: true
@ -636,8 +632,8 @@ packages:
resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
engines: {node: '>=14'} engines: {node: '>=14'}
form-data@4.0.6: form-data@4.0.5:
resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
forwarded@0.2.0: forwarded@0.2.0:
@ -655,6 +651,10 @@ packages:
resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
fs-extra@11.2.0:
resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==}
engines: {node: '>=14.14'}
fs-extra@11.3.1: fs-extra@11.3.1:
resolution: {integrity: sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==} resolution: {integrity: sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==}
engines: {node: '>=14.14'} engines: {node: '>=14.14'}
@ -736,10 +736,6 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'} 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: hosted-git-info@4.1.0:
resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -834,8 +830,8 @@ packages:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true hasBin: true
js-yaml@5.2.0: js-yaml@4.1.1:
resolution: {integrity: sha512-YeLUMlvR4Ou1B119LIaM0r65JvbOBooJDc9yEu0dClb/uSC5P4FrLU8OCCz/HXWvtPoIrR0dRzABTjo1sTN9Bw==} resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
hasBin: true hasBin: true
json-buffer@3.0.1: json-buffer@3.0.1:
@ -858,6 +854,9 @@ packages:
jsonfile@4.0.0: jsonfile@4.0.0:
resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
jsonfile@6.1.0:
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
jsonfile@6.2.0: jsonfile@6.2.0:
resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
@ -1299,8 +1298,8 @@ packages:
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
tar@7.5.17: tar@7.5.15:
resolution: {integrity: sha512-wPEBwzapC+2PaTYPH6e2L+cNOEE227S47wUYFqlegcs8zlLLmeb9Fcff1HVZY4Fwku/1Eyv38n7GYwB2aaS71g==} resolution: {integrity: sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
temp-file@3.4.0: temp-file@3.4.0:
@ -1316,8 +1315,8 @@ packages:
tmp-promise@3.0.3: tmp-promise@3.0.3:
resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==}
tmp@0.2.7: tmp@0.2.6:
resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} resolution: {integrity: sha512-5sJPdPjfI5Kx+qbrDesxkglRBxW//g7hCsqspEjwkewGvBMGIKMOTKzLt1hFVJzyadba3lDUN20O9qhvbQUSTA==}
engines: {node: '>=14.14'} engines: {node: '>=14.14'}
toidentifier@1.0.1: toidentifier@1.0.1:
@ -1346,12 +1345,12 @@ packages:
undici-types@7.16.0: undici-types@7.16.0:
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
undici@6.27.0: undici@6.26.0:
resolution: {integrity: sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==} resolution: {integrity: sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==}
engines: {node: '>=18.17'} engines: {node: '>=18.17'}
undici@7.28.0: undici@7.27.2:
resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} resolution: {integrity: sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==}
engines: {node: '>=20.18.1'} engines: {node: '>=20.18.1'}
universalify@0.1.2: universalify@0.1.2:
@ -1366,8 +1365,8 @@ packages:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
unzipper@0.12.5: unzipper@0.12.3:
resolution: {integrity: sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A==} resolution: {integrity: sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA==}
uri-js@4.4.1: uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
@ -1488,7 +1487,7 @@ snapshots:
semver: 7.8.1 semver: 7.8.1
sumchecker: 3.0.1 sumchecker: 3.0.1
optionalDependencies: optionalDependencies:
undici: 7.28.0 undici: 7.27.2
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -1712,7 +1711,7 @@ snapshots:
app-builder-bin@4.0.0: {} 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: dependencies:
'@develar/schema-utils': 2.6.5 '@develar/schema-utils': 2.6.5
'@electron/notarize': 2.2.1 '@electron/notarize': 2.2.1
@ -1726,27 +1725,27 @@ snapshots:
builder-util-runtime: 9.2.4 builder-util-runtime: 9.2.4
chromium-pickle-js: 0.2.0 chromium-pickle-js: 0.2.0
debug: 4.4.3 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 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 electron-publish: 24.13.1
form-data: 4.0.6 form-data: 4.0.5
fs-extra: 10.1.0 fs-extra: 10.1.0
hosted-git-info: 4.1.0 hosted-git-info: 4.1.0
is-ci: 3.0.1 is-ci: 3.0.1
isbinaryfile: 5.0.7 isbinaryfile: 5.0.7
js-yaml: 5.2.0 js-yaml: 4.1.1
lazy-val: 1.0.5 lazy-val: 1.0.5
minimatch: 10.2.5 minimatch: 10.2.5
read-config-file: 6.3.2 read-config-file: 6.3.2
sanitize-filename: 1.6.4 sanitize-filename: 1.6.4
semver: 7.8.1 semver: 7.8.1
tar: 7.5.17 tar: 7.5.15
temp-file: 3.4.0 temp-file: 3.4.0
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - 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: dependencies:
'@electron/asar': 3.4.1 '@electron/asar': 3.4.1
'@electron/fuses': 1.8.0 '@electron/fuses': 1.8.0
@ -1762,22 +1761,22 @@ snapshots:
ajv: 8.20.0 ajv: 8.20.0
asn1js: 3.0.10 asn1js: 3.0.10
async-exit-hook: 2.0.1 async-exit-hook: 2.0.1
builder-util: 26.15.3 builder-util: 26.15.0
builder-util-runtime: 9.7.0 builder-util-runtime: 9.7.0
chromium-pickle-js: 0.2.0 chromium-pickle-js: 0.2.0
ci-info: 4.3.1 ci-info: 4.3.1
debug: 4.4.3 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: 16.4.5
dotenv-expand: 11.0.6 dotenv-expand: 11.0.6
ejs: 3.1.10 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: 26.15.3 electron-publish: 26.15.1
fs-extra: 10.1.0 fs-extra: 10.1.0
hosted-git-info: 4.1.0 hosted-git-info: 4.1.0
isbinaryfile: 5.0.7 isbinaryfile: 5.0.7
jiti: 2.6.1 jiti: 2.6.1
js-yaml: 5.2.0 js-yaml: 4.1.1
json5: 2.2.3 json5: 2.2.3
lazy-val: 1.0.5 lazy-val: 1.0.5
minimatch: 10.2.5 minimatch: 10.2.5
@ -1786,10 +1785,10 @@ snapshots:
proper-lockfile: 4.1.2 proper-lockfile: 4.1.2
resedit: 1.7.2 resedit: 1.7.2
semver: 7.7.4 semver: 7.7.4
tar: 7.5.17 tar: 7.5.15
temp-file: 3.4.0 temp-file: 3.4.0
tiny-async-pool: 1.3.0 tiny-async-pool: 1.3.0
unzipper: 0.12.5 unzipper: 0.12.3
which: 5.0.0 which: 5.0.0
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -1924,14 +1923,14 @@ snapshots:
http-proxy-agent: 5.0.0 http-proxy-agent: 5.0.0
https-proxy-agent: 5.0.1 https-proxy-agent: 5.0.1
is-ci: 3.0.1 is-ci: 3.0.1
js-yaml: 5.2.0 js-yaml: 4.1.1
source-map-support: 0.5.21 source-map-support: 0.5.21
stat-mode: 1.0.0 stat-mode: 1.0.0
temp-file: 3.4.0 temp-file: 3.4.0
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
builder-util@26.15.3: builder-util@26.15.0:
dependencies: dependencies:
'@types/debug': 4.1.13 '@types/debug': 4.1.13
builder-util-runtime: 9.7.0 builder-util-runtime: 9.7.0
@ -1941,7 +1940,7 @@ snapshots:
fs-extra: 10.1.0 fs-extra: 10.1.0
http-proxy-agent: 7.0.2 http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.5 https-proxy-agent: 7.0.5
js-yaml: 5.2.0 js-yaml: 4.1.1
sanitize-filename: 1.6.4 sanitize-filename: 1.6.4
source-map-support: 0.5.21 source-map-support: 0.5.21
stat-mode: 1.0.0 stat-mode: 1.0.0
@ -2089,12 +2088,12 @@ snapshots:
minimatch: 10.2.5 minimatch: 10.2.5
p-limit: 3.1.0 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: dependencies:
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)
builder-util: 26.15.3 builder-util: 26.15.0
fs-extra: 10.1.0 fs-extra: 10.1.0
js-yaml: 5.2.0 js-yaml: 4.1.1
transitivePeerDependencies: transitivePeerDependencies:
- electron-builder-squirrel-windows - electron-builder-squirrel-windows
- supports-color - supports-color
@ -2127,9 +2126,9 @@ snapshots:
dependencies: dependencies:
jake: 10.8.7 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: 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 archiver: 5.3.2
builder-util: 24.13.1 builder-util: 24.13.1
fs-extra: 10.1.0 fs-extra: 10.1.0
@ -2137,14 +2136,14 @@ snapshots:
- dmg-builder - dmg-builder
- supports-color - 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: dependencies:
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)
builder-util: 26.15.3 builder-util: 26.15.0
builder-util-runtime: 9.7.0 builder-util-runtime: 9.7.0
chalk: 4.1.2 chalk: 4.1.2
ci-info: 4.3.1 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 fs-extra: 10.1.0
lazy-val: 1.0.5 lazy-val: 1.0.5
simple-update-notifier: 2.0.0 simple-update-notifier: 2.0.0
@ -2165,21 +2164,21 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
electron-publish@26.15.3: electron-publish@26.15.1:
dependencies: dependencies:
'@types/fs-extra': 9.0.13 '@types/fs-extra': 9.0.13
aws4: 1.13.2 aws4: 1.13.2
builder-util: 26.15.3 builder-util: 26.15.0
builder-util-runtime: 9.7.0 builder-util-runtime: 9.7.0
chalk: 4.1.2 chalk: 4.1.2
form-data: 4.0.6 form-data: 4.0.5
fs-extra: 10.1.0 fs-extra: 10.1.0
lazy-val: 1.0.5 lazy-val: 1.0.5
mime: 2.6.0 mime: 2.6.0
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
electron@40.10.5: electron@40.10.3:
dependencies: dependencies:
'@electron-internal/extract-zip': 1.0.2 '@electron-internal/extract-zip': 1.0.2
'@electron/get': 5.0.0 '@electron/get': 5.0.0
@ -2216,7 +2215,7 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
get-intrinsic: 1.3.0 get-intrinsic: 1.3.0
has-tostringtag: 1.0.2 has-tostringtag: 1.0.2
hasown: 2.0.4 hasown: 2.0.2
es6-error@4.1.1: es6-error@4.1.1:
optional: true optional: true
@ -2295,12 +2294,12 @@ snapshots:
cross-spawn: 7.0.6 cross-spawn: 7.0.6
signal-exit: 4.1.0 signal-exit: 4.1.0
form-data@4.0.6: form-data@4.0.5:
dependencies: dependencies:
asynckit: 0.4.0 asynckit: 0.4.0
combined-stream: 1.0.8 combined-stream: 1.0.8
es-set-tostringtag: 2.1.0 es-set-tostringtag: 2.1.0
hasown: 2.0.4 hasown: 2.0.2
mime-types: 2.1.35 mime-types: 2.1.35
forwarded@0.2.0: {} forwarded@0.2.0: {}
@ -2315,6 +2314,12 @@ snapshots:
jsonfile: 6.2.0 jsonfile: 6.2.0
universalify: 2.0.1 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: fs-extra@11.3.1:
dependencies: dependencies:
graceful-fs: 4.2.11 graceful-fs: 4.2.11
@ -2431,10 +2436,6 @@ snapshots:
dependencies: dependencies:
function-bind: 1.1.2 function-bind: 1.1.2
hasown@2.0.4:
dependencies:
function-bind: 1.1.2
hosted-git-info@4.1.0: hosted-git-info@4.1.0:
dependencies: dependencies:
lru-cache: 6.0.0 lru-cache: 6.0.0
@ -2533,7 +2534,7 @@ snapshots:
jiti@2.6.1: {} jiti@2.6.1: {}
js-yaml@5.2.0: js-yaml@4.1.1:
dependencies: dependencies:
argparse: 2.0.1 argparse: 2.0.1
@ -2552,6 +2553,12 @@ snapshots:
optionalDependencies: optionalDependencies:
graceful-fs: 4.2.11 graceful-fs: 4.2.11
jsonfile@6.1.0:
dependencies:
universalify: 2.0.1
optionalDependencies:
graceful-fs: 4.2.11
jsonfile@6.2.0: jsonfile@6.2.0:
dependencies: dependencies:
universalify: 2.0.1 universalify: 2.0.1
@ -2649,9 +2656,9 @@ snapshots:
nopt: 9.0.0 nopt: 9.0.0
proc-log: 6.1.0 proc-log: 6.1.0
semver: 7.8.1 semver: 7.8.1
tar: 7.5.17 tar: 7.5.15
tinyglobby: 0.2.15 tinyglobby: 0.2.15
undici: 6.27.0 undici: 6.26.0
which: 6.0.1 which: 6.0.1
node-int64@0.4.0: {} node-int64@0.4.0: {}
@ -2784,7 +2791,7 @@ snapshots:
config-file-ts: 0.2.6 config-file-ts: 0.2.6
dotenv: 9.0.2 dotenv: 9.0.2
dotenv-expand: 5.1.0 dotenv-expand: 5.1.0
js-yaml: 5.2.0 js-yaml: 4.1.1
json5: 2.2.3 json5: 2.2.3
lazy-val: 1.0.5 lazy-val: 1.0.5
@ -3001,7 +3008,7 @@ snapshots:
inherits: 2.0.4 inherits: 2.0.4
readable-stream: 3.6.2 readable-stream: 3.6.2
tar@7.5.17: tar@7.5.15:
dependencies: dependencies:
'@isaacs/fs-minipass': 4.0.1 '@isaacs/fs-minipass': 4.0.1
chownr: 3.0.0 chownr: 3.0.0
@ -3025,9 +3032,9 @@ snapshots:
tmp-promise@3.0.3: tmp-promise@3.0.3:
dependencies: dependencies:
tmp: 0.2.7 tmp: 0.2.6
tmp@0.2.7: {} tmp@0.2.6: {}
toidentifier@1.0.1: {} toidentifier@1.0.1: {}
@ -3050,9 +3057,9 @@ snapshots:
undici-types@7.16.0: {} undici-types@7.16.0: {}
undici@6.27.0: {} undici@6.26.0: {}
undici@7.28.0: undici@7.27.2:
optional: true optional: true
universalify@0.1.2: {} universalify@0.1.2: {}
@ -3061,11 +3068,11 @@ snapshots:
unpipe@1.0.0: {} unpipe@1.0.0: {}
unzipper@0.12.5: unzipper@0.12.3:
dependencies: dependencies:
bluebird: 3.7.2 bluebird: 3.7.2
duplexer2: 0.1.4 duplexer2: 0.1.4
fs-extra: 11.3.1 fs-extra: 11.2.0
graceful-fs: 4.2.11 graceful-fs: 4.2.11
node-int64: 0.4.0 node-int64: 0.4.0

View File

@ -3,11 +3,10 @@
"devenv": { "devenv": {
"locked": { "locked": {
"dir": "src/modules", "dir": "src/modules",
"lastModified": 1782492839, "lastModified": 1773012232,
"narHash": "sha256-j9wrcB4al5QhMelEghJ0Qs+RQPT+wyCcI4070NEgPLQ=",
"owner": "cachix", "owner": "cachix",
"repo": "devenv", "repo": "devenv",
"rev": "3d39d0817d62069f7b18821c34a617b5141cb278", "rev": "46a4bd0299a26ad948b71d3053174ba7b90522f7",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -22,11 +21,10 @@
"nixpkgs-src": "nixpkgs-src" "nixpkgs-src": "nixpkgs-src"
}, },
"locked": { "locked": {
"lastModified": 1782132010, "lastModified": 1772749504,
"narHash": "sha256-ZnAVHdVrotp80iIMm5CSR1fdxPlw7Uwmwxb+O/wsgZ8=",
"owner": "cachix", "owner": "cachix",
"repo": "devenv-nixpkgs", "repo": "devenv-nixpkgs",
"rev": "12866ae2dddbc0ab8b329915f8072bb9c75bde89", "rev": "08543693199362c1fddb8f52126030d0d374ba2e",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -39,11 +37,11 @@
"nixpkgs-src": { "nixpkgs-src": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1781607440, "lastModified": 1769922788,
"narHash": "sha256-rxO+uc/KFbSJp+pgyXRuAX6QlG9hJdnt0BXpEQRXY+U=", "narHash": "sha256-H3AfG4ObMDTkTJYkd8cz1/RbY9LatN5Mk4UF48VuSXc=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "3e41b24abd260e8f71dbe2f5737d24122f972158", "rev": "207d15f1a6603226e1e223dc79ac29c7846da32e",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -55,11 +53,10 @@
}, },
"nixpkgs-unstable": { "nixpkgs-unstable": {
"locked": { "locked": {
"lastModified": 1782467914, "lastModified": 1772773019,
"narHash": "sha256-pGvFkM8N0xEkIIXDe5YYfbEAvHrk4IxBrjB/x8OomhE=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "e73de5be04e0eff4190a1432b946d469c794e7b4", "rev": "aca4d95fce4914b3892661bcb80b8087293536c6",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@ -1 +1 @@
24.18.0 24.13.0

View File

@ -13,7 +13,7 @@
}, },
"homepage": "https://vikunja.io/", "homepage": "https://vikunja.io/",
"funding": "https://opencollective.com/vikunja", "funding": "https://opencollective.com/vikunja",
"packageManager": "pnpm@10.34.4", "packageManager": "pnpm@10.28.1",
"engines": { "engines": {
"node": ">=24.0.0" "node": ">=24.0.0"
}, },
@ -51,95 +51,95 @@
"story:preview": "histoire preview" "story:preview": "histoire preview"
}, },
"dependencies": { "dependencies": {
"@floating-ui/dom": "1.7.6", "@floating-ui/dom": "1.7.4",
"@fortawesome/fontawesome-svg-core": "7.3.0", "@fortawesome/fontawesome-svg-core": "7.1.0",
"@fortawesome/free-regular-svg-icons": "7.3.0", "@fortawesome/free-regular-svg-icons": "7.1.0",
"@fortawesome/free-solid-svg-icons": "7.3.0", "@fortawesome/free-solid-svg-icons": "7.1.0",
"@fortawesome/vue-fontawesome": "3.3.0", "@fortawesome/vue-fontawesome": "3.1.3",
"@intlify/unplugin-vue-i18n": "11.2.4", "@intlify/unplugin-vue-i18n": "11.0.3",
"@kyvg/vue3-notification": "3.4.2", "@kyvg/vue3-notification": "3.4.2",
"@sentry/vue": "10.62.0", "@sentry/vue": "10.36.0",
"@tiptap/core": "3.27.1", "@tiptap/core": "3.17.0",
"@tiptap/extension-blockquote": "3.27.1", "@tiptap/extension-blockquote": "3.17.0",
"@tiptap/extension-code-block-lowlight": "3.27.1", "@tiptap/extension-code-block-lowlight": "3.17.0",
"@tiptap/extension-hard-break": "3.27.1", "@tiptap/extension-hard-break": "3.17.0",
"@tiptap/extension-image": "3.27.1", "@tiptap/extension-image": "3.17.0",
"@tiptap/extension-link": "3.27.1", "@tiptap/extension-link": "3.17.0",
"@tiptap/extension-list": "3.27.1", "@tiptap/extension-list": "3.17.0",
"@tiptap/extension-mention": "3.27.1", "@tiptap/extension-mention": "3.17.0",
"@tiptap/extension-table": "3.27.1", "@tiptap/extension-table": "3.17.0",
"@tiptap/extension-typography": "3.27.1", "@tiptap/extension-typography": "3.17.0",
"@tiptap/extension-underline": "3.27.1", "@tiptap/extension-underline": "3.17.0",
"@tiptap/extensions": "3.27.1", "@tiptap/extensions": "3.17.0",
"@tiptap/pm": "3.27.1", "@tiptap/pm": "3.17.0",
"@tiptap/starter-kit": "3.27.1", "@tiptap/starter-kit": "3.17.0",
"@tiptap/suggestion": "3.27.1", "@tiptap/suggestion": "3.17.0",
"@tiptap/vue-3": "3.27.1", "@tiptap/vue-3": "3.17.0",
"@vueuse/core": "14.3.0", "@vueuse/core": "14.1.0",
"@vueuse/router": "14.3.0", "@vueuse/router": "14.1.0",
"axios": "1.18.1", "axios": "1.16.0",
"blurhash": "2.0.5", "blurhash": "2.0.5",
"bulma-css-variables": "0.9.33", "bulma-css-variables": "0.9.33",
"change-case": "5.4.4", "change-case": "5.4.4",
"dayjs": "1.11.21", "dayjs": "1.11.19",
"dompurify": "3.4.11", "dompurify": "3.4.0",
"fast-deep-equal": "3.1.3", "fast-deep-equal": "3.1.3",
"flatpickr": "4.6.13", "flatpickr": "4.6.13",
"floating-vue": "5.2.2", "floating-vue": "5.2.2",
"is-touch-device": "1.0.1", "is-touch-device": "1.0.1",
"klona": "2.0.6", "klona": "2.0.6",
"lowlight": "3.3.0", "lowlight": "3.3.0",
"marked": "17.0.6", "marked": "17.0.1",
"nanoid": "5.1.16", "nanoid": "5.1.6",
"pinia": "3.0.4", "pinia": "3.0.4",
"register-service-worker": "1.7.2", "register-service-worker": "1.7.2",
"sortablejs": "1.15.7", "sortablejs": "1.15.6",
"ufo": "1.6.4", "ufo": "1.6.3",
"vue": "3.5.39", "vue": "3.5.27",
"vue-advanced-cropper": "2.8.9", "vue-advanced-cropper": "2.8.9",
"vue-flatpickr-component": "11.0.5", "vue-flatpickr-component": "11.0.5",
"vue-i18n": "11.4.6", "vue-i18n": "11.2.8",
"vue-router": "4.6.4", "vue-router": "4.6.4",
"vuemoji-picker": "0.3.2", "vuemoji-picker": "0.3.2",
"workbox-precaching": "7.4.1", "workbox-precaching": "7.4.1",
"zhyswan-vuedraggable": "4.1.3" "zhyswan-vuedraggable": "4.1.3"
}, },
"devDependencies": { "devDependencies": {
"@faker-js/faker": "10.5.0", "@faker-js/faker": "10.4.0",
"@histoire/plugin-screenshot": "1.0.0-beta.1", "@histoire/plugin-screenshot": "1.0.0-beta.1",
"@histoire/plugin-vue": "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", "@sentry/vite-plugin": "3.6.1",
"@tailwindcss/vite": "4.3.1", "@tailwindcss/vite": "4.3.0",
"@tsconfig/node24": "24.0.4", "@tsconfig/node24": "24.0.4",
"@types/codemirror": "5.60.17", "@types/codemirror": "5.60.17",
"@types/is-touch-device": "1.0.3", "@types/is-touch-device": "1.0.3",
"@types/node": "24.13.2", "@types/node": "24.13.1",
"@types/sortablejs": "1.15.9", "@types/sortablejs": "1.15.9",
"@types/ws": "8.18.1", "@types/ws": "8.18.1",
"@typescript-eslint/eslint-plugin": "8.62.0", "@typescript-eslint/eslint-plugin": "8.61.0",
"@typescript-eslint/parser": "8.62.0", "@typescript-eslint/parser": "8.61.0",
"@vitejs/plugin-vue": "6.0.7", "@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/test-utils": "2.4.11",
"@vue/tsconfig": "0.9.1", "@vue/tsconfig": "0.9.1",
"@vueuse/shared": "14.3.0", "@vueuse/shared": "14.3.0",
"autoprefixer": "10.5.2", "autoprefixer": "10.5.0",
"browserslist": "4.28.4", "browserslist": "4.28.2",
"caniuse-lite": "1.0.30001799", "caniuse-lite": "1.0.30001797",
"csstype": "3.2.3", "csstype": "3.2.3",
"esbuild": "0.28.1", "esbuild": "0.28.0",
"eslint": "9.39.4", "eslint": "9.39.4",
"eslint-plugin-depend": "1.5.0", "eslint-plugin-depend": "1.5.0",
"eslint-plugin-vue": "10.9.2", "eslint-plugin-vue": "10.9.2",
"happy-dom": "20.10.6", "happy-dom": "20.10.2",
"histoire": "1.0.0-beta.1", "histoire": "1.0.0-beta.1",
"otplib": "12.0.1", "otplib": "12.0.1",
"postcss": "8.5.15", "postcss": "8.5.15",
"postcss-easing-gradients": "3.0.1", "postcss-easing-gradients": "3.0.1",
"postcss-html": "1.8.1", "postcss-html": "1.8.1",
"postcss-preset-env": "11.3.1", "postcss-preset-env": "11.3.0",
"rollup": "4.62.2", "rollup": "4.61.1",
"rollup-plugin-visualizer": "6.0.11", "rollup-plugin-visualizer": "6.0.11",
"sass-embedded": "1.100.0", "sass-embedded": "1.100.0",
"stylelint": "17.13.0", "stylelint": "17.13.0",
@ -147,15 +147,15 @@
"stylelint-config-recommended-vue": "1.6.1", "stylelint-config-recommended-vue": "1.6.1",
"stylelint-config-standard-scss": "17.0.0", "stylelint-config-standard-scss": "17.0.0",
"stylelint-use-logical": "2.1.3", "stylelint-use-logical": "2.1.3",
"tailwindcss": "4.3.1", "tailwindcss": "4.3.0",
"typescript": "5.9.3", "typescript": "5.9.3",
"unplugin-inject-preload": "3.0.0", "unplugin-inject-preload": "3.0.0",
"vite": "7.3.6", "vite": "7.3.5",
"vite-plugin-pwa": "1.3.0", "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", "vite-svg-loader": "5.1.1",
"vitest": "4.1.9", "vitest": "4.1.8",
"vue-tsc": "3.3.5", "vue-tsc": "3.3.4",
"wait-on": "9.0.10", "wait-on": "9.0.10",
"workbox-cli": "7.4.1", "workbox-cli": "7.4.1",
"ws": "8.21.0" "ws": "8.21.0"
@ -169,20 +169,14 @@
"vue-demi" "vue-demi"
], ],
"overrides": { "overrides": {
"minimatch": "10.2.5", "minimatch": "^10.2.3",
"rollup": "$rollup", "rollup": "$rollup",
"basic-ftp": "6.0.1", "basic-ftp": ">=5.2.2",
"serialize-javascript": "7.0.6", "serialize-javascript": "^7.0.5",
"flatted": "3.4.2", "flatted": "^3.4.1",
"ip-address": "10.2.0", "ip-address": ">=10.1.1",
"postcss": "8.5.15", "postcss": ">=8.5.10",
"tmp": "0.2.7", "tmp": ">=0.2.6"
"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"
} }
} }
} }

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -61,7 +61,6 @@ import {useAuthStore} from '@/stores/auth'
import {useBaseStore} from '@/stores/base' import {useBaseStore} from '@/stores/base'
import {useColorScheme} from '@/composables/useColorScheme' import {useColorScheme} from '@/composables/useColorScheme'
import {useTimeTrackingFavicon} from '@/composables/useTimeTrackingFavicon'
import {useBodyClass} from '@/composables/useBodyClass' import {useBodyClass} from '@/composables/useBodyClass'
import QuickAddOverlay from '@/components/quick-actions/QuickAddOverlay.vue' import QuickAddOverlay from '@/components/quick-actions/QuickAddOverlay.vue'
import AddToHomeScreen from '@/components/home/AddToHomeScreen.vue' import AddToHomeScreen from '@/components/home/AddToHomeScreen.vue'
@ -108,7 +107,6 @@ watch(accountDeletionConfirm, async (accountDeletionConfirm) => {
setLanguage(authStore.settings.language ?? DEFAULT_LANGUAGE) setLanguage(authStore.settings.language ?? DEFAULT_LANGUAGE)
useColorScheme() useColorScheme()
useTimeTrackingFavicon()
</script> </script>
<style src="@/styles/tailwind.css" /> <style src="@/styles/tailwind.css" />

View File

@ -13,14 +13,14 @@
<div class="gantt-chart-wrapper"> <div class="gantt-chart-wrapper">
<GanttTimelineHeader <GanttTimelineHeader
:timeline-data="timelineData" :timeline-data="timelineData"
:day-width-pixels="dayWidthPixels" :day-width-pixels="DAY_WIDTH_PIXELS"
/> />
<GanttVerticalGridLines <GanttVerticalGridLines
:timeline-data="timelineData" :timeline-data="timelineData"
:total-width="totalWidth" :total-width="totalWidth"
:height="ganttRows.length * 40" :height="ganttRows.length * 40"
:day-width-pixels="dayWidthPixels" :day-width-pixels="DAY_WIDTH_PIXELS"
/> />
<GanttChartBody <GanttChartBody
@ -57,7 +57,7 @@
:total-width="totalWidth" :total-width="totalWidth"
:date-from-date="dateFromDate" :date-from-date="dateFromDate"
:date-to-date="dateToDate" :date-to-date="dateToDate"
:day-width-pixels="dayWidthPixels" :day-width-pixels="DAY_WIDTH_PIXELS"
:is-dragging="isDragging" :is-dragging="isDragging"
:is-resizing="isResizing" :is-resizing="isResizing"
:drag-state="dragState" :drag-state="dragState"
@ -89,7 +89,7 @@
</template> </template>
<script setup lang="ts"> <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 {useRouter} from 'vue-router'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import {useDayjsLanguageSync} from '@/i18n/useDayjsLanguageSync' import {useDayjsLanguageSync} from '@/i18n/useDayjsLanguageSync'
@ -126,9 +126,7 @@ const emit = defineEmits<{
(e: 'update:task', task: ITaskPartialWithId): void (e: 'update:task', task: ITaskPartialWithId): void
}>() }>()
const DAY_WIDTH_PIXELS_MIN = 30 const DAY_WIDTH_PIXELS = 30
const dayWidthPixels = ref(0)
let resizeObserver: ResizeObserver
const {tasks, filters} = toRefs(props) const {tasks, filters} = toRefs(props)
@ -160,7 +158,7 @@ const dateToDate = computed(() => dayjs(filters.value.dateTo).endOf('day').toDat
const totalWidth = computed(() => { const totalWidth = computed(() => {
const dateDiff = Math.ceil((dateToDate.value.valueOf() - dateFromDate.value.valueOf()) / MILLISECONDS_A_DAY) 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(() => { 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 // Build the task tree when tasks change
watch( watch(
[tasks, filters], [tasks, filters],
@ -402,7 +351,7 @@ const ROW_HEIGHT = 40
const barPositions = computed(() => { const barPositions = computed(() => {
const positions = new Map<number, GanttBarPosition>() const positions = new Map<number, GanttBarPosition>()
const ds = dragState.value 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) => { ganttBars.value.forEach((rowBars, rowIndex) => {
for (const bar of rowBars) { for (const bar of rowBars) {
@ -437,7 +386,7 @@ function computeBarX(date: Date): number {
(roundToNaturalDayBoundary(date, true).getTime() - dateFromDate.value.getTime()) / (roundToNaturalDayBoundary(date, true).getTime() - dateFromDate.value.getTime()) /
MILLISECONDS_A_DAY, MILLISECONDS_A_DAY,
) )
return diff * dayWidthPixels.value return diff * DAY_WIDTH_PIXELS
} }
function computeBarWidth(bar: GanttBarModel): number { function computeBarWidth(bar: GanttBarModel): number {
@ -445,7 +394,7 @@ function computeBarWidth(bar: GanttBarModel): number {
(roundToNaturalDayBoundary(bar.end).getTime() - roundToNaturalDayBoundary(bar.start, true).getTime()) / (roundToNaturalDayBoundary(bar.end).getTime() - roundToNaturalDayBoundary(bar.start, true).getTime()) /
MILLISECONDS_A_DAY, MILLISECONDS_A_DAY,
) )
return diff * dayWidthPixels.value return diff * DAY_WIDTH_PIXELS
} }
// Compute relation arrows // Compute relation arrows
@ -641,7 +590,7 @@ function startDrag(bar: GanttBarModel, event: PointerEvent) {
if (!dragState.value || !isDragging.value) return if (!dragState.value || !isDragging.value) return
const diff = e.clientX - dragState.value.startX 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) { if (days !== dragState.value.currentDays) {
dragState.value.currentDays = days dragState.value.currentDays = days
@ -703,7 +652,7 @@ function startResize(bar: GanttBarModel, edge: 'start' | 'end', event: PointerEv
if (!dragState.value || !isResizing.value) return if (!dragState.value || !isResizing.value) return
const diff = e.clientX - dragState.value.startX 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') { if (edge === 'start') {
const newStart = new Date(dragState.value.originalStart) const newStart = new Date(dragState.value.originalStart)
@ -781,7 +730,7 @@ function focusTaskBar(rowId: string) {
setTimeout(() => { setTimeout(() => {
const taskBarElement = document.querySelector(`[data-row-id="${rowId}"] [role="slider"]`) as HTMLElement const taskBarElement = document.querySelector(`[data-row-id="${rowId}"] [role="slider"]`) as HTMLElement
if (taskBarElement) { if (taskBarElement) {
taskBarElement.focus({preventScroll: true}) taskBarElement.focus()
} }
}, 0) }, 0)
} }

View File

@ -722,7 +722,7 @@ async function addImage(event: Event) {
return return
} }
const url = await inputPrompt(event.target.getBoundingClientRect(), '', editor.value) const url = await inputPrompt(event.target.getBoundingClientRect())
if (url) { if (url) {
editor.value?.chain().focus().setImage({src: url}).run() editor.value?.chain().focus().setImage({src: url}).run()

View File

@ -5,7 +5,6 @@ import {PluginKey, type EditorState} from '@tiptap/pm/state'
import EmojiList from './EmojiList.vue' import EmojiList from './EmojiList.vue'
import {loadEmojis, filterEmojis, type EmojiEntry} from './emojiData' import {loadEmojis, filterEmojis, type EmojiEntry} from './emojiData'
import {getPopupContainer} from '../popupContainer'
export const EmojiSuggestionPluginKey = new PluginKey('emojiSuggestion') export const EmojiSuggestionPluginKey = new PluginKey('emojiSuggestion')
@ -79,7 +78,7 @@ export default function emojiSuggestionSetup() {
popupElement.style.left = '0' popupElement.style.left = '0'
popupElement.style.zIndex = '4700' popupElement.style.zIndex = '4700'
popupElement.appendChild(component.element!) popupElement.appendChild(component.element!)
getPopupContainer(props.editor).appendChild(popupElement) document.body.appendChild(popupElement)
const rect = props.clientRect() const rect = props.clientRect()
if (!rect) { if (!rect) {
@ -109,7 +108,7 @@ export default function emojiSuggestionSetup() {
cleanupFloating = null cleanupFloating = null
} }
if (popupElement) { if (popupElement) {
popupElement.remove() document.body.removeChild(popupElement)
popupElement = null popupElement = null
} }
component?.destroy() component?.destroy()

View File

@ -3,7 +3,7 @@ import inputPrompt from '@/helpers/inputPrompt'
export async function setLinkInEditor(pos: DOMRect, editor: Editor | null | undefined) { export async function setLinkInEditor(pos: DOMRect, editor: Editor | null | undefined) {
const previousUrl = editor?.getAttributes('link').href || '' const previousUrl = editor?.getAttributes('link').href || ''
const url = await inputPrompt(pos, previousUrl, editor ?? undefined) const url = await inputPrompt(pos, previousUrl)
// empty // empty
if (url === '') { if (url === '') {

View File

@ -68,7 +68,7 @@ const props = withDefaults(defineProps<{
enabled?: boolean, enabled?: boolean,
overflow?: boolean, overflow?: boolean,
wide?: boolean, wide?: boolean,
variant?: 'default' | 'hint-modal' | 'scrolling' | 'top', variant?: 'default' | 'hint-modal' | 'scrolling',
}>(), { }>(), {
enabled: true, enabled: true,
overflow: false, overflow: false,
@ -211,13 +211,7 @@ $modal-width: 1024px;
// Reset UA dialog styles // Reset UA dialog styles
padding: 0; padding: 0;
border: none; border: none;
// The scrim lives on the dialog element, not on ::backdrop: Chromium background: transparent;
// 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);
color: #ffffff; color: #ffffff;
// Fill viewport // Fill viewport
position: fixed; position: fixed;
@ -227,12 +221,10 @@ $modal-width: 1024px;
max-inline-size: 100%; max-inline-size: 100%;
max-block-size: 100%; max-block-size: 100%;
// Transitions. No display/allow-discrete transition needed: the close // Transitions
// fade runs while the dialog is still [open] (data-closing + timer in
// closeDialog), and transitioning display triggers the Chromium paint
// bug above.
opacity: 0; opacity: 0;
transition: opacity 150ms ease; transition: opacity 150ms ease,
display 150ms ease allow-discrete;
&[open]:not([data-closing]) { &[open]:not([data-closing]) {
opacity: 1; opacity: 1;
@ -244,11 +236,16 @@ $modal-width: 1024px;
&::backdrop { &::backdrop {
background-color: rgba(0, 0, 0, 0); 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 &[open]:not([data-closing])::backdrop {
&:has(.is-quick-add-mode) { background-color: rgba(0, 0, 0, .8);
background: transparent;
@starting-style {
background-color: rgba(0, 0, 0, 0);
}
} }
} }
@ -264,20 +261,13 @@ $modal-width: 1024px;
} }
.default .modal-content, .default .modal-content,
.hint-modal .modal-content, .hint-modal .modal-content {
.top .modal-content {
text-align: center; text-align: center;
position: absolute; position: absolute;
// fine to use top/left since we're only using this to position it centered // fine to use top/left since we're only using this to position it centered
inset-block-start: 50%; inset-block-start: 50%;
inset-inline-start: 50%; inset-inline-start: 50%;
transform: translate(-50%, -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"] & { [dir="rtl"] & {
transform: translate(50%, -50%); transform: translate(50%, -50%);
@ -287,9 +277,6 @@ $modal-width: 1024px;
margin: 0; margin: 0;
position: static; position: static;
transform: none; transform: none;
// the fullscreen mobile layout flows and scrolls in .modal-container
max-block-size: none;
overflow: visible;
} }
.modal-header { .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 // 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 // `wide` prop can still expand the modal (the .is-wide rule below would
// otherwise be outranked by .default .modal-content's specificity). // otherwise be outranked by .default .modal-content's specificity).
.default .modal-content:not(.is-wide), .default .modal-content:not(.is-wide),
.hint-modal .modal-content:not(.is-wide), .hint-modal .modal-content:not(.is-wide) {
.top .modal-content:not(.is-wide) {
inline-size: calc(100% - 2rem); inline-size: calc(100% - 2rem);
max-inline-size: 640px; max-inline-size: 640px;
@ -436,7 +403,6 @@ $modal-width: 1024px;
block-size: auto; block-size: auto;
max-inline-size: none; max-inline-size: none;
max-block-size: none; max-block-size: none;
background: transparent;
&::backdrop { &::backdrop {
display: none; display: none;

View File

@ -2,7 +2,6 @@
<Modal <Modal
:enabled="active" :enabled="active"
:overflow="isNewTaskCommand" :overflow="isNewTaskCommand"
variant="top"
@close="closeQuickActions" @close="closeQuickActions"
> >
<div <div
@ -705,16 +704,15 @@ function reset() {
<style lang="scss" scoped> <style lang="scss" scoped>
.quick-actions { .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; overflow: hidden;
justify-content: flex-start !important; 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 { &.is-quick-add-mode {
padding: 0; padding: 0;
margin: 0; margin: 0;

View File

@ -25,7 +25,6 @@
rows="1" rows="1"
@keydown="resetEmptyTitleError" @keydown="resetEmptyTitleError"
@keydown.enter="handleEnter" @keydown.enter="handleEnter"
@keydown.esc="blurTaskInput"
/> />
<QuickAddMagic <QuickAddMagic
:highlight-hint-icon="taskAddHovered" :highlight-hint-icon="taskAddHovered"
@ -283,10 +282,6 @@ function focusTaskInput() {
newTaskInput.value?.focus() newTaskInput.value?.focus()
} }
function blurTaskInput() {
newTaskInput.value?.blur()
}
defineExpose({ defineExpose({
focusTaskInput, focusTaskInput,
}) })

View File

@ -123,7 +123,7 @@
</XButton> </XButton>
<!-- Dropzone --> <!-- Dropzone -->
<Teleport :to="dropzoneTeleportTarget"> <Teleport to="body">
<div <div
v-if="editEnabled" v-if="editEnabled"
:class="{hidden: !showDropzone}" :class="{hidden: !showDropzone}"
@ -185,7 +185,7 @@
</template> </template>
<script setup lang="ts"> <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 {useDropZone} from '@vueuse/core'
import User from '@/components/misc/User.vue' import User from '@/components/misc/User.vue'
@ -322,34 +322,6 @@ const showDropzone = computed(() =>
props.editEnabled && isDraggingFiles.value && !isDragOverEditor.value, 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 => { watch(() => props.editEnabled, enabled => {
if (!enabled) { if (!enabled) {
resetDragState() resetDragState()
@ -506,7 +478,7 @@ defineExpose({
inset-inline-start: 0; inset-inline-start: 0;
inset-block-end: 0; inset-block-end: 0;
inset-inline-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; text-align: center;
&.hidden { &.hidden {

View File

@ -4,7 +4,7 @@
v-if="editEnabled && Object.keys(relatedTasks).length > 0" v-if="editEnabled && Object.keys(relatedTasks).length > 0"
id="showRelatedTasksFormButton" id="showRelatedTasksFormButton"
v-tooltip="$t('task.relation.add')" 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}" :class="{'is-active': showNewRelationForm}"
variant="secondary" variant="secondary"
icon="plus" icon="plus"

View File

@ -326,17 +326,9 @@ const isOverdue = computed(() => (
let oldTask let oldTask
async function markAsDone(checked: boolean, wasReverted: boolean = false) { async function markAsDone(checked: boolean, wasReverted: boolean = false) {
const updateFunc = async () => {
oldTask = {...task.value} oldTask = {...task.value}
const newTask = await taskStore.update(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
task.value = newTask task.value = newTask
updateDueDate() updateDueDate()
@ -362,9 +354,9 @@ async function markAsDone(checked: boolean, wasReverted: boolean = false) {
} }
if (checked) { 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 { } 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
} }
} }

View File

@ -1,4 +1,4 @@
import { getCurrentInstance, ref } from 'vue' import { ref } from 'vue'
import { createGlobalState, useIntervalFn } from '@vueuse/core' import { createGlobalState, useIntervalFn } from '@vueuse/core'
import { onBeforeRouteUpdate } from 'vue-router' import { onBeforeRouteUpdate } from 'vue-router'
@ -18,14 +18,10 @@ export const useGlobalNow = createGlobalState(() => {
useIntervalFn(update, GLOBAL_NOW_INTERVAL, { immediate: true }) 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 // ensure the now value is refreshed when the route changes
onBeforeRouteUpdate(() => { onBeforeRouteUpdate(() => {
update() update()
}) })
}
return { return {
now, now,

View File

@ -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({})
})
})

View File

@ -1,6 +1,4 @@
import {ref, shallowReactive, watch, computed, type ComputedGetter} from 'vue' 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 {useRouteQuery} from '@vueuse/router'
import TaskCollectionService, { import TaskCollectionService, {
@ -12,7 +10,6 @@ import type {ITask} from '@/modelTypes/ITask'
import {error} from '@/message' import {error} from '@/message'
import type {IProject} from '@/modelTypes/IProject' import type {IProject} from '@/modelTypes/IProject'
import {useAuthStore} from '@/stores/auth' import {useAuthStore} from '@/stores/auth'
import {useViewFiltersStore} from '@/stores/viewFilters'
import type {IProjectView} from '@/modelTypes/IProjectView' import type {IProjectView} from '@/modelTypes/IProjectView'
export type Order = 'asc' | 'desc' | 'none' export type Order = 'asc' | 'desc' | 'none'
@ -62,22 +59,6 @@ const SORT_BY_DEFAULT: SortBy = {
id: 'desc', 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. // 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 // 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. // precedence over everything else, making any other sort columns pretty useless.
@ -113,9 +94,6 @@ export function useTaskList(
const projectId = computed(() => projectIdGetter()) const projectId = computed(() => projectIdGetter())
const projectViewId = computed(() => projectViewIdGetter()) const projectViewId = computed(() => projectViewIdGetter())
const router = useRouter()
const viewFiltersStore = useViewFiltersStore()
const params = ref<TaskFilterParams>({...getDefaultTaskFilterParams()}) const params = ref<TaskFilterParams>({...getDefaultTaskFilterParams()})
const page = useRouteQuery('page', '1', { transform: Number }) 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 allParams = computed(() => {
const loadParams = {...params.value} const loadParams = {...params.value}

View File

@ -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))
})

View File

@ -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='

View File

@ -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])
})
})

View File

@ -33,53 +33,18 @@ export const removeToken = () => {
savedToken = null savedToken = null
localStorage.removeItem('token') localStorage.removeItem('token')
localStorage.removeItem('desktopOAuthRefreshToken') 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. * Refreshes an auth token while ensuring it is updated everywhere.
* The refresh token is sent automatically as an HttpOnly cookie. * The refresh token is sent automatically as an HttpOnly cookie.
* The server rotates the cookie on every call. * The server rotates the cookie on every call.
* *
* Same-tab concurrent calls share one in-flight refresh (always-on dedup); the * Uses the Web Locks API to coordinate across browser tabs. Only one tab
* Web Locks API inside adds cross-tab coordination only in secure contexts. * 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> { 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 // In desktop mode, refresh via IPC to the Electron main process
if (isDesktopApp()) { if (isDesktopApp()) {
const storedRefreshToken = localStorage.getItem('desktopOAuthRefreshToken') const storedRefreshToken = localStorage.getItem('desktopOAuthRefreshToken')
@ -88,9 +53,6 @@ async function doRefresh(persist: boolean): Promise<void> {
} }
try { try {
const tokens = await refreshDesktopToken(window.API_URL, storedRefreshToken) const tokens = await refreshDesktopToken(window.API_URL, storedRefreshToken)
if (loggedOutSinceStart()) {
return
}
saveToken(tokens.access_token, persist) saveToken(tokens.access_token, persist)
localStorage.setItem('desktopOAuthRefreshToken', tokens.refresh_token) localStorage.setItem('desktopOAuthRefreshToken', tokens.refresh_token)
} catch (e) { } catch (e) {
@ -103,13 +65,7 @@ async function doRefresh(persist: boolean): Promise<void> {
// if another tab refreshed while we were queued. // if another tab refreshed while we were queued.
const tokenBeforeLock = localStorage.getItem('token') const tokenBeforeLock = localStorage.getItem('token')
const refreshUnderLock = async () => { const doRefresh = 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
}
// If the token in localStorage changed while waiting for the lock, // If the token in localStorage changed while waiting for the lock,
// another tab already refreshed. Just adopt the new token. // another tab already refreshed. Just adopt the new token.
const currentToken = localStorage.getItem('token') const currentToken = localStorage.getItem('token')
@ -122,9 +78,6 @@ async function doRefresh(persist: boolean): Promise<void> {
const HTTP = HTTPFactory() const HTTP = HTTPFactory()
try { try {
const response = await HTTP.post('user/token/refresh') const response = await HTTP.post('user/token/refresh')
if (loggedOutSinceStart()) {
return
}
saveToken(response.data.token, persist) saveToken(response.data.token, persist)
} catch (e) { } catch (e) {
throw new Error('Error renewing token: ', {cause: e}) throw new Error('Error renewing token: ', {cause: e})
@ -132,10 +85,10 @@ async function doRefresh(persist: boolean): Promise<void> {
} }
if (navigator.locks) { if (navigator.locks) {
await navigator.locks.request('vikunja-token-refresh', refreshUnderLock) await navigator.locks.request('vikunja-token-refresh', doRefresh)
} else { } else {
// Fallback for environments without Web Locks (e.g. insecure HTTP) // Fallback for environments without Web Locks (e.g. insecure HTTP)
await refreshUnderLock() await doRefresh()
} }
} }

View File

@ -10,9 +10,5 @@ export function getProjectTitle(project: IProject) {
return i18n.global.t('project.inboxTitle') return i18n.global.t('project.inboxTitle')
} }
if (project.title === 'My Open Tasks') {
return i18n.global.t('project.myOpenTasksFilterTitle')
}
return project.title return project.title
} }

View File

@ -2,17 +2,10 @@ import {createRandomID} from '@/helpers/randomId'
import {computePosition, flip, shift, offset} from '@floating-ui/dom' import {computePosition, flip, shift, offset} from '@floating-ui/dom'
import {nextTick} from 'vue' import {nextTick} from 'vue'
import {eventToShortcutString} from '@/helpers/shortcut' 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) => { return new Promise((resolve) => {
const id = 'link-input-' + createRandomID() 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 // Create popup element
const popupElement = document.createElement('div') const popupElement = document.createElement('div')
@ -33,7 +26,7 @@ export default function inputPrompt(pos: ClientRect, oldValue: string = '', edit
inputElement.value = oldValue inputElement.value = oldValue
wrapperDiv.appendChild(inputElement) wrapperDiv.appendChild(inputElement)
popupElement.appendChild(wrapperDiv) popupElement.appendChild(wrapperDiv)
container.appendChild(popupElement) document.body.appendChild(popupElement)
// Create a local mutable copy of the position for scroll tracking // Create a local mutable copy of the position for scroll tracking
let currentRect = new DOMRect(pos.left, pos.top, pos.width, pos.height) 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()) 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 = () => { const cleanup = () => {
window.removeEventListener('scroll', handleScroll, true) window.removeEventListener('scroll', handleScroll, true)
document.removeEventListener('click', handleClickOutside) if (document.body.contains(popupElement)) {
dialog?.removeEventListener('cancel', handleDialogCancel) document.body.removeChild(popupElement)
if (container.contains(popupElement)) {
container.removeChild(popupElement)
} }
} }
document.getElementById(id)?.addEventListener('keydown', event => { document.getElementById(id)?.addEventListener('keydown', event => {
const shortcutString = eventToShortcutString(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') { if (shortcutString !== 'Enter') {
return return
} }
@ -138,6 +105,15 @@ export default function inputPrompt(pos: ClientRect, oldValue: string = '', edit
cleanup() 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 // Add slight delay to prevent immediate closing
setTimeout(() => { setTimeout(() => {
document.addEventListener('click', handleClickOutside) document.addEventListener('click', handleClickOutside)

View File

@ -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}` 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) { if (provider.logoutUrl.length > 0) {
window.location.href = `${provider.logoutUrl}` window.location.href = `${provider.logoutUrl}`
return true
} }
return false
} }

View File

@ -5,7 +5,6 @@ import {i18n} from '@/i18n'
import {createSharedComposable} from '@vueuse/core' import {createSharedComposable} from '@vueuse/core'
import {computed, toValue, type MaybeRefOrGetter} from 'vue' import {computed, toValue, type MaybeRefOrGetter} from 'vue'
import {useDateDisplay} from '@/composables/useDateDisplay' import {useDateDisplay} from '@/composables/useDateDisplay'
import {useGlobalNow} from '@/composables/useGlobalNow'
import {useTimeFormat} from '@/composables/useTimeFormat' import {useTimeFormat} from '@/composables/useTimeFormat'
import {DATE_DISPLAY, type DateDisplay} from '@/constants/dateDisplay' import {DATE_DISPLAY, type DateDisplay} from '@/constants/dateDisplay'
import {TIME_FORMAT, type TimeFormat} from '@/constants/timeFormat' 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' 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 return date
? dayjs(date).locale(locale).from(now.value) ? dayjs(date).locale(locale).fromNow()
: '' : ''
} }

View File

@ -30,7 +30,6 @@ export const SUPPORTED_LOCALES = {
'ja-JP': '日本語', 'ja-JP': '日本語',
'hu-HU': 'Magyar', 'hu-HU': 'Magyar',
'ar-SA': 'اَلْعَرَبِيَّةُ', 'ar-SA': 'اَلْعَرَبِيَّةُ',
'fa-IR': 'فارسی',
'sl-SI': 'Slovenščina', 'sl-SI': 'Slovenščina',
'pt-BR': 'Português Brasileiro', 'pt-BR': 'Português Brasileiro',
'hr-HR': 'Hrvatski', 'hr-HR': 'Hrvatski',
@ -53,7 +52,7 @@ export const DEFAULT_LANGUAGE: SupportedLocale= 'en'
export type ISOLanguage = string 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 { export function isRTLLanguage(locale: SupportedLocale): boolean {
return RTL_LANGUAGES.includes(locale as typeof RTL_LANGUAGES[number]) return RTL_LANGUAGES.includes(locale as typeof RTL_LANGUAGES[number])

View File

@ -349,7 +349,6 @@
"shared": "Geteilte Projekte", "shared": "Geteilte Projekte",
"noDescriptionAvailable": "Keine Projektbeschreibung verfügbar.", "noDescriptionAvailable": "Keine Projektbeschreibung verfügbar.",
"inboxTitle": "Eingang", "inboxTitle": "Eingang",
"myOpenTasksFilterTitle": "Meine offenen Aufgaben",
"favorite": "Dieses Projekt als Favorit markieren", "favorite": "Dieses Projekt als Favorit markieren",
"unfavorite": "Dieses Projekt von Favoriten entfernen", "unfavorite": "Dieses Projekt von Favoriten entfernen",
"openSettingsMenu": "Projekteinstellungen öffnen", "openSettingsMenu": "Projekteinstellungen öffnen",
@ -394,7 +393,6 @@
"title": "Dupliziere dieses Projekt", "title": "Dupliziere dieses Projekt",
"label": "Duplizieren", "label": "Duplizieren",
"text": "Wähle ein übergeordnetes Projekt aus, welches das duplizierte Projekt enthalten soll:", "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." "success": "Das Projekt wurde erfolgreich dupliziert."
}, },
"edit": { "edit": {

View File

@ -349,7 +349,6 @@
"shared": "Geteilte Projekte", "shared": "Geteilte Projekte",
"noDescriptionAvailable": "Keine Projektbeschreibung verfügbar.", "noDescriptionAvailable": "Keine Projektbeschreibung verfügbar.",
"inboxTitle": "Eingang", "inboxTitle": "Eingang",
"myOpenTasksFilterTitle": "Meine offenen Aufgaben",
"favorite": "Dieses Projekt als Favorit markieren", "favorite": "Dieses Projekt als Favorit markieren",
"unfavorite": "Dieses Projekt von Favoriten entfernen", "unfavorite": "Dieses Projekt von Favoriten entfernen",
"openSettingsMenu": "Projekteinstellungen öffnen", "openSettingsMenu": "Projekteinstellungen öffnen",
@ -394,7 +393,6 @@
"title": "Dupliziere dieses Projekt", "title": "Dupliziere dieses Projekt",
"label": "Duplizieren", "label": "Duplizieren",
"text": "Wähle ein übergeordnetes Projekt aus, welches das duplizierte Projekt enthalten soll:", "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." "success": "Das Projekt wurde erfolgreich dupliziert."
}, },
"edit": { "edit": {

View File

@ -172,7 +172,6 @@
"yyyy/mm/dd": "ΕΕΕΕ/ΜΜ/ΗΗ" "yyyy/mm/dd": "ΕΕΕΕ/ΜΜ/ΗΗ"
}, },
"timeFormat": "Μορφή ώρας", "timeFormat": "Μορφή ώρας",
"timeTrackingDefaultStart": "Ώρα έναρξης παρακολούθησης χρόνου με έξυπνο-γέμισμα",
"timeFormatOptions": { "timeFormatOptions": {
"12h": "12 ώρες (ΠΜ/ΜΜ)", "12h": "12 ώρες (ΠΜ/ΜΜ)",
"24h": "24 ώρες (ΩΩ:ΛΛ)" "24h": "24 ώρες (ΩΩ:ΛΛ)"
@ -393,7 +392,6 @@
"title": "Αντιγραφή του έργου", "title": "Αντιγραφή του έργου",
"label": "Αντιγραφή", "label": "Αντιγραφή",
"text": "Επιλέξτε ένα γονικό έργο που θα περιλαμβάνει το αντίγραφο του έργου:", "text": "Επιλέξτε ένα γονικό έργο που θα περιλαμβάνει το αντίγραφο του έργου:",
"shares": "Αντιγραφή διαμοιρασμών (χρήστες, ομάδες και σύνδεσμοι διαμοιρασμού) στο αντίγραφο",
"success": "Το έργο αντιγράφηκε με επιτυχία." "success": "Το έργο αντιγράφηκε με επιτυχία."
}, },
"edit": { "edit": {
@ -783,10 +781,7 @@
"closeDialog": "Κλείσμο του διαλόγου", "closeDialog": "Κλείσμο του διαλόγου",
"closeQuickActions": "Κλείσιμο των γρήγορων ενεργειών", "closeQuickActions": "Κλείσιμο των γρήγορων ενεργειών",
"skipToContent": "Μετάβαση στο κύριο περιεχόμενο", "skipToContent": "Μετάβαση στο κύριο περιεχόμενο",
"sortBy": "Ταξινόμηση ανά", "sortBy": "Ταξινόμηση ανά"
"dateRange": "Εύρος ημερομηνιών",
"notSet": "Μη ορισμένο",
"user": "Χρήστης"
}, },
"input": { "input": {
"projectColor": "Χρώμα έργου", "projectColor": "Χρώμα έργου",
@ -996,7 +991,6 @@
"repeatAfter": "Ορισμός Επαναλαμβανόμενου Διαστήματος", "repeatAfter": "Ορισμός Επαναλαμβανόμενου Διαστήματος",
"percentDone": "Ορισμός Προόδου", "percentDone": "Ορισμός Προόδου",
"attachments": "Προσθήκη Συνημμένων", "attachments": "Προσθήκη Συνημμένων",
"timeTracking": "Χρόνος ίχνους",
"relatedTasks": "Προσθήκη Συσχέτισης", "relatedTasks": "Προσθήκη Συσχέτισης",
"moveProject": "Μετακίνηση", "moveProject": "Μετακίνηση",
"duplicate": "Αντιγραφή", "duplicate": "Αντιγραφή",
@ -1466,32 +1460,6 @@
"frontendVersion": "Έκδοση frontend: {version}", "frontendVersion": "Έκδοση frontend: {version}",
"apiVersion": "Έκδοση API: {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": { "time": {
"units": { "units": {
"seconds": "δευτερόλεπτο|δευτερόλεπτα", "seconds": "δευτερόλεπτο|δευτερόλεπτα",

View File

@ -349,7 +349,6 @@
"shared": "Shared Projects", "shared": "Shared Projects",
"noDescriptionAvailable": "No project description is available.", "noDescriptionAvailable": "No project description is available.",
"inboxTitle": "Inbox", "inboxTitle": "Inbox",
"myOpenTasksFilterTitle": "My Open Tasks",
"favorite": "Mark this project as favorite", "favorite": "Mark this project as favorite",
"unfavorite": "Remove this project from favorites", "unfavorite": "Remove this project from favorites",
"openSettingsMenu": "Open project settings menu", "openSettingsMenu": "Open project settings menu",
@ -394,7 +393,6 @@
"title": "Duplicate this project", "title": "Duplicate this project",
"label": "Duplicate", "label": "Duplicate",
"text": "Select a parent project which should hold the duplicated project:", "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." "success": "The project was successfully duplicated."
}, },
"edit": { "edit": {

View File

@ -5,32 +5,9 @@
}, },
"home": { "home": {
"welcomeNight": "Доброй ночи, {username}!", "welcomeNight": "Доброй ночи, {username}!",
"welcomeNightOwl": "Привет, ночная сова {username}",
"welcomeNightBurning": "Работаешь допоздна, {username}?",
"welcomeNightQuiet": "Тихие часы, {username}",
"welcomeNightLate": "Поздно, {username}",
"welcomeMorning": "Доброе утро, {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}!", "welcomeDay": "Привет, {username}!",
"welcomeDayFocus": "Давайте сосредоточимся, {username}",
"welcomeDayKeepGoing": "Так держать, {username}",
"welcomeDayWhatsNext": "Что дальше, {username}?",
"welcomeDayGood": "Добрый день, {username}",
"welcomeEvening": "Добрый вечер, {username}!", "welcomeEvening": "Добрый вечер, {username}!",
"welcomeEveningWind": "Заканчиваешь, {username}?",
"welcomeEveningReturns": "{username} возвращается",
"welcomeEveningOneMore": "Ещё одна вещь, {username}?",
"lastViewed": "Последние просмотренные", "lastViewed": "Последние просмотренные",
"addToHomeScreen": "Добавьте это приложение на домашний экран для быстрого доступа и удобной работы.", "addToHomeScreen": "Добавьте это приложение на домашний экран для быстрого доступа и удобной работы.",
"goToOverview": "Перейти к обзору", "goToOverview": "Перейти к обзору",
@ -80,11 +57,6 @@
"openIdTotpSubmit": "Продолжить", "openIdTotpSubmit": "Продолжить",
"oauthMissingParams": "Отсутствуют необходимые параметры OAuth: {params}", "oauthMissingParams": "Отсутствуют необходимые параметры OAuth: {params}",
"oauthRedirectedToApp": "Вы были перенаправлены в приложение. Теперь вы можете закрыть эту вкладку.", "oauthRedirectedToApp": "Вы были перенаправлены в приложение. Теперь вы можете закрыть эту вкладку.",
"desktopTryDemo": "Попробовать демо-версию",
"desktopCustomServer": "Пользовательский URL сервера",
"desktopCustomServerDescription": "Введите URL сервера Vikunja, чтобы начать.",
"desktopWaitingForAuth": "Ожидание аутентификации…",
"desktopOAuthError": "Ошибка аутентификации: {error}",
"logout": "Выйти", "logout": "Выйти",
"emailInvalid": "Введите корректный email адрес.", "emailInvalid": "Введите корректный email адрес.",
"usernameRequired": "Введите имя пользователя.", "usernameRequired": "Введите имя пользователя.",
@ -103,19 +75,6 @@
"registrationFailed": "Произошла ошибка при регистрации. Проверьте введённые данные и повторите попытку." "registrationFailed": "Произошла ошибка при регистрации. Проверьте введённые данные и повторите попытку."
}, },
"settings": { "settings": {
"bots": {
"title": "Боты",
"description": "Боты — это пользователи, которые принадлежат вам и которые имеют доступ только к API. Их можно добавить в проекты, назначить задачи, и аутентификация выполняется с помощью токенов API. Боты не могут использовать обычный интерфейс.",
"namePlaceholder": "Мой помощник",
"create": "Создать бота",
"enable": "Включить",
"badge": "Бот",
"delete": {
"header": "Удалить бота",
"text1": "Удалить бота «{username}»?",
"text2": "Это необратимо. Любые токены API, принадлежащие этому боту, будут аннулированы."
}
},
"title": "Настройки", "title": "Настройки",
"newPasswordTitle": "Изменить пароль", "newPasswordTitle": "Изменить пароль",
"newPassword": "Новый пароль", "newPassword": "Новый пароль",
@ -141,11 +100,6 @@
"weekStart": "Первый день недели", "weekStart": "Первый день недели",
"weekStartSunday": "Воскресенье", "weekStartSunday": "Воскресенье",
"weekStartMonday": "Понедельник", "weekStartMonday": "Понедельник",
"weekStartTuesday": "Вторник",
"weekStartWednesday": "Среда",
"weekStartThursday": "Четверг",
"weekStartFriday": "Пятница",
"weekStartSaturday": "Суббота",
"language": "Язык", "language": "Язык",
"defaultProject": "Проект по умолчанию", "defaultProject": "Проект по умолчанию",
"defaultView": "Представление по умолчанию", "defaultView": "Представление по умолчанию",
@ -179,13 +133,7 @@
"taskAndNotifications": "Проекты и задачи", "taskAndNotifications": "Проекты и задачи",
"privacy": "Конфиденциальность", "privacy": "Конфиденциальность",
"localization": "Локализация", "localization": "Локализация",
"appearance": "Внешний вид и поведение", "appearance": "Внешний вид и поведение"
"desktop": "Настольное приложение"
},
"desktop": {
"quickEntryShortcut": "Ярлык быстрого входа",
"shortcutRecorderPlaceholder": "Нажмите, чтобы задать ярлык",
"shortcutRecorderRecording": "Нажмите комбинацию клавиш…"
}, },
"totp": { "totp": {
"title": "Двухфакторная аутентификация", "title": "Двухфакторная аутентификация",
@ -215,13 +163,6 @@
"usernameIs": "Имя пользователя для CalDAV: {0}", "usernameIs": "Имя пользователя для CalDAV: {0}",
"apiTokenHint": "Вы также можете использовать токен API с разрешением CalDAV. Создайте его в {link}." "apiTokenHint": "Вы также можете использовать токен API с разрешением CalDAV. Создайте его в {link}."
}, },
"feeds": {
"title": "Atom-лента",
"howTo": "Вы можете подписаться на уведомления Vikunja в любом приложении для чтения новостей, поддерживающем Atom-ленты. Используйте следующий URL:",
"usernameIs": "Имя пользователя для доступа к ленте: {0}",
"apiTokenHint": "Для аутентификации используйте токен API с разрешением {scope}. Создайте его на странице {link}.",
"tokenTitle": "Atom-лента"
},
"avatar": { "avatar": {
"title": "Аватар", "title": "Аватар",
"initials": "Инициалы", "initials": "Инициалы",
@ -344,7 +285,6 @@
"shared": "Общие проекты", "shared": "Общие проекты",
"noDescriptionAvailable": "Описание проекта отсутствует.", "noDescriptionAvailable": "Описание проекта отсутствует.",
"inboxTitle": "Входящие", "inboxTitle": "Входящие",
"myOpenTasksFilterTitle": "Мои открытые задачи",
"favorite": "Отметить проект как избранный", "favorite": "Отметить проект как избранный",
"unfavorite": "Удалить проект из избранного", "unfavorite": "Удалить проект из избранного",
"openSettingsMenu": "Открыть настройки проекта", "openSettingsMenu": "Открыть настройки проекта",
@ -389,7 +329,6 @@
"title": "Создание копии проекта", "title": "Создание копии проекта",
"label": "Создать копию", "label": "Создать копию",
"text": "Выберите родительский проект, в который поместить копию проекта:", "text": "Выберите родительский проект, в который поместить копию проекта:",
"shares": "Скопировать настройки доступа (пользователей, групп и ссылок для обмена)",
"success": "Копия проекта создана." "success": "Копия проекта создана."
}, },
"edit": { "edit": {
@ -486,8 +425,7 @@
"partialDatesStart": "Только дата начала (без окончания)", "partialDatesStart": "Только дата начала (без окончания)",
"partialDatesEnd": "Только дата окончания (без начала)", "partialDatesEnd": "Только дата окончания (без начала)",
"expandGroup": "Развернуть группу: {task}", "expandGroup": "Развернуть группу: {task}",
"collapseGroup": "Свернуть группу: {task}", "collapseGroup": "Свернуть группу: {task}"
"toggleRelationArrows": "Переключить стрелки связи"
}, },
"table": { "table": {
"title": "Таблица", "title": "Таблица",
@ -516,8 +454,7 @@
"bucketTitleSavedSuccess": "Название колонки сохранено.", "bucketTitleSavedSuccess": "Название колонки сохранено.",
"bucketLimitSavedSuccess": "Лимит колонки сохранён.", "bucketLimitSavedSuccess": "Лимит колонки сохранён.",
"collapse": "Свернуть эту колонку", "collapse": "Свернуть эту колонку",
"bucketLimitReached": "Вы достигли лимита колонки. Удалите какие-нибудь задачи или увеличьте лимит, чтобы добавить новые задачи.", "bucketLimitReached": "Вы достигли лимита колонки. Удалите какие-нибудь задачи или увеличьте лимит, чтобы добавить новые задачи."
"bucketOptions": "Настройки колонки"
}, },
"pseudo": { "pseudo": {
"favorites": { "favorites": {
@ -740,9 +677,7 @@
"upcoming": "Предстоящие задачи", "upcoming": "Предстоящие задачи",
"settings": "Настройки", "settings": "Настройки",
"imprint": "Отпечаток", "imprint": "Отпечаток",
"privacy": "Политика конфиденциальности", "privacy": "Политика конфиденциальности"
"closeSidebar": "Закрыть боковую панель",
"home": "Главная страница Vikunja"
}, },
"misc": { "misc": {
"loading": "Загрузка…", "loading": "Загрузка…",
@ -774,17 +709,9 @@
"createdBy": "Создатель {0}", "createdBy": "Создатель {0}",
"actions": "Действия", "actions": "Действия",
"cannotBeUndone": "Это действие отменить нельзя!", "cannotBeUndone": "Это действие отменить нельзя!",
"avatarOfUser": "Изображение профиля {user}", "avatarOfUser": "Изображение профиля {user}"
"closeBanner": "Закрыть баннер",
"closeDialog": "Закрыть диалог",
"closeQuickActions": "Закрыть быстрые действия",
"skipToContent": "Перейти к основному содержимому",
"dateRange": "Диапазон",
"notSet": "Не задано",
"user": "Пользователь"
}, },
"input": { "input": {
"projectColor": "Цвет проекта",
"resetColor": "Сбросить цвет", "resetColor": "Сбросить цвет",
"datepicker": { "datepicker": {
"today": "Сегодня", "today": "Сегодня",
@ -857,7 +784,6 @@
"date": "Дата", "date": "Дата",
"ranges": { "ranges": {
"today": "Сегодня", "today": "Сегодня",
"tomorrow": "Завтра",
"thisWeek": "Эта неделя", "thisWeek": "Эта неделя",
"restOfThisWeek": "Остаток этой недели", "restOfThisWeek": "Остаток этой недели",
"nextWeek": "Следующая неделя", "nextWeek": "Следующая неделя",
@ -965,8 +891,6 @@
"belongsToProject": "Задача принадлежит проекту «{project}»", "belongsToProject": "Задача принадлежит проекту «{project}»",
"back": "Вернуться к проекту", "back": "Вернуться к проекту",
"due": "Истекает {at}", "due": "Истекает {at}",
"closeTaskDetail": "Закрыть детали задачи",
"title": "Детали задачи",
"scrollToBottom": "Прокрутить до конца страницы", "scrollToBottom": "Прокрутить до конца страницы",
"organization": "Организация", "organization": "Организация",
"management": "Управление", "management": "Управление",
@ -1060,10 +984,7 @@
"addedSuccess": "Комментарий добавлен.", "addedSuccess": "Комментарий добавлен.",
"permalink": "Скопировать постоянную ссылку на комментарий", "permalink": "Скопировать постоянную ссылку на комментарий",
"sortNewestFirst": "Сначала новые", "sortNewestFirst": "Сначала новые",
"sortOldestFirst": "Сначала старые", "sortOldestFirst": "Сначала старые"
"reply": "Ответить",
"jumpToOriginal": "Перейти к исходному комментарию",
"deletedComment": "удалённый комментарий"
}, },
"mention": { "mention": {
"noUsersFound": "Пользователи не найдены" "noUsersFound": "Пользователи не найдены"
@ -1327,11 +1248,9 @@
"none": "Уведомлений нет. Хорошего дня!", "none": "Уведомлений нет. Хорошего дня!",
"explainer": "Здесь появятся уведомления, когда что-нибудь произойдёт с проектами или задачами, на которые вы подписаны.", "explainer": "Здесь появятся уведомления, когда что-нибудь произойдёт с проектами или задачами, на которые вы подписаны.",
"markAllRead": "Отметить всё как прочитанное", "markAllRead": "Отметить всё как прочитанное",
"markAllReadSuccess": "Все уведомления отмечены как прочитанные.", "markAllReadSuccess": "Все уведомления отмечены как прочитанные."
"subscribeFeed": "Подписаться на уведомления через Atom-ленту"
}, },
"quickActions": { "quickActions": {
"notLoggedIn": "Сначала войдите в главное окно Vikunja.",
"commands": "Команды", "commands": "Команды",
"placeholder": "Введите команду или поисковый запрос…", "placeholder": "Введите команду или поисковый запрос…",
"hint": "Используйте {project}, чтобы ограничить поиск проектом. Комбинируйте {project} и {label} (метки) с поисковым запросом для поиска задачи с этими метками или на этом проекте. Используйте {assignee} для поиска команд.", "hint": "Используйте {project}, чтобы ограничить поиск проектом. Комбинируйте {project} и {label} (метки) с поисковым запросом для поиска задачи с этими метками или на этом проекте. Используйте {assignee} для поиска команд.",
@ -1458,66 +1377,5 @@
"weeks": "неделя|недели|недель", "weeks": "неделя|недели|недель",
"years": "год|года|лет" "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": "Новый владелец"
}
} }
} }

View File

@ -172,7 +172,6 @@
"yyyy/mm/dd": "YYYY/MM/DD" "yyyy/mm/dd": "YYYY/MM/DD"
}, },
"timeFormat": "Формат часу", "timeFormat": "Формат часу",
"timeTrackingDefaultStart": "Початковий час для автоматичного заповнення обліку часу",
"timeFormatOptions": { "timeFormatOptions": {
"12h": "12-годинний (AM/PM)", "12h": "12-годинний (AM/PM)",
"24h": "24-годинний (HH:mm)" "24h": "24-годинний (HH:mm)"
@ -393,7 +392,6 @@
"title": "Дублювати цей проєкт", "title": "Дублювати цей проєкт",
"label": "Дублювати", "label": "Дублювати",
"text": "Оберіть батьківський проєкт, який повинен складатися з дубльованих проєктів:", "text": "Оберіть батьківський проєкт, який повинен складатися з дубльованих проєктів:",
"shares": "Скопіювати налаштування спільного доступу (користувачів, команди та посилання) до копії проєкту",
"success": "Проєкт дубльовано." "success": "Проєкт дубльовано."
}, },
"edit": { "edit": {
@ -783,10 +781,7 @@
"closeDialog": "Закрити діалог", "closeDialog": "Закрити діалог",
"closeQuickActions": "Закрити швидкі дії", "closeQuickActions": "Закрити швидкі дії",
"skipToContent": "Перейти до основного вмісту", "skipToContent": "Перейти до основного вмісту",
"sortBy": "Сортувати за", "sortBy": "Сортувати за"
"dateRange": "Діапазон дат",
"notSet": "Не встановлено",
"user": "Користувач"
}, },
"input": { "input": {
"projectColor": "Колір проєкту", "projectColor": "Колір проєкту",
@ -989,14 +984,13 @@
"assign": "Доручити", "assign": "Доручити",
"label": "Позначки", "label": "Позначки",
"priority": "Встановити пріоритет", "priority": "Встановити пріоритет",
"dueDate": "Встановити термін виконання", "dueDate": "Встановити термін",
"startDate": "Почати", "startDate": "Почати",
"endDate": "Встановити дату завершення", "endDate": "Встановити дату завершення",
"reminders": "Нагадування", "reminders": "Нагадування",
"repeatAfter": "Повторювати", "repeatAfter": "Повторювати",
"percentDone": "Встановити прогрес", "percentDone": "Встановити прогрес",
"attachments": "Вкласти", "attachments": "Вкласти",
"timeTracking": "Відстежити час",
"relatedTasks": "Пов'язати", "relatedTasks": "Пов'язати",
"moveProject": "Перемістити", "moveProject": "Перемістити",
"duplicate": "Дублювати", "duplicate": "Дублювати",
@ -1152,7 +1146,6 @@
"repeat": { "repeat": {
"everyDay": "Щодня", "everyDay": "Щодня",
"everyWeek": "Щотижня", "everyWeek": "Щотижня",
"every30d": "Кожні 30 днів",
"mode": "Спосіб", "mode": "Спосіб",
"monthly": "Щомісяця", "monthly": "Щомісяця",
"fromCurrentDate": "З дня закінчення", "fromCurrentDate": "З дня закінчення",
@ -1466,32 +1459,6 @@
"frontendVersion": "Версія інтерфейсу: {version}", "frontendVersion": "Версія інтерфейсу: {version}",
"apiVersion": "API версія: {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": { "time": {
"units": { "units": {
"seconds": "секунда|секунд(и)", "seconds": "секунда|секунд(и)",

View File

@ -22,7 +22,6 @@ export const DAYJS_LOCALE_MAPPING = {
'ja-jp': 'ja', 'ja-jp': 'ja',
'hu-hu': 'hu', 'hu-hu': 'hu',
'ar-sa': 'ar-sa', 'ar-sa': 'ar-sa',
'fa-ir': 'fa',
'sl-si': 'sl', 'sl-si': 'sl',
'pt-br': 'pt', 'pt-br': 'pt',
'hr-hr': 'hr', 'hr-hr': 'hr',
@ -56,7 +55,6 @@ export const DAYJS_LANGUAGE_IMPORTS = {
'ja-jp': () => import('dayjs/locale/ja'), 'ja-jp': () => import('dayjs/locale/ja'),
'hu-hu': () => import('dayjs/locale/hu'), 'hu-hu': () => import('dayjs/locale/hu'),
'ar-sa': () => import('dayjs/locale/ar-sa'), 'ar-sa': () => import('dayjs/locale/ar-sa'),
'fa-ir': () => import('dayjs/locale/fa'),
'sl-si': () => import('dayjs/locale/sl'), 'sl-si': () => import('dayjs/locale/sl'),
'pt-br': () => import('dayjs/locale/pt-br'), 'pt-br': () => import('dayjs/locale/pt-br'),
'hr-hr': () => import('dayjs/locale/hr'), 'hr-hr': () => import('dayjs/locale/hr'),

View File

@ -5,5 +5,4 @@ export interface IProjectDuplicate extends IAbstract {
projectId: number projectId: number
duplicatedProject: IProject | null duplicatedProject: IProject | null
parentProjectId: IProject['id'] parentProjectId: IProject['id']
duplicateShares: boolean
} }

View File

@ -8,7 +8,6 @@ export default class ProjectDuplicateModel extends AbstractModel<IProjectDuplica
projectId = 0 projectId = 0
duplicatedProject: IProject | null = null duplicatedProject: IProject | null = null
parentProjectId = 0 parentProjectId = 0
duplicateShares = false
constructor(data : Partial<IProjectDuplicate>) { constructor(data : Partial<IProjectDuplicate>) {
super() super()

View File

@ -6,7 +6,6 @@ import {getProjectViewId} from '@/helpers/projectView'
import {parseDateOrString} from '@/helpers/time/parseDateOrString' import {parseDateOrString} from '@/helpers/time/parseDateOrString'
import {getNextWeekDate} from '@/helpers/time/getNextWeekDate' import {getNextWeekDate} from '@/helpers/time/getNextWeekDate'
import {LINK_SHARE_HASH_PREFIX} from '@/constants/linkShareHash' import {LINK_SHARE_HASH_PREFIX} from '@/constants/linkShareHash'
import {REDIRECT_HASH_PREFIX} from '@/constants/redirectHash'
import {AUTH_ROUTE_NAMES} from '@/constants/authRouteNames' import {AUTH_ROUTE_NAMES} from '@/constants/authRouteNames'
import {PRO_FEATURE} from '@/constants/proFeatures' import {PRO_FEATURE} from '@/constants/proFeatures'
@ -31,7 +30,7 @@ const router = createRouter({
} }
// Scroll to anchor should still work // 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} return {el: to.hash}
} }
@ -473,19 +472,7 @@ const router = createRouter({
}) })
export async function getAuthForRoute(to: RouteLocation, authStore) { 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) { 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 return
} }
@ -512,26 +499,6 @@ 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 // 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. // redirect the user after successful login.
const isValidUserAppRoute = !AUTH_ROUTE_NAMES.has(to.name as string) && const isValidUserAppRoute = !AUTH_ROUTE_NAMES.has(to.name as string) &&
@ -598,25 +565,12 @@ router.beforeEach(async (to, from) => {
const newRoute = await getAuthForRoute(to, authStore) const newRoute = await getAuthForRoute(to, authStore)
if(newRoute) { 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 { return {
hash: to.hash,
...newRoute, ...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)) { if(!to.fullPath.endsWith(to.hash)) {
return to.fullPath + to.hash return to.fullPath + to.hash
} }

View File

@ -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)
})
})

View File

@ -28,11 +28,6 @@ import {TIME_FORMAT} from '@/constants/timeFormat'
import {RELATION_KIND} from '@/types/IRelationKind' import {RELATION_KIND} from '@/types/IRelationKind'
import type {IProvider} from '@/types/IProvider' 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() { function redirectToSpecifiedProvider() {
const {auth} = useConfigStore() 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 { function getLoggedInVia(): string | null {
return localStorage.getItem('loggedInViaProvider') return localStorage.getItem('loggedInViaProvider')
} }
@ -368,7 +352,7 @@ export const useAuthStore = defineStore('auth', () => {
// refresh before giving up. This lets users who reopen the app // refresh before giving up. This lets users who reopen the app
// after the short JWT TTL seamlessly resume their session. // after the short JWT TTL seamlessly resume their session.
try { try {
await refreshTokenWithRetry(true) await refreshToken(true)
const freshJwt = getToken() const freshJwt = getToken()
if (freshJwt) { if (freshJwt) {
const b64 = freshJwt.split('.')[1].replace(/-/g, '+').replace(/_/g, '/') const b64 = freshJwt.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')
@ -528,7 +512,7 @@ export const useAuthStore = defineStore('auth', () => {
saveToken(response.data.token, false) saveToken(response.data.token, false)
} else { } else {
// User sessions renew via the refresh-token cookie. // User sessions renew via the refresh-token cookie.
await refreshTokenWithRetry(true) await refreshToken(true)
} }
await checkAuth() await checkAuth()
} catch (e) { } catch (e) {
@ -549,11 +533,9 @@ export const useAuthStore = defineStore('auth', () => {
// Revoke the server session so the refresh token can't be reused. // Revoke the server session so the refresh token can't be reused.
// Best-effort: if the network call fails, still clean up locally. // Best-effort: if the network call fails, still clean up locally.
let oidcLogoutUrl = ''
try { try {
const HTTP = AuthenticatedHTTPFactory() const HTTP = AuthenticatedHTTPFactory()
const {data} = await HTTP.post('user/logout') await HTTP.post('user/logout')
oidcLogoutUrl = data?.oidc_logout_url ?? ''
} catch (_e) { } catch (_e) {
// Ignore — session will expire naturally // Ignore — session will expire naturally
} }
@ -562,25 +544,14 @@ export const useAuthStore = defineStore('auth', () => {
const loggedInVia = getLoggedInVia() const loggedInVia = getLoggedInVia()
window.localStorage.clear() // Clear all settings and history we might have saved in local storage. window.localStorage.clear() // Clear all settings and history we might have saved in local storage.
lastUserInfoRefresh.value = null 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 router.push({name: 'user.login'})
await checkAuth() 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 { return {

View File

@ -47,7 +47,6 @@ export interface ConfigState {
publicTeamsEnabled: boolean, publicTeamsEnabled: boolean,
allowIconChanges: boolean, allowIconChanges: boolean,
enabledProFeatures: string[], enabledProFeatures: string[],
concurrentWrites: boolean,
} }
export const useConfigStore = defineStore('config', () => { export const useConfigStore = defineStore('config', () => {
@ -89,7 +88,6 @@ export const useConfigStore = defineStore('config', () => {
publicTeamsEnabled: false, publicTeamsEnabled: false,
allowIconChanges: true, allowIconChanges: true,
enabledProFeatures: [], enabledProFeatures: [],
concurrentWrites: false,
}) })
const migratorsEnabled = computed(() => state.availableMigrators?.length > 0) const migratorsEnabled = computed(() => state.availableMigrators?.length > 0)

View File

@ -380,11 +380,10 @@ export function useProject(projectId: MaybeRefOrGetter<IProject['id']>) {
success({message: t('project.edit.success')}) 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({ const projectDuplicate = new ProjectDuplicateModel({
projectId: Number(toValue(projectId)), projectId: Number(toValue(projectId)),
parentProjectId, parentProjectId,
duplicateShares,
}) })
const duplicate = await projectDuplicateService.create(projectDuplicate) const duplicate = await projectDuplicateService.create(projectDuplicate)

View File

@ -1,5 +1,5 @@
import {describe, expect, it} from 'vitest' 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 {REMINDER_PERIOD_RELATIVE_TO_TYPES} from '@/types/IReminderPeriodRelativeTo'
import type {ITaskReminder} from '@/modelTypes/ITaskReminder' import type {ITaskReminder} from '@/modelTypes/ITaskReminder'
@ -42,39 +42,3 @@ describe('buildDefaultRemindersForQuickAdd', () => {
expect(result[0].relativeTo).toBe(REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE) 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)
})
})

View File

@ -27,7 +27,6 @@ import type {IProject} from '@/modelTypes/IProject'
import {REMINDER_PERIOD_RELATIVE_TO_TYPES} from '@/types/IReminderPeriodRelativeTo' import {REMINDER_PERIOD_RELATIVE_TO_TYPES} from '@/types/IReminderPeriodRelativeTo'
import {setModuleLoading} from '@/stores/helper' import {setModuleLoading} from '@/stores/helper'
import {useConfigStore} from '@/stores/config'
import {useLabelStore} from '@/stores/labels' import {useLabelStore} from '@/stores/labels'
import {useProjectStore} from '@/stores/projects' import {useProjectStore} from '@/stores/projects'
import {useKanbanStore} from '@/stores/kanban' 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 // IDEA: maybe use a small fuzzy search here to prevent errors
function findPropertyByValue(object, key, value, fuzzy = false) { function findPropertyByValue(object, key, value, fuzzy = false) {
return Object.values(object).find(l => { return Object.values(object).find(l => {
@ -148,7 +131,6 @@ export const useTaskStore = defineStore('task', () => {
const labelStore = useLabelStore() const labelStore = useLabelStore()
const projectStore = useProjectStore() const projectStore = useProjectStore()
const authStore = useAuthStore() const authStore = useAuthStore()
const configStore = useConfigStore()
const tasks = ref<{ [id: ITask['id']]: ITask }>({}) // TODO: or is this ITask[] const tasks = ref<{ [id: ITask['id']]: ITask }>({}) // TODO: or is this ITask[]
const isLoading = ref(false) const isLoading = ref(false)
@ -413,7 +395,10 @@ export const useTaskStore = defineStore('task', () => {
} }
const labels = await ensureLabelsExist(parsedLabels) 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 return task
} }

View File

@ -4,10 +4,6 @@
} }
} }
.is-pulled-end { .is-pulled-right {
float: right !important; float: right !important;
} }
[dir="rtl"] .is-pulled-end {
float: left !important;
}

View File

@ -5,7 +5,7 @@
> >
<XButton <XButton
:to="{name:'labels.create'}" :to="{name:'labels.create'}"
class="is-pulled-end" class="is-pulled-right"
icon="plus" icon="plus"
> >
{{ $t('label.create.header') }} {{ $t('label.create.header') }}

View File

@ -8,12 +8,6 @@
> >
<p>{{ $t('project.duplicate.text') }}</p> <p>{{ $t('project.duplicate.text') }}</p>
<ProjectSearch v-model="parentProject" /> <ProjectSearch v-model="parentProject" />
<FancyCheckbox
v-model="duplicateShares"
class="mbs-2"
>
{{ $t('project.duplicate.shares') }}
</FancyCheckbox>
</CreateEdit> </CreateEdit>
</template> </template>
@ -24,7 +18,6 @@ import {useI18n} from 'vue-i18n'
import CreateEdit from '@/components/misc/CreateEdit.vue' import CreateEdit from '@/components/misc/CreateEdit.vue'
import ProjectSearch from '@/components/tasks/partials/ProjectSearch.vue' import ProjectSearch from '@/components/tasks/partials/ProjectSearch.vue'
import FancyCheckbox from '@/components/input/FancyCheckbox.vue'
import {success} from '@/message' import {success} from '@/message'
import {useTitle} from '@/composables/useTitle' import {useTitle} from '@/composables/useTitle'
@ -40,7 +33,6 @@ const projectStore = useProjectStore()
const {project, isLoading, duplicateProject} = useProject(route.params.projectId) const {project, isLoading, duplicateProject} = useProject(route.params.projectId)
const parentProject = ref<IProject | null>(null) const parentProject = ref<IProject | null>(null)
const duplicateShares = ref(true)
const isDuplicating = ref(false) const isDuplicating = ref(false)
const loadingModel = computed({ const loadingModel = computed({
@ -61,7 +53,7 @@ async function duplicate() {
isDuplicating.value = true isDuplicating.value = true
try { try {
await duplicateProject(parentProject.value?.id ?? 0, duplicateShares.value) await duplicateProject(parentProject.value?.id ?? 0)
success({message: t('project.duplicate.success')}) success({message: t('project.duplicate.success')})
} finally { } finally {
isDuplicating.value = false isDuplicating.value = false

View File

@ -5,7 +5,7 @@
> >
<XButton <XButton
:to="{name:'teams.create'}" :to="{name:'teams.create'}"
class="is-pulled-end" class="is-pulled-right"
icon="plus" icon="plus"
> >
{{ $t('team.create.title') }} {{ $t('team.create.title') }}

View File

@ -136,7 +136,7 @@ import {redirectToProvider} from '@/helpers/redirectToProvider'
import {useRedirectToLastVisited} from '@/composables/useRedirectToLastVisited' import {useRedirectToLastVisited} from '@/composables/useRedirectToLastVisited'
import {isDesktopApp} from '@/helpers/desktopAuth' 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 {useConfigStore} from '@/stores/config'
import {useTitle} from '@/composables/useTitle' import {useTitle} from '@/composables/useTitle'
@ -181,25 +181,6 @@ onBeforeMount(() => {
// route before the submit() handler gets a chance to use it. // route before the submit() handler gets a chance to use it.
if (authenticated.value) { if (authenticated.value) {
router.push({name: 'home'}) 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])
} }
}) })

View File

@ -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()
})
})

View File

@ -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/)
})
})

View File

@ -32,20 +32,10 @@ test.describe('OAuth 2.0 Authorization Flow', () => {
}) })
// Navigate to the OAuth authorize frontend route. // Navigate to the OAuth authorize frontend route.
// The user is not logged in, so the router guard redirects to /login while // The user is not logged in, so the router guard saves the route
// carrying the authorize destination in a copyable #redirect= hash (not a // and redirects to /login.
// query param, to keep the OAuth params out of access logs).
await page.goto(`/oauth/authorize?${authorizeParams}`) await page.goto(`/oauth/authorize?${authorizeParams}`)
await expect(page).toHaveURL(/\/login#redirect=/) await expect(page).toHaveURL(/\/login/)
// 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}`)
// Register the response listener BEFORE clicking Login, because after // Register the response listener BEFORE clicking Login, because after
// login redirectIfSaved() navigates back to /oauth/authorize and the // 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.token_type).toBe('bearer')
expect(tokenBody.expires_in).toBeGreaterThan(0) 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
View File

@ -16,58 +16,56 @@
module code.vikunja.io/api module code.vikunja.io/api
go 1.26.4 go 1.25.7
require ( require (
code.dny.dev/ssrf v0.2.0 code.dny.dev/ssrf v0.2.0
dario.cat/mergo v1.0.2 dario.cat/mergo v1.0.2
github.com/JohannesKaufmann/dom v0.3.1 github.com/ThreeDotsLabs/watermill v1.5.1
github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.2
github.com/ThreeDotsLabs/watermill v1.5.2
github.com/adlio/trello v1.12.0 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/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
github.com/aws/aws-sdk-go-v2 v1.42.0 github.com/aws/aws-sdk-go-v2 v1.41.5
github.com/aws/aws-sdk-go-v2/config v1.32.26 github.com/aws/aws-sdk-go-v2/config v1.32.10
github.com/aws/aws-sdk-go-v2/credentials v1.19.25 github.com/aws/aws-sdk-go-v2/credentials v1.19.10
github.com/aws/aws-sdk-go-v2/service/s3 v1.104.1 github.com/aws/aws-sdk-go-v2/service/s3 v1.97.3
github.com/aws/smithy-go v1.27.3 github.com/aws/smithy-go v1.24.2
github.com/bbrks/go-blurhash v1.2.0 github.com/bbrks/go-blurhash v1.1.1
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500 github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500
github.com/coder/websocket v1.8.15 github.com/coder/websocket v1.8.14
github.com/coreos/go-oidc/v3 v3.19.0 github.com/coreos/go-oidc/v3 v3.17.0
github.com/d4l3k/messagediff v1.2.1 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/disintegration/imaging v1.6.2
github.com/dustinkirkland/golang-petname v0.0.0-20240422154211-76c06c4bde6b 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/gabriel-vasile/mimetype v1.4.13
github.com/ganigeorgiev/fexpr v0.5.0 github.com/ganigeorgiev/fexpr v0.5.0
github.com/getsentry/sentry-go v0.41.0 github.com/getsentry/sentry-go v0.41.0
github.com/go-ldap/ldap/v3 v3.4.13 github.com/go-ldap/ldap/v3 v3.4.12
github.com/go-sql-driver/mysql v1.10.0 github.com/go-sql-driver/mysql v1.9.3
github.com/go-testfixtures/testfixtures/v3 v3.19.0 github.com/go-testfixtures/testfixtures/v3 v3.19.0
github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a 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/google/uuid v1.6.0
github.com/gorilla/feeds v1.2.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/hhsnopek/etag v0.0.0-20171206181245-aea95f647346
github.com/huandu/go-clone/generic v1.7.3 github.com/huandu/go-clone/generic v1.7.3
github.com/iancoleman/strcase v0.3.0 github.com/iancoleman/strcase v0.3.0
github.com/jaswdr/faker/v2 v2.9.1 github.com/jaswdr/faker/v2 v2.9.1
github.com/jinzhu/copier v0.4.0 github.com/jinzhu/copier v0.4.0
github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6
github.com/labstack/echo-jwt/v5 v5.0.1 github.com/labstack/echo-jwt/v5 v5.0.0
github.com/labstack/echo/v5 v5.2.1 github.com/labstack/echo/v5 v5.0.3
github.com/lib/pq v1.12.3 github.com/lib/pq v1.10.9
github.com/magefile/mage v1.17.2 github.com/magefile/mage v1.15.0
github.com/mattn/go-sqlite3 v1.14.47 github.com/mattn/go-sqlite3 v1.14.33
github.com/microcosm-cc/bluemonday v1.0.27 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/pquerna/otp v1.5.0
github.com/prometheus/client_golang v1.23.2 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/robfig/cron/v3 v3.0.1
github.com/samedi/caldav-go v3.0.0+incompatible github.com/samedi/caldav-go v3.0.0+incompatible
github.com/schollz/progressbar/v3 v3.19.0 github.com/schollz/progressbar/v3 v3.19.0
@ -78,43 +76,43 @@ require (
github.com/tkuchiki/go-timezone v0.2.3 github.com/tkuchiki/go-timezone v0.2.3
github.com/traefik/yaegi v0.16.1 github.com/traefik/yaegi v0.16.1
github.com/ulule/limiter/v3 v3.11.2 github.com/ulule/limiter/v3 v3.11.2
github.com/wneessen/go-mail v0.7.3 github.com/wneessen/go-mail v0.7.2
github.com/yuin/goldmark v1.8.2 github.com/yuin/goldmark v1.7.16
golang.org/x/crypto v0.53.0 golang.org/x/crypto v0.48.0
golang.org/x/image v0.38.0 golang.org/x/image v0.38.0
golang.org/x/net v0.55.0 golang.org/x/net v0.50.0
golang.org/x/oauth2 v0.36.0 golang.org/x/oauth2 v0.34.0
golang.org/x/sync v0.21.0 golang.org/x/sync v0.20.0
golang.org/x/sys v0.46.0 golang.org/x/sys v0.41.0
golang.org/x/term v0.44.0 golang.org/x/term v0.40.0
golang.org/x/text v0.38.0 golang.org/x/text v0.35.0
gopkg.in/d4l3k/messagediff.v1 v1.2.1 gopkg.in/d4l3k/messagediff.v1 v1.2.1
mvdan.cc/xurls/v2 v2.6.0 mvdan.cc/xurls/v2 v2.6.0
src.techknowlogick.com/xormigrate v1.7.1 src.techknowlogick.com/xormigrate v1.7.1
xorm.io/builder v0.3.13 xorm.io/builder v0.3.13
xorm.io/xorm v1.4.1 xorm.io/xorm v1.3.11
) )
require ( 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-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Azure/go-ntlmssp v0.1.1 // indirect github.com/Azure/go-ntlmssp v0.1.1 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect
github.com/Microsoft/go-winio v0.6.2 // 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/aws/protocol/eventstream v1.7.8 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 // 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.29 // 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.29 // 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/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/internal/v4a v1.4.22 // 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/accept-encoding v1.13.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22 // 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.29 // 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.30 // 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.2.1 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.31.4 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.7 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.43.4 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect
github.com/aymerick/douceur v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect
github.com/beevik/etree v1.1.0 // indirect github.com/beevik/etree v1.1.0 // indirect
github.com/beorn7/perks v1.0.1 // 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/evanphx/json-patch/v5 v5.9.11 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // 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-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/go-chi/chi/v5 v5.2.5 // indirect github.com/go-chi/chi/v5 v5.2.5 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // 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/spec v0.20.4 // indirect
github.com/go-openapi/swag v0.23.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.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/goccy/go-yaml v1.19.2 // indirect
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/gorilla/css v1.0.1 // 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/lithammer/shortuuid/v3 v3.0.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.21 // indirect github.com/mattn/go-runewidth v0.0.20 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/moby/api v1.53.0 // 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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oklog/ulid v1.3.1 // indirect github.com/oklog/ulid v1.3.1 // indirect
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
github.com/olekukonko/errors v1.2.0 // indirect github.com/olekukonko/errors v1.1.0 // indirect
github.com/olekukonko/ll v0.1.6 // indirect github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 // indirect
github.com/onsi/ginkgo v1.16.4 // indirect github.com/onsi/ginkgo v1.16.4 // indirect
github.com/onsi/gomega v1.16.0 // indirect github.com/onsi/gomega v1.16.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // 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/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // 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/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.20.1 // indirect github.com/prometheus/procfs v0.17.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.11.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 v1.41.0 // indirect
go.opentelemetry.io/otel/metric v1.41.0 // indirect go.opentelemetry.io/otel/metric v1.41.0 // indirect
go.opentelemetry.io/otel/trace 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.2 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9 // 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/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 google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect

179
go.sum
View File

@ -4,8 +4,6 @@ dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= 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 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw=
filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 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 h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU= 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= 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 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw=
github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk= 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/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 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= 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= 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/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 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=
github.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4= 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 h1:JqOE2GFHQ9YtEviRRRSnicSxPbt4WFOxhqXzjMOw8lw=
github.com/adlio/trello v1.12.0/go.mod h1:I4Lti4jf2KxjTNgTqs5W3lLuE78QZZdYbbPnQQGwjOo= 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 h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= 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 h1:MGNjcXJFSuCXmYX/RpZhR2HDCYoFuK8vTPFLEdFC3JY=
github.com/arran4/golang-ical v0.3.2/go.mod h1:xblDGxxIUMWwFZk9dlECUlc1iXNV65LJZOTHLVwu8bo= 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 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= 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 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.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 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.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 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.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 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.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 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.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 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.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 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.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 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/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 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.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 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.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 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.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 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.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 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.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 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.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 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.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 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.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 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.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 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.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 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= 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 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= 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 h1:uoXOxRPDca9zHYabUTwvS4KnY++KKUbwFo+Yxb8ME4M=
github.com/bbrks/go-blurhash v1.1.1/go.mod h1:lkAsdyXp+EhARcUo85yS2G1o+Sh43I2ebF5togC4bAY= 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 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 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/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 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= 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 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= 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 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= 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 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= 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-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/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= 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/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 h1:6Av0Vj45Vk5lDxRVfoO2iPlEdvCvwLc7pl5nbqGOkYM=
github.com/danielgtaylor/huma/v2 v2.37.3/go.mod h1:OeHHtCEAaNiuVbAVdYu4IQ0UOmnb4x3yMUOShNlZ53g= 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 h1:nA9bsGRmNlJeVCPFgGf7WhrLuKag/+iWfOaJ03iKFPI=
github.com/danielgtaylor/mexpr v1.9.1/go.mod h1:kAivYNRnBeE/IJinqBvVFvLrX54xX//9zFYwADo4Bc8= github.com/danielgtaylor/mexpr v1.9.1/go.mod h1:kAivYNRnBeE/IJinqBvVFvLrX54xX//9zFYwADo4Bc8=
github.com/danielgtaylor/shorthand/v2 v2.2.0 h1:hVsemdRq6v3JocP6YRTfu9rOoghZI9PFmkngdKqzAVQ= 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/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 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 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 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 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/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 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.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 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= 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= 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-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 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo= 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-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.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 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.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 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= 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-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-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= 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.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 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.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 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 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/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 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.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-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 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 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-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 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 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 h1:Odeq5rB6OZSkib5gqTG+EM1iF0bUVjYYd33XB1ULv00=
github.com/hhsnopek/etag v0.0.0-20171206181245-aea95f647346/go.mod h1:4ggHM2qnyyZjenBb7RpwVzIj+JMsu9kHCVxMjB30hGs= 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= 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/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 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.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 h1:Jql8sDtCYXrhh2Mbs6jKwjR6r7X8FSQQmch+w6QS7kc=
github.com/labstack/echo/v5 v5.0.3/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo= 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 h1:RZnRnSID1skF35j/15KJ6hKZkdIC/teQClJK5wP5LU4=
github.com/laurent22/ical-go v0.1.1-0.20181107184520-7e5d6ade8eef/go.mod h1:4LATl0uhhtytR6p9n1AlktDyIz4u2iUnWEdI3L/hXiw= 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= 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.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 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.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 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=
github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts= 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 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= 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-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/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= 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.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.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.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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= 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 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ=
github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= 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.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.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 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.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 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= 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= 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/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 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= 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 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.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 h1:VSHhghXxrP0JHl+0NnKid7WoEmd9/urKRJLysb70nnA=
github.com/olekukonko/tablewriter v1.1.3/go.mod h1:9VU0knjhmMkXjnMKrZ3+L2JhhtsQ/L38BbL3CRNE8tM= 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.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.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 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/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 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.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 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= 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= 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/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 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= 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 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= 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 h1:fN29NdNrE17KttK5Ndf20buqfDZwGNgoUr9qjl1DQx4=
github.com/redis/go-redis/v9 v9.17.3/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= 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-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 h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 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/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 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc=
github.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec= 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 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.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.3.1/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/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 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=
github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k= 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 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 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 h1:0sw0nJM544SpsihWx1bkXdYLQDlzRflMgFJQ4Yih9ts=
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4/go.mod h1:+ccdNT0xMY1dtc5XBxumbYfOUhmduiGudqaDgD2rVRE= 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.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 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.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= 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/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= 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.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.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.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 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 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.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 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= 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 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 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= 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-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.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.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9 h1:yZNXmy+j/JpX19vZkVktWqAo7Gny4PBWYYK3zskGpx4= 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/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= 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.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.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.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.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 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-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/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-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.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.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= 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 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= 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-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-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-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.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 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= 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-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-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/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-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.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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 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-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.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.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.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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.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.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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
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/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 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 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 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-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
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/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/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.3/go.mod h1:qFJGFoVYbbIdnz2vaL5OxSQ2raleMpyRRalnq3n9OJo=
xorm.io/xorm v1.3.11 h1:i4tlVUASogb0ZZFJHA7dZqoRU2pUpUsutnNdaOlFyMI= xorm.io/xorm v1.3.11 h1:i4tlVUASogb0ZZFJHA7dZqoRU2pUpUsutnNdaOlFyMI=
xorm.io/xorm v1.3.11/go.mod h1:cs0ePc8O4a0jD78cNvD+0VFwhqotTvLQZv372QsDw7Q= 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=

View File

@ -1,4 +1,4 @@
[tools] [tools]
node = "24.18.0" # keep in sync with frontend/.nvmrc node = "24.13.0" # keep in sync with frontend/.nvmrc
pnpm = "10.34.4" # keep in sync with frontend/package.json#packageManager pnpm = "10.28.1" # keep in sync with frontend/package.json#packageManager
go = "1.26.4" # keep in sync with go.mod go = "1.25.7" # keep in sync with go.mod

46
pkg/audit/doc.go Normal file
View File

@ -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

View File

@ -14,33 +14,6 @@
// You should have received a copy of the GNU Affero General Public License // 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/>. // 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 package audit
import "time" import "time"

View File

@ -45,15 +45,27 @@ func RegisterEventForAudit[T any, PT interface {
events.Event events.Event
}](toEntry func(PT) *Entry) { }](toEntry func(PT) *Entry) {
name := PT(new(T)).Name() 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 { events.RegisterListener(name, &auditListener{handle: func(msg *message.Message) error {
if !license.IsFeatureEnabled(license.FeatureAuditLogs) { if !license.IsFeatureEnabled(license.FeatureAuditLogs) {
return nil // license is runtime-mutable — checked per event, not at registration return nil // license is runtime-mutable — checked per event, not at registration
} }
e := PT(new(T)) // fresh instance per message — handlers run concurrently entry, err := toEntry(msg.Payload)
if err := json.Unmarshal(msg.Payload, e); err != nil { if err != nil {
return err return err
} }
entry := toEntry(e)
if entry == nil { if entry == nil {
return nil return nil
} }

24
pkg/audit/sinks/sink.go Normal file
View File

@ -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
}

View File

@ -14,34 +14,31 @@
// You should have received a copy of the GNU Affero General Public License // 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/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
package richtext package sinks
import ( import (
"io"
"os" "os"
"testing" "sync"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/user"
) )
// TestMain bootstraps a test DB with user fixtures so the mention-resolution // Stdout writes each entry as one line to standard output.
// tests can look up real users. The pure converter tests don't touch the DB. type Stdout struct {
func TestMain(m *testing.M) { mu sync.Mutex
log.InitLogger() // out exists so tests can capture the output.
out io.Writer
x, err := db.CreateTestEngine() }
if err != nil {
log.Fatal(err) func NewStdout() *Stdout {
} return &Stdout{out: os.Stdout}
}
if err := x.Sync2(user.GetTables()...); err != nil {
log.Fatal(err) func (s *Stdout) Write(line []byte) error {
} s.mu.Lock()
defer s.mu.Unlock()
if err := db.InitTestFixtures("users"); err != nil { if _, err := s.out.Write(line); err != nil {
log.Fatal(err) return err
} }
_, err := s.out.Write([]byte{'\n'})
os.Exit(m.Run()) return err
} }

116
pkg/audit/sinks/syslog.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -18,7 +18,6 @@ package audit
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@ -26,6 +25,7 @@ import (
"sync" "sync"
"time" "time"
"code.vikunja.io/api/pkg/audit/sinks"
"code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/log"
@ -41,9 +41,10 @@ var (
maxSizeBytes int64 maxSizeBytes int64
maxAge time.Duration maxAge time.Duration
lastSync time.Time 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). // Safe to call again to re-read the config (used by tests).
func Init() error { func Init() error {
mu.Lock() mu.Lock()
@ -65,6 +66,13 @@ func Init() error {
return err return err
} }
var err error
forwarders, err = buildForwarders(config.AuditForwarders.Get())
if err != nil {
closeLocked()
return err
}
initialized = true initialized = true
return nil return nil
} }
@ -82,6 +90,7 @@ func closeLocked() {
_ = logFile.Close() _ = logFile.Close()
logFile = nil logFile = nil
} }
forwarders = nil
initialized = false initialized = false
} }
@ -100,8 +109,74 @@ func openLogFileLocked() error {
return nil return nil
} }
// WriteAuditEvent writes one entry to the local audit log. A failed write is func buildForwarders(raw any) (built []sinks.Sink, err error) {
// returned so the event router retries it. 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 { func WriteAuditEvent(entry *Entry) error {
if entry.EventID == "" { if entry.EventID == "" {
id, err := uuid.NewV7() id, err := uuid.NewV7()
@ -133,27 +208,24 @@ func WriteAuditEvent(entry *Entry) error {
return err 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')) written, err := logFile.Write(append(line, '\n'))
currentSize += int64(written) currentSize += int64(written)
if err == nil && time.Since(lastSync) > time.Second { if err == nil && time.Since(lastSync) > time.Second {
err = logFile.Sync() err = logFile.Sync()
lastSync = time.Now() lastSync = time.Now()
} }
currentForwarders := forwarders
mu.Unlock() mu.Unlock()
if err != nil { if err != nil {
return fmt.Errorf("could not write audit entry: %w", err) 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 return nil
} }
@ -169,9 +241,7 @@ func rotateIfNeededLocked(addition int64) error {
rotatedPath := rotatedFileName(logfilePath, time.Now().UTC()) rotatedPath := rotatedFileName(logfilePath, time.Now().UTC())
if err := os.Rename(logfilePath, rotatedPath); err != nil { if err := os.Rename(logfilePath, rotatedPath); err != nil {
// Reopen the original so logging continues even if rotation failed. // Reopen the original so logging continues even if rotation failed.
if openErr := openLogFileLocked(); openErr != nil { _ = openLogFileLocked()
return errors.Join(fmt.Errorf("could not rotate audit log: %w", err), openErr)
}
return fmt.Errorf("could not rotate audit log: %w", err) return fmt.Errorf("could not rotate audit log: %w", err)
} }

View File

@ -21,9 +21,7 @@ import (
"strings" "strings"
"time" "time"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/richtext"
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
"code.vikunja.io/api/pkg/utils" "code.vikunja.io/api/pkg/utils"
) )
@ -181,18 +179,8 @@ DURATION:PT` + formatDuration(t.Duration)
DTEND:` + makeCalDavTimeFromTimeStamp(t.End) DTEND:` + makeCalDavTimeFromTimeStamp(t.End)
} }
if t.Description != "" { 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 += ` caldavtodos += `
DESCRIPTION:` + escapeICalText(description) DESCRIPTION:` + escapeICalText(t.Description)
}
} }
if t.Completed.Unix() > 0 { if t.Completed.Unix() > 0 {
caldavtodos += ` caldavtodos += `

View File

@ -48,7 +48,8 @@ func TestParseTodos(t *testing.T) {
todos: []*Todo{ todos: []*Todo{
{ {
Summary: "Todo #1", Summary: "Todo #1",
Description: `<p>Lorem Ipsum</p><p>Dolor sit amet</p>`, Description: `Lorem Ipsum
Dolor sit amet`,
UID: "randommduid", UID: "randommduid",
Timestamp: time.Unix(1543626724, 0).In(config.GetTimeZone()), Timestamp: time.Unix(1543626724, 0).In(config.GetTimeZone()),
Color: "affffe", Color: "affffe",
@ -72,7 +73,7 @@ X-APPLE-CALENDAR-COLOR:#affffeFF
X-OUTLOOK-COLOR:#affffeFF X-OUTLOOK-COLOR:#affffeFF
X-FUNAMBOL-COLOR:#affffeFF X-FUNAMBOL-COLOR:#affffeFF
COLOR:#affffeFF COLOR:#affffeFF
DESCRIPTION:Lorem Ipsum\n\nDolor sit amet DESCRIPTION:Lorem Ipsum\nDolor sit amet
LAST-MODIFIED:00010101T000000Z LAST-MODIFIED:00010101T000000Z
END:VTODO END:VTODO
END:VCALENDAR`, 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) { func TestGetCaldavColor(t *testing.T) {
tests := []struct { tests := []struct {
name string name string

View File

@ -95,7 +95,6 @@ const (
AuthLdapBindPassword Key = `auth.ldap.bindpassword` AuthLdapBindPassword Key = `auth.ldap.bindpassword`
AuthLdapGroupSyncEnabled Key = `auth.ldap.groupsyncenabled` AuthLdapGroupSyncEnabled Key = `auth.ldap.groupsyncenabled`
AuthLdapGroupSyncFilter Key = `auth.ldap.groupsyncfilter` AuthLdapGroupSyncFilter Key = `auth.ldap.groupsyncfilter`
AuthLdapGroupSyncUseServiceAccount Key = `auth.ldap.groupsyncuseserviceaccount`
AuthLdapAvatarSyncAttribute Key = `auth.ldap.avatarsyncattribute` AuthLdapAvatarSyncAttribute Key = `auth.ldap.avatarsyncattribute`
AuthLdapAttributeUsername Key = `auth.ldap.attribute.username` AuthLdapAttributeUsername Key = `auth.ldap.attribute.username`
AuthLdapAttributeEmail Key = `auth.ldap.attribute.email` AuthLdapAttributeEmail Key = `auth.ldap.attribute.email`
@ -225,6 +224,7 @@ const (
AuditLogfile Key = `audit.logfile` AuditLogfile Key = `audit.logfile`
AuditRotationMaxSizeMB Key = `audit.rotation.maxsizemb` AuditRotationMaxSizeMB Key = `audit.rotation.maxsizemb`
AuditRotationMaxAge Key = `audit.rotation.maxage` AuditRotationMaxAge Key = `audit.rotation.maxage`
AuditForwarders Key = `audit.forwarders`
OutgoingRequestsAllowNonRoutableIPs Key = `outgoingrequests.allownonroutableips` OutgoingRequestsAllowNonRoutableIPs Key = `outgoingrequests.allownonroutableips`
OutgoingRequestsProxyURL Key = `outgoingrequests.proxyurl` OutgoingRequestsProxyURL Key = `outgoingrequests.proxyurl`
@ -390,7 +390,6 @@ func InitDefaultConfig() {
AuthLdapVerifyTLS.setDefault(true) AuthLdapVerifyTLS.setDefault(true)
AuthLdapGroupSyncEnabled.setDefault(false) AuthLdapGroupSyncEnabled.setDefault(false)
AuthLdapGroupSyncFilter.setDefault("(&(objectclass=*)(|(objectclass=group)(objectclass=groupOfNames)))") AuthLdapGroupSyncFilter.setDefault("(&(objectclass=*)(|(objectclass=group)(objectclass=groupOfNames)))")
AuthLdapGroupSyncUseServiceAccount.setDefault(false)
AuthLdapAttributeUsername.setDefault("uid") AuthLdapAttributeUsername.setDefault("uid")
AuthLdapAttributeEmail.setDefault("mail") AuthLdapAttributeEmail.setDefault("mail")
AuthLdapAttributeDisplayname.setDefault("displayName") AuthLdapAttributeDisplayname.setDefault("displayName")

View File

@ -127,7 +127,7 @@ func RestoreAndTruncate(table string, contents []map[string]interface{}) (err er
return err return err
} }
} else { } else {
if _, err := x.Query("TRUNCATE TABLE " + x.Quote(table)); err != nil { if _, err := x.Query("TRUNCATE TABLE ?", table); err != nil {
return err return err
} }
} }
@ -148,7 +148,7 @@ func TruncateAllTables() error {
return err return err
} }
} else { } else {
if _, err := x.Query("TRUNCATE TABLE " + x.Quote(name)); err != nil { if _, err := x.Query("TRUNCATE TABLE ?", name); err != nil {
return err return err
} }
} }

View File

@ -41,41 +41,3 @@
created_by_id: 3 created_by_id: 3
created: 2024-01-01 00:00:00 created: 2024-01-01 00:00:00
updated: 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

View File

@ -76,18 +76,6 @@ func ClearDispatchedEvents() {
dispatchedTestEvents = nil 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. // CountDispatchedEvents counts how many events of a specific type have been dispatched.
func CountDispatchedEvents(eventName string) int { func CountDispatchedEvents(eventName string) int {
count := 0 count := 0

View File

@ -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
},
})
}

View File

@ -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
},
})
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -74,13 +74,6 @@ func stripAPIVersion(path string) string {
return path 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) { func getRouteGroupName(path string) (finalName string, filteredParts []string) {
parts := strings.Split(stripAPIVersion(path), "/") parts := strings.Split(stripAPIVersion(path), "/")
filteredParts = []string{} filteredParts = []string{}
@ -89,7 +82,7 @@ func getRouteGroupName(path string) (finalName string, filteredParts []string) {
continue continue
} }
filteredParts = append(filteredParts, canonicalAPITokenGroup(part)) filteredParts = append(filteredParts, part)
} }
finalName = strings.Join(filteredParts, "_") finalName = strings.Join(filteredParts, "_")
@ -190,7 +183,7 @@ func isStandardCRUDRoute(routeGroupName string, routeParts []string, _ string) b
"comments": true, "comments": true,
"relations": true, "relations": true,
"attachments": true, "attachments": true,
"time_entries": true, "time-entries": true,
"projects_views": true, "projects_views": true,
"projects_teams": true, "projects_teams": true,
"projects_users": true, "projects_users": true,
@ -410,8 +403,7 @@ func CanDoAPIRoute(c *echo.Context, token *APIToken) (can bool) {
} }
method := c.Request().Method method := c.Request().Method
for rawGroup, perms := range token.APIPermissions { for group, perms := range token.APIPermissions {
group := canonicalAPITokenGroup(rawGroup)
tables := []APITokenRoute{apiTokenRoutes[group], apiTokenRoutesV2[group]} tables := []APITokenRoute{apiTokenRoutes[group], apiTokenRoutesV2[group]}
for _, routes := range tables { for _, routes := range tables {
if routes == nil { 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 // Two list endpoints share tasks.read_all but only one
// survives collection, so allow either explicitly. // survives collection, so allow either explicitly.
if group == "tasks" && p == "read_all" && method == http.MethodGet && if group == "tasks" && p == "read_all" && method == http.MethodGet &&
(path == "/api/v1/tasks" || path == "/api/v1/projects/:project/tasks" || (path == "/api/v1/tasks" || path == "/api/v1/projects/:project/tasks") {
path == "/api/v2/tasks" || path == "/api/v2/projects/:project/tasks") {
return true return true
} }
} }
@ -456,9 +447,8 @@ func PermissionsAreValid(permissions APIPermissions) (err error) {
// resources (no v1 counterpart) live solely in apiTokenRoutesV2, so // resources (no v1 counterpart) live solely in apiTokenRoutesV2, so
// validating against the union lets tokens grant them. CanDoAPIRoute // validating against the union lets tokens grant them. CanDoAPIRoute
// already consults both tables when authorising. // already consults both tables when authorising.
group := canonicalAPITokenGroup(key) v1Routes := apiTokenRoutes[key]
v1Routes := apiTokenRoutes[group] v2Routes := apiTokenRoutesV2[key]
v2Routes := apiTokenRoutesV2[group]
if v1Routes == nil && v2Routes == nil { if v1Routes == nil && v2Routes == nil {
return &ErrInvalidAPITokenPermission{ return &ErrInvalidAPITokenPermission{
Group: key, Group: key,

View File

@ -121,9 +121,9 @@ func TestCollectRoutesV2(t *testing.T) {
assert.Equal(t, "DELETE", labels["delete"].Method) assert.Equal(t, "DELETE", labels["delete"].Method)
} }
// TestCollectRoutes_TimeEntriesV2 pins the v2-only time-entries resource to a // TestCollectRoutes_TimeEntriesV2 verifies the v2-only time-entries resource
// snake_case "time_entries" group (not the "other" catch-all, not a hyphenated // lands under a clean "time-entries" group rather than the "other" catch-all,
// key the frontend's snake_case transform would mangle on save). // so its scopes read sensibly for token clients.
func TestCollectRoutes_TimeEntriesV2(t *testing.T) { func TestCollectRoutes_TimeEntriesV2(t *testing.T) {
apiTokenRoutes = make(map[string]APITokenRoute) apiTokenRoutes = make(map[string]APITokenRoute)
apiTokenRoutesV2 = make(map[string]APITokenRoute) apiTokenRoutesV2 = make(map[string]APITokenRoute)
@ -137,11 +137,8 @@ func TestCollectRoutes_TimeEntriesV2(t *testing.T) {
_, isOther := apiTokenRoutesV2["other"] _, isOther := apiTokenRoutesV2["other"]
assert.False(t, isOther, "time-entries CRUD must not fall into the 'other' bucket") assert.False(t, isOther, "time-entries CRUD must not fall into the 'other' bucket")
_, hyphenated := apiTokenRoutesV2["time-entries"] te, has := apiTokenRoutesV2["time-entries"]
assert.False(t, hyphenated, "group key must be canonicalised to snake_case") 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, "GET", te["read_all"].Method)
assert.Equal(t, "/api/v2/time-entries", te["read_all"].Path) assert.Equal(t, "/api/v2/time-entries", te["read_all"].Path)
assert.Equal(t, "GET", te["read_one"].Method) 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 // 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. // discover and grant them, without mutating the v1 table itself.
func TestGetAPITokenRoutes_ExposesV2Only(t *testing.T) { func TestGetAPITokenRoutes_ExposesV2Only(t *testing.T) {
apiTokenRoutes = make(map[string]APITokenRoute) apiTokenRoutes = make(map[string]APITokenRoute)
@ -165,35 +162,14 @@ func TestGetAPITokenRoutes_ExposesV2Only(t *testing.T) {
_, hasLabels := routes["labels"] _, hasLabels := routes["labels"]
assert.True(t, hasLabels, "v1 groups stay exposed") assert.True(t, hasLabels, "v1 groups stay exposed")
te, hasTE := routes["time_entries"] te, hasTE := routes["time-entries"]
require.True(t, hasTE, "v2-only time_entries must be exposed via /routes") require.True(t, hasTE, "v2-only time-entries must be exposed via /routes")
assert.Equal(t, "GET", te["read_all"].Method) 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") 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, // TestGetRouteDetail_V2Verbs verifies the v2 verb mapping: POST→create,
// PUT/PATCH→update. v1 inverts POST and PUT so we need a separate mapping // PUT/PATCH→update. v1 inverts POST and PUT so we need a separate mapping
// path. // 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 // 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 // integration test in pkg/webtests/huma_label_test.go (see the token-auth
// scenarios in that file) which exercises the full auth pipeline. // scenarios in that file) which exercises the full auth pipeline.

View File

@ -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 // 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.", 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.",
}
}

View File

@ -18,6 +18,7 @@ package models
import ( import (
"code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/user"
"code.vikunja.io/api/pkg/web"
) )
///////////////// /////////////////
@ -230,7 +231,7 @@ func (l *ProjectCreatedEvent) Name() string {
// ProjectUpdatedEvent represents an event where a project has been updated // ProjectUpdatedEvent represents an event where a project has been updated
type ProjectUpdatedEvent struct { type ProjectUpdatedEvent struct {
Project *Project `json:"project"` Project *Project `json:"project"`
Doer *user.User `json:"doer"` Doer web.Auth `json:"doer"`
} }
// Name defines the name for ProjectUpdatedEvent // Name defines the name for ProjectUpdatedEvent
@ -241,7 +242,7 @@ func (p *ProjectUpdatedEvent) Name() string {
// ProjectDeletedEvent represents an event where a project has been deleted // ProjectDeletedEvent represents an event where a project has been deleted
type ProjectDeletedEvent struct { type ProjectDeletedEvent struct {
Project *Project `json:"project"` Project *Project `json:"project"`
Doer *user.User `json:"doer"` Doer web.Auth `json:"doer"`
} }
// Name defines the name for ProjectDeletedEvent // Name defines the name for ProjectDeletedEvent
@ -257,7 +258,7 @@ func (p *ProjectDeletedEvent) Name() string {
type ProjectSharedWithUserEvent struct { type ProjectSharedWithUserEvent struct {
Project *Project `json:"project"` Project *Project `json:"project"`
User *user.User `json:"user"` User *user.User `json:"user"`
Doer *user.User `json:"doer"` Doer web.Auth `json:"doer"`
} }
// Name defines the name for ProjectSharedWithUserEvent // Name defines the name for ProjectSharedWithUserEvent
@ -269,7 +270,7 @@ func (p *ProjectSharedWithUserEvent) Name() string {
type ProjectSharedWithTeamEvent struct { type ProjectSharedWithTeamEvent struct {
Project *Project `json:"project"` Project *Project `json:"project"`
Team *Team `json:"team"` Team *Team `json:"team"`
Doer *user.User `json:"doer"` Doer web.Auth `json:"doer"`
} }
// Name defines the name for ProjectSharedWithTeamEvent // Name defines the name for ProjectSharedWithTeamEvent
@ -308,7 +309,7 @@ func (t *TeamMemberRemovedEvent) Name() string {
// TeamCreatedEvent represents a TeamCreatedEvent event // TeamCreatedEvent represents a TeamCreatedEvent event
type TeamCreatedEvent struct { type TeamCreatedEvent struct {
Team *Team `json:"team"` Team *Team `json:"team"`
Doer *user.User `json:"doer"` Doer web.Auth `json:"doer"`
} }
// Name defines the name for TeamCreatedEvent // Name defines the name for TeamCreatedEvent
@ -319,7 +320,7 @@ func (t *TeamCreatedEvent) Name() string {
// TeamDeletedEvent represents a TeamDeletedEvent event // TeamDeletedEvent represents a TeamDeletedEvent event
type TeamDeletedEvent struct { type TeamDeletedEvent struct {
Team *Team `json:"team"` Team *Team `json:"team"`
Doer *user.User `json:"doer"` Doer web.Auth `json:"doer"`
} }
// Name defines the name for TeamDeletedEvent // Name defines the name for TeamDeletedEvent

View File

@ -404,64 +404,6 @@ func exportProjectBackgrounds(s *xorm.Session, u *user.User, wr *zip.Writer) (er
return utils.WriteFilesToZip(backgroundFiles, wr) 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() { func RegisterOldExportCleanupCron() {
const logPrefix = "[User Export Cleanup Cron] " const logPrefix = "[User Export Cleanup Cron] "

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