Merge branch 'main' into landing-page

This commit is contained in:
surfingbytes 2026-03-23 11:52:17 +01:00 committed by GitHub
commit 6fb389ba90
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
77 changed files with 2565 additions and 1009 deletions

View File

@ -5,6 +5,7 @@ env:
on:
pull_request:
merge_group:
push:
tags:
- v*

View File

@ -7,6 +7,258 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
All releases can be found on https://code.vikunja.io/vikunja/releases.
## [2.2.0] - 2026-03-20
### Bug Fixes
* *(attachments)* Sync kanban store and task ref on attachment changes
* *(auth)* Use SameSite=None for refresh token cookie to fix desktop app
* *(auth)* Make SameSite=None conditional on HTTPS for refresh cookie
* *(caldav)* Eliminate nested db session in CalDAV auth
* *(caldav)* Parse timestamps in configured timezone
* *(caldav)* Use /dav/projects/ as home to make iOS/MacOS reminders work (#2417)
* *(ci)* Remove HTML comments inside table that break markdown rendering
* *(cli)* Make user deletion confirmation check Windows compatible (#2339)
* *(db)* Prevent SQLite "database is locked" errors under concurrent writes
* *(db)* Use immediate txlock for SQLite instead of MaxOpenConns(1)
* *(db)* Use WAL mode for SQLite and temp file for ephemeral databases
* *(desktop)* Disable nodeIntegration and enable contextIsolation/sandbox
* *(desktop)* Validate URL schemes before shell.openExternal
* *(desktop)* Block same-window navigation to external origins
* *(docker)* Remove COPY for deleted patches directory
* *(e2e)* Drain event handlers and stop browser between tests
* *(events)* Defer task event dispatch until after transaction commit
* *(events)* Defer event dispatch for task sub-entities
* *(events)* Defer event dispatch for project operations
* *(events)* Defer event dispatch for team operations
* *(events)* Defer event dispatch for user creation and task positions
* *(events)* Dispatch pending events in CalDAV handlers after commit
* *(events)* Dispatch pending events in migration and export handlers
* *(frontend)* Add horizontal overflow handling to tables on mobile
* *(frontend)* Use semantic class instead of targeting Tailwind utility
* *(frontend)* Use mbs-2 utility class instead of scoped CSS
* *(gantt)* Always show relation arrows and fix arrow Y positioning
* *(gantt)* Update relation arrows in real-time during drag and resize
* *(gantt)* Make relation arrows smaller and dash precedes lines
* *(gantt)* Spread overlapping relation arrows at shared endpoints
* *(gantt)* Improve parent task bar styling and visual grouping
* *(gantt)* Make collapse/expand triangle smaller
* *(gantt)* Move parent diamonds outward with stroke and remove hover effect
* *(gantt)* Only set hasDerivedDates when children have actual dates
* *(gantt)* Clamp collapse chevron x position to prevent negative offset
* *(gantt)* Remove unreachable hover rule on relation arrows
* *(gantt)* Render collapse chevron after bars for correct SVG paint order
* *(menu)* Prevent dropdown from closing when cursor crosses offset gap (#2367)
* *(menu)* Show all project menu items in sidebar dropdown
* *(migration)* Support space-separated date format in TickTick importer
* *(nav)* Project drag handle position
* *(shortcuts)* Resolve lint errors in shortcut module
* *(shortcuts)* Track active sequences explicitly to prevent misfires
* *(tasks)* Support both expand and expand[] query parameter formats (#2415)
* *(test)* Update mobile kanban test to use close button instead of back button
* *(views)* Assign default position when creating new project views
* Use MinPositionSpacing threshold in calculateNewPositionForTask (#2320) ([3ca4913](3ca4913fcb6dc287adec552dd62024a3b63f477a))
* Remove invalidateAvatarCache call that broke request deduplication (#2317) ([7297682](7297682cadae3e2c48f2a09d20a6191b561c1eeb))
* Add /tmp directory to Docker image to fix data export ([84d563c](84d563c51b6cd15000f4af6e058362c5e45c8dc2))
* Update old kolaente.dev URLs to code.vikunja.io (#2342) ([a160048](a160048cc3259773405654746117bf6dc0565eee))
* Validate default settings timezone on startup (#2345) ([40bcf2b](40bcf2b36f777c6338a40581a472333974770c93))
* Correct package.json indentation after dependency removal ([f8763d8](f8763d812e2a7c7f9b2d28ff3e502693419f859e))
* Remove duplicate close button on mobile task detail view ([8a4f3a9](8a4f3a916f2eae71f0106c42d257b5ee4dc77928))
* Prevent nil pointer panic in mention notification listeners ([18f1687](18f16878a84952cf5d0ddb583385dc340d1f5ff3))
* Only drop Vikunja-owned tables in WipeEverything ([14e2c95](14e2c95a830eb4206390a58f85b4bc49068f23cd))
* Only dump Vikunja-owned tables ([cd7d405](cd7d40583aaa43e1d9445e9f54ea81d14eb12232))
* Remove debug log statements from task duplicate ([6da0f68](6da0f685624c66806027070d537648be9b100e29))
* Close source file handle when duplicating attachments ([7aad96b](7aad96b1991a981245cc119bce189de327ea36ce))
* Preserve cover image when duplicating task ([9c23e19](9c23e196440830d0b94ca18bfb1002a0db27b54c))
* Allow browser caching for file downloads (#2349) ([54d9775](54d977532e9e9a99281bc56965583d07f3913b21))
* Handle deleted user in saved filter view event listener ([7288483](72884838790db52852c8643ab17be5f6fc0067f0))
* Include remote IP address in HTTP request logs ([f9cb0a2](f9cb0a2de1d7ed64aa04f74f4209f117ea60186f))
* Use ParadeDB v2 fuzzy prefix matching for search (#2346) ([0a38ec0](0a38ec08388c9d2716f9e41185af0bcfb0ed7f8d))
* Prefer working directory for service.rootpath default ([d3cbc4f](d3cbc4fc4fb7d7fe054c4c022656f2b4d5c42bde))
* Ensure /tmp is writable by container user in Docker image ([f497e8b](f497e8bb6d78f3b01c2a87540e28d7727e17676e))
* Remove debounce from color picker to prevent stale color on save ([d196af0](d196af0503053d00e05afb8d2585a67b229a5144))
* Send account deletion notification before deleting user row ([79a612a](79a612aa5d95f89cd84148295146a92ccddefa74))
* Register bulk label route correctly for API token permissions ([e19bea8](e19bea8e3a2804485479748b1c91dc58719dbe11))
* Prevent authenticated UI flash when server rejects JWT session (#2387) ([28cc9e0](28cc9e0571c98bb04d216e5fe47aaa503a1e887b))
* Preserve CalDAV inverse relations when parent has no RELATED-TO (#2389) ([ada2eba](ada2ebab9e1738bb145db1c498d2dda84d11c10b))
* Collapse view buttons into dropdown when overflowing (#2306) ([7b6b432](7b6b4323015239098a55adcb134d12dc9785f5cb))
* Invalidate all sessions when enabling TOTP ([3bc0093](3bc009368628fb286632b456f9bf2d575a8bfa43))
* Make mage fmt skip gitignored files ([e74265d](e74265d921b9b12bf89882e791743758b42f5f3d))
* Ensure frontend dist directory exists for lint and fmt commands ([c62b7e6](c62b7e680f82253d89f8cefbfe4bb4b4bb64c5e9))
* Handle S3 backend in user export download ([b0ede53](b0ede53c051d45a3e861450187e64c5342be5362))
* Use file mime type instead of hardcoded application/zip in S3 export ([4cd63f9](4cd63f93a48d784dd2566c26a0642ec0c69d3d8f))
* Configure Echo IPExtractor to prevent rate limit bypass via spoofed headers ([a498dd6](a498dd69915a006c07e9d82660a2185d7e8136ee))
* Block login for StatusAccountLocked users ([4c80932](4c80932b6475ad54a2e2a81541d89a3b8471a762))
* Prevent password reset from re-enabling admin-disabled accounts ([d8570c6](d8570c603da1f26635ce6048d6af85ede827abfb))
* Reject password reset token requests for disabled users ([708ccab](708ccab895a23ed59b330db4a58a441bf5fbfcb2))
* Prevent email confirmation from re-enabling admin-disabled accounts ([049f4a6](049f4a6be46f9460bd516f489ef9f569574bc70d))
* Update test expectations for new disabled user fixture ([89923eb](89923ebe7090038c57ee3ad23eca86858c9c2eca))
* Reject images exceeding 50M pixels before decode ([af61d0f](af61d0f1a0d6e9394546d2d64dff043cfbe641f7))
* Adapt image preview DoS protection to new FileStorage interface ([be0aaa7](be0aaa70601af919f68fa1153f76bcf6335bc0b5))
* Verify comment belongs to task in URL to prevent IDOR ([bc6d843](bc6d843ed4df82a6c89f10aa676a7a33d27bf2fd))
* Require CanUpdate for project background deletion ([f066eb3](f066eb3ea4d1648ef925a745836e48a71b600a5f))
* Only enforce task_id check when TaskID is provided ([4941961](49419619bd0052bdd7e727404a9284acd928a903))
* Use require.Error instead of assert.Error for error assertions ([b7a1408](b7a14080983d2781e1428be9b77fae319e7788e4))
* Reject CalDAV basic auth when TOTP is enabled ([cdf5d30](cdf5d30a425d032f749b78b98b828f25ad882615))
* Use user10 instead of user1 for TOTP fixture to avoid breaking login tests ([659e73a](659e73af05af154dda315d025e8b3a12705e4a7e))
* Update TOTP fixtures and tests to avoid conflicts with existing enrollment tests ([1ed813c](1ed813caf00224d90c3c89c5b8078788f5730f51))
### Dependencies
* *(deps)* Update dev-dependencies
* *(deps)* Upgrade serialize-javascript to 7.0.3
* *(deps)* Update dependency @vue/tsconfig to v0.9.0
* *(deps)* Use forked afero-s3 to fix S3 read performance regression (#2313)
* *(deps)* Update dependency flexsearch to v0.8.212
* *(deps)* Remove obsolete flexsearch 0.7.43 patch
* *(deps)* Remove @github/hotkey dependency
* *(deps)* Update dependency rollup-plugin-visualizer to v6.0.11
* *(deps)* Update dependency electron to v40.7.0
* *(deps)* Update immutable to 5.1.5
* *(deps)* Update svgo to 3.3.3
* *(deps)* Update tar to 7.5.10 and @tootallnate/once to 3.0.1 in desktop
* *(deps)* Update dependency vite-svg-loader to v5.1.1
* *(deps)* Bump dompurify from 3.3.1 to 3.3.2 in /frontend
* *(deps)* Update dependency eslint to v9.39.4
* *(deps)* Update dev-dependencies to v8.57.0
* *(deps)* Update dependency sass-embedded to v1.98.0
* *(deps)* Update dev-dependencies (#2395)
* *(deps)* Update dependency caniuse-lite to v1.0.30001779
* *(deps)* Override flatted to 3.4.1 to fix unbounded recursion DoS
* *(deps)* Update tar override to 7.5.11 to fix symlink path traversal
* *(deps)* Update dependency vue-tsc to v3.2.6
* *(deps)* Update dependency electron to v40.8.3
* *(deps)* Update dev-dependencies to v4.2.2
* *(deps)* Add daenney/ssrf for webhook SSRF protection
* *(deps)* Update dependency stylelint to v17.5.0
### Documentation
* Update user search endpoint description for external team bypass ([b5086fe](b5086febc71a80467302584b9d41e10459d9d77e))
* Update rootpath description to mention working directory default ([ddfc565](ddfc565c614761d3dda037902c8309bf5a27fdd1))
* Document database.schema config option for PostgreSQL ([8868b21](8868b214ca2f0b34a6506066af1c4c96e13ca40d))
* Document IP extraction and trusted proxy config options ([015a172](015a172c2a07d3fc3827645d9e1bfe986ee58a03))
### Features
* *(ci)* Post preview deployment comment on PRs
* *(ci)* Enable merge queue trigger
* *(config)* Add webhooks.allownonroutableips setting
* *(events)* Add DispatchOnCommit/DispatchPending for deferred event dispatch
* *(frontend)* Upgrade Tailwind CSS from v3 to v4
* *(frontend)* Highlight overdue tasks consistently (#958)
* *(gantt)* Add expand=subtasks to Gantt API params
* *(gantt)* Add task tree builder utility for hierarchy
* *(gantt)* Add dependency arrow data builder
* *(gantt)* Integrate task tree into Gantt rendering with collapse
* *(gantt)* Add collapse/expand chevron and indent indicators
* *(gantt)* Render parent summary bars with diamond endpoints
* *(gantt)* Create arrow SVG overlay component for relations
* *(gantt)* Wire relation arrows into GanttChart with toggle
* *(handlers)* Dispatch pending events after transaction commit
* *(release)* Update frontend package.json version on release
* *(shortcuts)* Add event.code-based shortcut module
* *(webhooks)* Add built-in SSRF protection using daenney/ssrf
* Ensure forms submit on Enter (#959) ([e1d1e7c](e1d1e7c848bb2f0062a5fa522c7a357a2d3c723f))
* Use offical vite plugin for sentry (#873) ([0a9586e](0a9586e8d4351e47edacb63fa6667193d99ff7ee))
* Mini tiptap improvements ([b92735b](b92735b0e907bf7613b106ea633b82efa7f1781a))
* Surface API validation errors to registration form fields (#1902) ([c6f0d8b](c6f0d8babe6f36e6d25d22a932c9f0a075a5a359))
* Add table registration to db package ([d26936f](d26936f869c8489b06b0d9377af489236765a9e1))
* Register Vikunja tables with db package at init ([3dd2ba4](3dd2ba4aa4309b589e809621de2ecee89ee54159))
* Add RegisteredTableNames helper to db package ([0a8534d](0a8534ded9fca162fb1721a86d835677b30f2cdb))
* Add task duplicate backend model and tests ([d8f3a96](d8f3a96b06fc40d4b30954cc71a3bb43890f8cfc))
* Register task duplicate API route ([77fdf1b](77fdf1b84b27f80f4f332a26e9d7cf1ad032f211))
* Add task duplicate frontend model and service ([52bee37](52bee379d417d37b21b3d6f0cac8e67f83716925))
* Add duplicateTask action to task store ([2014d50](2014d50b953f86fb5a66bf32c74035b8d42c2e7a))
* Add duplicate button to task detail view ([6c9407c](6c9407c58f4ed01c0eac37aa51e7939cd5a11a1d))
* Bypass discoverability settings for external team members ([28b913f](28b913f29f812ef51f3b8fe967d5560c1d8ed927))
* Add InitEventsForTesting and Unfake for real event dispatch in tests ([1b1e8e5](1b1e8e5b19e9dd32a0d6089759d18c81883f8ffc))
* Add mage test:e2e-api target for e2e API tests ([24b800d](24b800d48d27a90447bfb9765f23093e5b9bde41))
* Add conversational email template and rendering ([d4b0302](d4b03026f0b98734a95e9cc22d3e77e89a7d3f4f))
* Convert notifications to conversational email style ([b3572c5](b3572c5932ba9eb7159e48129c1e52f0333cf96e))
* Add translation keys for conversational emails ([def73e2](def73e2f8eeadf807c9b2e2a422e2335444280dd))
* Add user_id to webhooks and user-directed event infrastructure ([d4577c6](d4577c660f5550a59f1b90a2ef1f5fba49cb73c6))
* Extend WebhookListener for user-level webhooks ([dbbc80a](dbbc80aea613779d43b015479fef0f7301d8e7e2))
* Add API routes for user-level webhooks ([47a0775](47a0775c7378faf6c8b3af3cd1429d3be7c51e70))
* Add user-level webhooks settings page ([2e1648e](2e1648ef4c7b1d1a05542567cd2a682f1038b03c))
* Replace afero-s3 with minimal S3 afero.Fs implementation ([b065c62](b065c6200782bfd6e9eea889847e83f1dead906d))
* Add service.ipextractionmethod and service.trustedproxies config options ([26324a7](26324a740a73d19748eea3c745c74f91f60cc86b))
* Add StatusAccountLocked user status for TOTP lockouts ([f42a045](f42a045bdc175fbffee4f8ee9592fa8dfedbc8aa))
### Miscellaneous Tasks
* *(dev)* Update devenv
* *(i18n)* Update translations via Crowdin
* Remove feature request issue template ([06ead58](06ead58ea3bb366970473d587db82bb36db07887))
### Other
* *(other)* [skip ci] Updated swagger docs
* *(other)* Add e2e API tests to CI pipeline
* *(other)* Upgrade ParadeDB image to support v2 fuzzy search API
### Refactor
* *(attachments)* Read from task prop instead of global store
* *(attachments)* Return uploaded attachments instead of writing to store
* *(attachments)* Use local state instead of global attachment store
* *(attachments)* Remove global attachment store
* *(shortcuts)* Update directive to use new shortcut module
* *(shortcuts)* Update v-shortcut values to event.code format
* *(shortcuts)* Replace eventToHotkeyString with eventToShortcutString
* *(shortcuts)* Use event.code for raw keyboard handlers
* Batch label inserts during task duplication ([e07eeed](e07eeed21156ab2bdc6c02aceede9cbc91468a28))
* Use TaskRelation.Create for copy relation ([692357a](692357a648367f1beb9ba192e3ed3425f8648893))
* Move ListUsers tests from pkg/user to pkg/models ([54c7c4a](54c7c4aef2fbdf7d4c04630d75cd36a0d121daec))
* Enable golangci-lint on magefile, fix errors ([cea8c78](cea8c7807d060e0a187c37c80ba42d02d4aa7637))
* Fix contextcheck lint errors on magefile by passing mage context ([0a1104b](0a1104b75ce1a6fcadb0cd0678400cf3585a0eb1))
* Merge last unique build tag "tools" into go.mod tools section ([1b5f3f4](1b5f3f4ccd15a954d1b3ac4fa49a99c2f299deff))
* Add centralized ResolvePath for rootpath-relative paths ([2a7165a](2a7165aaba736c53be32bb8cf0cf77e6fb7cd501))
* Use config.ResolvePath for all rootpath-relative paths ([a043940](a043940e14f686faa15339ecc06f91dd191d22d1))
* Replace afero with FileStorage interface ([0e1f44e](0e1f44e57efe06d08a47d980fa49bdd260f5fac3))
* Use StatusAccountLocked for TOTP lockouts ([7792bf6](7792bf6cea36ede6c38b9966f587222b476176cb))
* Rename checkProjectBackgroundWriteRights to checkProjectBackgroundWritePermissions ([4b91e5e](4b91e5efa173c90346567d4b296ab6233a9cc093))
### Styling
* Fix alignment in config key declarations ([ddd9ef5](ddd9ef5f2206dc5936cc14d359c70312806de233))
### Testing
* *(shortcuts)* Add unit tests for shortcut parsing logic
* *(webhooks)* Add SSRF protection tests
* *(webhooks)* Allow non-routable IPs in E2E tests
* Update event assertions to work with deferred dispatch ([f516bbe](f516bbe560a7b2a0d348e71ecdab00229c5cf554))
* Add web integration tests for task duplication ([4d494ba](4d494ba442b7bc6b4d7d06a3a3919f8d1bc6e066))
* Add user 11 to external team 14 for discoverability tests ([64e455a](64e455a613134b74c5734570eef19f3631253738))
* Add tests for external team user discoverability bypass ([3a73016](3a730165bc15f0fa2593aa8961e27192e93fcafb))
* Verify email masking for external team name search ([0661789](06617891fafa7c73c1c7110d404cb0a76812842d))
* Add e2e API test package with webhook pipeline verification ([1f3509b](1f3509bf27a9102ac96578d441d3731fb444dfa9))
* Add fixture task with compound word for prefix search testing ([275f714](275f714224cc93f0f9cd7b4590ba2b07a79398e4))
* Add web tests for prefix/substring search (#2346) ([892b38b](892b38b3b696e024e673dba3c0e302d5afa714fe))
* Rewrite MultiFieldSearch tests with SQL output verification ([ee2723d](ee2723d9cf3c603bd22be9e5411d67f1c9f38799))
* Call real MultiFieldSearch function and branch on db engine ([e6cbd67](e6cbd67ab52e92afadeaf0e9b3dbd96de3b3e1c1))
* Add task #48 to expected results in feature tests ([3568aaa](3568aaacee6d102ec8b749409cb1c8ca73c096f8))
* Adjust ParadeDB search tests for fuzzy prefix match broadening ([6268c48](6268c48f15955d812c6a569edb9c2d56e454fc27))
* Fix lint and adjust project search test for ParadeDB fuzzy matching ([b69705e](b69705e64bc45b93a834f877936aea5a7886bd9a))
* Add result count assertions for ParadeDB search tests ([c7c63e8](c7c63e8eadb174d163516590ec5c7ed945670cd5))
* Fix non-ParadeDB project search count assertion ([df0e3a8](df0e3a84a9cdf94b8a3f581ab7bf1690d36a6fe9))
* Fix ParadeDB project search count to 27 ([d36ac9d](d36ac9ddda5ddbc781a06017ee6d45ff2f8a45d8))
* Add tests for conversational email system ([aacf650](aacf650ec2c2817447107043620989d1b4c72130))
* Add e2e tests for user-level webhooks ([05cc65f](05cc65fe9e4fa448cda437d58480a9f3f19d69ed))
* Add web tests for bulk label task endpoint ([675dfb3](675dfb3ea47dd882de7e49ab1b0ace79a5e8bb9b))
* Add failing test for bulk label API token route registration ([554593c](554593cdb6bc0d31a1809c4b969b4fda9423edc3))
* Add FileStat assertion to validate storage path in attachment test ([17eccd8](17eccd848fd8688cd18f5dd46d1beb2c6ce96442))
* Add tests for disabled user password reset prevention ([241b0e8](241b0e80b6d9e91cda1f03a9e3a6368710d1fe36))
* Add web test for disabled user password reset rejection ([2260d76](2260d763b56290fcf8bfe5a9acfdee1a4332a65e))
* Add failing test for image preview with oversized dimensions ([f7592e2](f7592e2cfdc11fb06441007a4fb1d2ca5a2f1c5a))
* Add failing test for task comment IDOR ([2da8925](2da89258e53068253dcf8ef17d4dad141dba7d31))
* Add failing test for project background delete with read-only access ([f60f3af](f60f3af70b6d8258dd342a9ac15b71f48326e9af))
* Add TOTP fixture data for user1 ([27ef92b](27ef92b9bf36f437b151df13f801a504e73bddc8))
* Add failing test for CalDAV 2FA bypass via basic auth ([bda16e7](bda16e770fa76f212d15b1faec5c83f9046a0bb3))
* Register totp fixture in test setup ([a66bda2](a66bda2f51d4f7df8d353066a100de2d8c0aab32))
* Verify CalDAV token auth bypasses TOTP check ([1f2aef7](1f2aef776ccdd0ac1405fc8bcbb47084091d42eb))
## [2.1.0] - 2026-02-27
### Bug Fixes

View File

@ -2,7 +2,7 @@
[![Build Status](https://github.com/go-vikunja/vikunja/actions/workflows/ci.yml/badge.svg)](https://github.com/go-vikunja/vikunja/actions/workflows/ci.yml)
[![License: AGPL-3.0-or-later](https://img.shields.io/badge/License-AGPL--3.0--or--later-blue.svg)](LICENSE)
[![Install](https://img.shields.io/badge/download-v2.1.0-brightgreen.svg)](https://vikunja.io/docs/installing)
[![Install](https://img.shields.io/badge/download-v2.2.0-brightgreen.svg)](https://vikunja.io/docs/installing)
[![Docker Pulls](https://img.shields.io/docker/pulls/vikunja/vikunja.svg)](https://hub.docker.com/r/vikunja/vikunja/)
[![Swagger Docs](https://img.shields.io/badge/swagger-docs-brightgreen.svg)](https://try.vikunja.io/api/v1/docs)
[![Go Report Card](https://goreportcard.com/badge/code.vikunja.io/api)](https://goreportcard.com/report/code.vikunja.io/api)

View File

@ -147,6 +147,16 @@
"key": "enableopenidteamusersearch",
"default_value": "false",
"comment": "If enabled, users will only find other users who are part of an existing team when they are searching for a user by their partial name. The other existing team may be created from openid. It is still possible to add users to teams with their exact email address even when this is enabled."
},
{
"key": "ipextractionmethod",
"default_value": "direct",
"comment": "Method for extracting client IP addresses. 'direct' (default) uses the TCP remote address and ignores forwarding headers — use this when Vikunja faces the internet directly. 'xff' extracts from the X-Forwarded-For header — use this behind proxies like nginx, Traefik, or cloud load balancers. 'realip' extracts from the X-Real-IP header. When using 'xff' or 'realip', configure 'service.trustedproxies' with your proxy CIDR ranges."
},
{
"key": "trustedproxies",
"default_value": "",
"comment": "Comma-separated list of CIDR ranges for trusted reverse proxies. Only used when service.ipextractionmethod is 'xff' or 'realip'. X-Forwarded-For / X-Real-IP headers are only trusted from these addresses. Example: '127.0.0.1/32,::1/128,10.0.0.0/8,172.16.0.0/12'"
}
]
},

View File

@ -12,14 +12,43 @@ function createWindow() {
width: 1680,
height: 960,
webPreferences: {
nodeIntegration: true,
nodeIntegration: false,
contextIsolation: true,
sandbox: true,
webviewTag: false,
navigateOnDragDrop: false,
}
})
// Open external links in the browser
// Open external links in the browser, but only allow protocols
// that the TipTap editor also allows (see frontend/src/components/input/editor/TipTap.vue).
// TipTap allows: http, https (built-in) + ftp, git, obsidian, notion, message
// We also allow mailto since it's a standard safe protocol for email links.
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url);
return { action: 'deny' };
try {
const parsedUrl = new URL(url);
const allowedProtocols = [
'http:', 'https:', 'mailto:',
'ftp:', 'git:', 'obsidian:', 'notion:', 'message:',
];
if (allowedProtocols.includes(parsedUrl.protocol)) {
shell.openExternal(url);
}
} catch {
// Invalid URL, ignore silently
}
return { action: 'deny' };
});
// Prevent same-window navigation to external origins.
// Only allow navigation to the local express server.
mainWindow.webContents.on('will-navigate', (event, navigationUrl) => {
const parsedUrl = new URL(navigationUrl);
// Allow navigations to the local express server
if (parsedUrl.hostname === '127.0.0.1' || parsedUrl.hostname === 'localhost') {
return;
}
event.preventDefault();
});
// Hide the toolbar

View File

@ -2,7 +2,7 @@
"name": "vikunja-frontend",
"description": "The todo app to organize your life.",
"private": true,
"version": "0.10.0",
"version": "2.2.0",
"license": "AGPL-3.0-or-later",
"repository": {
"type": "git",
@ -125,7 +125,7 @@
"@vueuse/shared": "14.2.1",
"autoprefixer": "10.4.27",
"browserslist": "4.28.1",
"caniuse-lite": "1.0.30001780",
"caniuse-lite": "1.0.30001781",
"csstype": "3.2.3",
"esbuild": "0.27.4",
"eslint": "9.39.4",
@ -136,10 +136,10 @@
"postcss": "8.5.8",
"postcss-easing-gradients": "3.0.1",
"postcss-preset-env": "11.2.0",
"rollup": "4.59.0",
"rollup": "4.60.0",
"rollup-plugin-visualizer": "6.0.11",
"sass-embedded": "1.98.0",
"stylelint": "17.4.0",
"stylelint": "17.5.0",
"stylelint-config-property-sort-order-smacss": "10.0.0",
"stylelint-config-recommended-vue": "1.6.1",
"stylelint-config-standard-scss": "17.0.0",

View File

@ -6,7 +6,7 @@ settings:
overrides:
minimatch: ^10.2.3
rollup: 4.59.0
rollup: 4.60.0
basic-ftp: 5.2.0
serialize-javascript: ^7.0.3
flatted: ^3.4.1
@ -32,7 +32,7 @@ importers:
version: 3.1.3(@fortawesome/fontawesome-svg-core@7.1.0)(vue@3.5.27(typescript@5.9.3))
'@intlify/unplugin-vue-i18n':
specifier: 11.0.3
version: 11.0.3(@vue/compiler-dom@3.5.27)(eslint@9.39.4(jiti@2.4.2))(rollup@4.59.0)(typescript@5.9.3)(vue-i18n@11.2.8(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3))
version: 11.0.3(@vue/compiler-dom@3.5.27)(eslint@9.39.4(jiti@2.4.2))(rollup@4.60.0)(typescript@5.9.3)(vue-i18n@11.2.8(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3))
'@kyvg/vue3-notification':
specifier: 3.4.2
version: 3.4.2(vue@3.5.27(typescript@5.9.3))
@ -233,8 +233,8 @@ importers:
specifier: 4.28.1
version: 4.28.1
caniuse-lite:
specifier: 1.0.30001780
version: 1.0.30001780
specifier: 1.0.30001781
version: 1.0.30001781
csstype:
specifier: 3.2.3
version: 3.2.3
@ -266,29 +266,29 @@ importers:
specifier: 11.2.0
version: 11.2.0(postcss@8.5.8)
rollup:
specifier: 4.59.0
version: 4.59.0
specifier: 4.60.0
version: 4.60.0
rollup-plugin-visualizer:
specifier: 6.0.11
version: 6.0.11(rollup@4.59.0)
version: 6.0.11(rollup@4.60.0)
sass-embedded:
specifier: 1.98.0
version: 1.98.0
stylelint:
specifier: 17.4.0
version: 17.4.0(typescript@5.9.3)
specifier: 17.5.0
version: 17.5.0(typescript@5.9.3)
stylelint-config-property-sort-order-smacss:
specifier: 10.0.0
version: 10.0.0(stylelint@17.4.0(typescript@5.9.3))
version: 10.0.0(stylelint@17.5.0(typescript@5.9.3))
stylelint-config-recommended-vue:
specifier: 1.6.1
version: 1.6.1(postcss-html@1.8.0)(stylelint@17.4.0(typescript@5.9.3))
version: 1.6.1(postcss-html@1.8.0)(stylelint@17.5.0(typescript@5.9.3))
stylelint-config-standard-scss:
specifier: 17.0.0
version: 17.0.0(postcss@8.5.8)(stylelint@17.4.0(typescript@5.9.3))
version: 17.0.0(postcss@8.5.8)(stylelint@17.5.0(typescript@5.9.3))
stylelint-use-logical:
specifier: 2.1.3
version: 2.1.3(stylelint@17.4.0(typescript@5.9.3))
version: 2.1.3(stylelint@17.5.0(typescript@5.9.3))
tailwindcss:
specifier: 4.2.2
version: 4.2.2
@ -964,12 +964,17 @@ packages:
peerDependencies:
'@csstools/css-tokenizer': ^4.0.0
'@csstools/css-syntax-patches-for-csstree@1.0.26':
resolution: {integrity: sha512-6boXK0KkzT5u5xOgF6TKB+CLq9SOpEGmkZw0g5n9/7yg85wab3UzSxB8TxhLJ31L4SGJ6BCFRw/iftTha1CJXA==}
'@csstools/css-syntax-patches-for-csstree@1.0.28':
resolution: {integrity: sha512-1NRf1CUBjnr3K7hu8BLxjQrKCxEe8FP/xmPTenAxCRZWVLbmGotkFvG9mfNpjA6k7Bw1bw4BilZq9cu19RA5pg==}
'@csstools/css-syntax-patches-for-csstree@1.1.1':
resolution: {integrity: sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==}
peerDependencies:
css-tree: ^3.2.1
peerDependenciesMeta:
css-tree:
optional: true
'@csstools/css-tokenizer@3.0.4':
resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
engines: {node: '>=18'}
@ -2013,7 +2018,7 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0
'@types/babel__core': ^7.1.9
rollup: 4.59.0
rollup: 4.60.0
peerDependenciesMeta:
'@types/babel__core':
optional: true
@ -2022,7 +2027,7 @@ packages:
resolution: {integrity: sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: 4.59.0
rollup: 4.60.0
peerDependenciesMeta:
rollup:
optional: true
@ -2030,13 +2035,13 @@ packages:
'@rollup/plugin-replace@2.4.2':
resolution: {integrity: sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==}
peerDependencies:
rollup: 4.59.0
rollup: 4.60.0
'@rollup/plugin-terser@0.4.4':
resolution: {integrity: sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: 4.59.0
rollup: 4.60.0
peerDependenciesMeta:
rollup:
optional: true
@ -2045,139 +2050,139 @@ packages:
resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==}
engines: {node: '>= 8.0.0'}
peerDependencies:
rollup: 4.59.0
rollup: 4.60.0
'@rollup/pluginutils@5.1.3':
resolution: {integrity: sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: 4.59.0
rollup: 4.60.0
peerDependenciesMeta:
rollup:
optional: true
'@rollup/rollup-android-arm-eabi@4.59.0':
resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==}
'@rollup/rollup-android-arm-eabi@4.60.0':
resolution: {integrity: sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==}
cpu: [arm]
os: [android]
'@rollup/rollup-android-arm64@4.59.0':
resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==}
'@rollup/rollup-android-arm64@4.60.0':
resolution: {integrity: sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==}
cpu: [arm64]
os: [android]
'@rollup/rollup-darwin-arm64@4.59.0':
resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==}
'@rollup/rollup-darwin-arm64@4.60.0':
resolution: {integrity: sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==}
cpu: [arm64]
os: [darwin]
'@rollup/rollup-darwin-x64@4.59.0':
resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==}
'@rollup/rollup-darwin-x64@4.60.0':
resolution: {integrity: sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==}
cpu: [x64]
os: [darwin]
'@rollup/rollup-freebsd-arm64@4.59.0':
resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==}
'@rollup/rollup-freebsd-arm64@4.60.0':
resolution: {integrity: sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==}
cpu: [arm64]
os: [freebsd]
'@rollup/rollup-freebsd-x64@4.59.0':
resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==}
'@rollup/rollup-freebsd-x64@4.60.0':
resolution: {integrity: sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==}
cpu: [x64]
os: [freebsd]
'@rollup/rollup-linux-arm-gnueabihf@4.59.0':
resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==}
'@rollup/rollup-linux-arm-gnueabihf@4.60.0':
resolution: {integrity: sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==}
cpu: [arm]
os: [linux]
'@rollup/rollup-linux-arm-musleabihf@4.59.0':
resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==}
'@rollup/rollup-linux-arm-musleabihf@4.60.0':
resolution: {integrity: sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==}
cpu: [arm]
os: [linux]
'@rollup/rollup-linux-arm64-gnu@4.59.0':
resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==}
'@rollup/rollup-linux-arm64-gnu@4.60.0':
resolution: {integrity: sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==}
cpu: [arm64]
os: [linux]
'@rollup/rollup-linux-arm64-musl@4.59.0':
resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==}
'@rollup/rollup-linux-arm64-musl@4.60.0':
resolution: {integrity: sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==}
cpu: [arm64]
os: [linux]
'@rollup/rollup-linux-loong64-gnu@4.59.0':
resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==}
'@rollup/rollup-linux-loong64-gnu@4.60.0':
resolution: {integrity: sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==}
cpu: [loong64]
os: [linux]
'@rollup/rollup-linux-loong64-musl@4.59.0':
resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==}
'@rollup/rollup-linux-loong64-musl@4.60.0':
resolution: {integrity: sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==}
cpu: [loong64]
os: [linux]
'@rollup/rollup-linux-ppc64-gnu@4.59.0':
resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==}
'@rollup/rollup-linux-ppc64-gnu@4.60.0':
resolution: {integrity: sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==}
cpu: [ppc64]
os: [linux]
'@rollup/rollup-linux-ppc64-musl@4.59.0':
resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==}
'@rollup/rollup-linux-ppc64-musl@4.60.0':
resolution: {integrity: sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==}
cpu: [ppc64]
os: [linux]
'@rollup/rollup-linux-riscv64-gnu@4.59.0':
resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==}
'@rollup/rollup-linux-riscv64-gnu@4.60.0':
resolution: {integrity: sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==}
cpu: [riscv64]
os: [linux]
'@rollup/rollup-linux-riscv64-musl@4.59.0':
resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==}
'@rollup/rollup-linux-riscv64-musl@4.60.0':
resolution: {integrity: sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==}
cpu: [riscv64]
os: [linux]
'@rollup/rollup-linux-s390x-gnu@4.59.0':
resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==}
'@rollup/rollup-linux-s390x-gnu@4.60.0':
resolution: {integrity: sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==}
cpu: [s390x]
os: [linux]
'@rollup/rollup-linux-x64-gnu@4.59.0':
resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==}
'@rollup/rollup-linux-x64-gnu@4.60.0':
resolution: {integrity: sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==}
cpu: [x64]
os: [linux]
'@rollup/rollup-linux-x64-musl@4.59.0':
resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==}
'@rollup/rollup-linux-x64-musl@4.60.0':
resolution: {integrity: sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==}
cpu: [x64]
os: [linux]
'@rollup/rollup-openbsd-x64@4.59.0':
resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==}
'@rollup/rollup-openbsd-x64@4.60.0':
resolution: {integrity: sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==}
cpu: [x64]
os: [openbsd]
'@rollup/rollup-openharmony-arm64@4.59.0':
resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==}
'@rollup/rollup-openharmony-arm64@4.60.0':
resolution: {integrity: sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==}
cpu: [arm64]
os: [openharmony]
'@rollup/rollup-win32-arm64-msvc@4.59.0':
resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==}
'@rollup/rollup-win32-arm64-msvc@4.60.0':
resolution: {integrity: sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==}
cpu: [arm64]
os: [win32]
'@rollup/rollup-win32-ia32-msvc@4.59.0':
resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==}
'@rollup/rollup-win32-ia32-msvc@4.60.0':
resolution: {integrity: sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==}
cpu: [ia32]
os: [win32]
'@rollup/rollup-win32-x64-gnu@4.59.0':
resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==}
'@rollup/rollup-win32-x64-gnu@4.60.0':
resolution: {integrity: sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==}
cpu: [x64]
os: [win32]
'@rollup/rollup-win32-x64-msvc@4.59.0':
resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==}
'@rollup/rollup-win32-x64-msvc@4.60.0':
resolution: {integrity: sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==}
cpu: [x64]
os: [win32]
@ -3056,6 +3061,10 @@ packages:
resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==}
engines: {node: '>=12'}
ansi-regex@6.2.2:
resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
engines: {node: '>=12'}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
@ -3284,8 +3293,8 @@ packages:
resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==}
engines: {node: '>=16'}
caniuse-lite@1.0.30001780:
resolution: {integrity: sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==}
caniuse-lite@1.0.30001781:
resolution: {integrity: sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==}
capture-website@4.2.0:
resolution: {integrity: sha512-EmkSn36CXTC8tUsS6aNmvvsdpfVTYYkuRp7U5bV9gcJwcDbqqA5c0Op/iskYPKtDdOkuVp61mjn/LLywX0h7cw==}
@ -3426,6 +3435,15 @@ packages:
typescript:
optional: true
cosmiconfig@9.0.1:
resolution: {integrity: sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==}
engines: {node: '>=14'}
peerDependencies:
typescript: '>=4.9.5'
peerDependenciesMeta:
typescript:
optional: true
crelt@1.0.6:
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
@ -3477,6 +3495,10 @@ packages:
resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
css-tree@3.2.1:
resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
css-what@6.1.0:
resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
engines: {node: '>= 6'}
@ -4066,6 +4088,10 @@ packages:
resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==}
engines: {node: '>=18'}
get-east-asian-width@1.5.0:
resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==}
engines: {node: '>=18'}
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
@ -4141,8 +4167,8 @@ packages:
resolution: {integrity: sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==}
engines: {node: '>=18'}
globby@16.1.0:
resolution: {integrity: sha512-+A4Hq7m7Ze592k9gZRy4gJ27DrXRNnC1vPjxTt1qQxEY8RxagBkBxivkCwg7FxSTG0iLLEMaUx13oOr0R2/qcQ==}
globby@16.1.1:
resolution: {integrity: sha512-dW7vl+yiAJSp6aCekaVnVJxurRv7DCOLyXqEG3RYMYUg7AuJ2jCqPkZTA8ooqC2vtnkaMcV5WfFBMuEnTu1OQg==}
engines: {node: '>=20'}
globjoin@0.1.4:
@ -4821,11 +4847,14 @@ packages:
mdn-data@2.26.0:
resolution: {integrity: sha512-ZqI0qjKWHMPcGUfLmlr80NPNVHIOjPMHtIOe1qXYFGS0YBZ1YKAzo9yk8W+gGrLCN0Xdv/RKxqdIsqPakEfmow==}
mdn-data@2.27.1:
resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
mdurl@2.0.0:
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
meow@14.0.0:
resolution: {integrity: sha512-JhC3R1f6dbspVtmF3vKjAWz1EVIvwFrGGPLSdU6rK79xBwHWTuHoLnRX/t1/zHS1Ch1Y2UtIrih7DAHuH9JFJA==}
meow@14.1.0:
resolution: {integrity: sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw==}
engines: {node: '>=20'}
meow@7.1.1:
@ -5589,15 +5618,15 @@ packages:
hasBin: true
peerDependencies:
rolldown: 1.x || ^1.0.0-beta
rollup: 4.59.0
rollup: 4.60.0
peerDependenciesMeta:
rolldown:
optional: true
rollup:
optional: true
rollup@4.59.0:
resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==}
rollup@4.60.0:
resolution: {integrity: sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
@ -5943,8 +5972,8 @@ packages:
resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
engines: {node: '>=18'}
string-width@8.1.1:
resolution: {integrity: sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw==}
string-width@8.2.0:
resolution: {integrity: sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==}
engines: {node: '>=20'}
string.prototype.matchall@4.0.11:
@ -5980,6 +6009,10 @@ packages:
resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
engines: {node: '>=12'}
strip-ansi@7.2.0:
resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==}
engines: {node: '>=12'}
strip-bom-string@1.0.0:
resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==}
engines: {node: '>=0.10.0'}
@ -6084,8 +6117,8 @@ packages:
peerDependencies:
stylelint: '>= 11 < 18'
stylelint@17.4.0:
resolution: {integrity: sha512-3kQ2/cHv3Zt8OBg+h2B8XCx9evEABQIrv4hh3uXahGz/ZEHrTR80zxBiK2NfXNaSoyBzxO1pjsz1Vhdzwn5XSw==}
stylelint@17.5.0:
resolution: {integrity: sha512-o/NS6zhsPZFmgUm5tXX4pVNg1XDOZSlucLdf2qow/lVn4JIyzZIQ5b3kad1ugqUj3GSIgr2u5lQw7X8rjqw33g==}
engines: {node: '>=20.19.0'}
hasBin: true
@ -7703,10 +7736,12 @@ snapshots:
dependencies:
'@csstools/css-tokenizer': 4.0.0
'@csstools/css-syntax-patches-for-csstree@1.0.26': {}
'@csstools/css-syntax-patches-for-csstree@1.0.28': {}
'@csstools/css-syntax-patches-for-csstree@1.1.1(css-tree@3.2.1)':
optionalDependencies:
css-tree: 3.2.1
'@csstools/css-tokenizer@3.0.4': {}
'@csstools/css-tokenizer@4.0.0': {}
@ -8399,13 +8434,13 @@ snapshots:
'@intlify/shared@11.2.8': {}
'@intlify/unplugin-vue-i18n@11.0.3(@vue/compiler-dom@3.5.27)(eslint@9.39.4(jiti@2.4.2))(rollup@4.59.0)(typescript@5.9.3)(vue-i18n@11.2.8(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3))':
'@intlify/unplugin-vue-i18n@11.0.3(@vue/compiler-dom@3.5.27)(eslint@9.39.4(jiti@2.4.2))(rollup@4.60.0)(typescript@5.9.3)(vue-i18n@11.2.8(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3))':
dependencies:
'@eslint-community/eslint-utils': 4.9.0(eslint@9.39.4(jiti@2.4.2))
'@intlify/bundle-utils': 11.0.3(vue-i18n@11.2.8(vue@3.5.27(typescript@5.9.3)))
'@intlify/shared': 11.2.2
'@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.2.2)(@vue/compiler-dom@3.5.27)(vue-i18n@11.2.8(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3))
'@rollup/pluginutils': 5.1.3(rollup@4.59.0)
'@rollup/pluginutils': 5.1.3(rollup@4.60.0)
'@typescript-eslint/scope-manager': 8.49.0
'@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3)
debug: 4.4.3
@ -8629,128 +8664,128 @@ snapshots:
'@rolldown/pluginutils@1.0.0-rc.2': {}
'@rollup/plugin-babel@5.3.1(@babel/core@7.26.0)(rollup@4.59.0)':
'@rollup/plugin-babel@5.3.1(@babel/core@7.26.0)(rollup@4.60.0)':
dependencies:
'@babel/core': 7.26.0
'@babel/helper-module-imports': 7.25.9
'@rollup/pluginutils': 3.1.0(rollup@4.59.0)
rollup: 4.59.0
'@rollup/pluginutils': 3.1.0(rollup@4.60.0)
rollup: 4.60.0
transitivePeerDependencies:
- supports-color
'@rollup/plugin-node-resolve@15.2.3(rollup@4.59.0)':
'@rollup/plugin-node-resolve@15.2.3(rollup@4.60.0)':
dependencies:
'@rollup/pluginutils': 5.1.3(rollup@4.59.0)
'@rollup/pluginutils': 5.1.3(rollup@4.60.0)
'@types/resolve': 1.20.2
deepmerge: 4.3.1
is-builtin-module: 3.2.1
is-module: 1.0.0
resolve: 1.22.8
optionalDependencies:
rollup: 4.59.0
rollup: 4.60.0
'@rollup/plugin-replace@2.4.2(rollup@4.59.0)':
'@rollup/plugin-replace@2.4.2(rollup@4.60.0)':
dependencies:
'@rollup/pluginutils': 3.1.0(rollup@4.59.0)
'@rollup/pluginutils': 3.1.0(rollup@4.60.0)
magic-string: 0.25.9
rollup: 4.59.0
rollup: 4.60.0
'@rollup/plugin-terser@0.4.4(rollup@4.59.0)':
'@rollup/plugin-terser@0.4.4(rollup@4.60.0)':
dependencies:
serialize-javascript: 7.0.3
smob: 1.5.0
terser: 5.31.6
optionalDependencies:
rollup: 4.59.0
rollup: 4.60.0
'@rollup/pluginutils@3.1.0(rollup@4.59.0)':
'@rollup/pluginutils@3.1.0(rollup@4.60.0)':
dependencies:
'@types/estree': 0.0.39
estree-walker: 1.0.1
picomatch: 2.3.1
rollup: 4.59.0
rollup: 4.60.0
'@rollup/pluginutils@5.1.3(rollup@4.59.0)':
'@rollup/pluginutils@5.1.3(rollup@4.60.0)':
dependencies:
'@types/estree': 1.0.8
estree-walker: 2.0.2
picomatch: 4.0.3
optionalDependencies:
rollup: 4.59.0
rollup: 4.60.0
'@rollup/rollup-android-arm-eabi@4.59.0':
'@rollup/rollup-android-arm-eabi@4.60.0':
optional: true
'@rollup/rollup-android-arm64@4.59.0':
'@rollup/rollup-android-arm64@4.60.0':
optional: true
'@rollup/rollup-darwin-arm64@4.59.0':
'@rollup/rollup-darwin-arm64@4.60.0':
optional: true
'@rollup/rollup-darwin-x64@4.59.0':
'@rollup/rollup-darwin-x64@4.60.0':
optional: true
'@rollup/rollup-freebsd-arm64@4.59.0':
'@rollup/rollup-freebsd-arm64@4.60.0':
optional: true
'@rollup/rollup-freebsd-x64@4.59.0':
'@rollup/rollup-freebsd-x64@4.60.0':
optional: true
'@rollup/rollup-linux-arm-gnueabihf@4.59.0':
'@rollup/rollup-linux-arm-gnueabihf@4.60.0':
optional: true
'@rollup/rollup-linux-arm-musleabihf@4.59.0':
'@rollup/rollup-linux-arm-musleabihf@4.60.0':
optional: true
'@rollup/rollup-linux-arm64-gnu@4.59.0':
'@rollup/rollup-linux-arm64-gnu@4.60.0':
optional: true
'@rollup/rollup-linux-arm64-musl@4.59.0':
'@rollup/rollup-linux-arm64-musl@4.60.0':
optional: true
'@rollup/rollup-linux-loong64-gnu@4.59.0':
'@rollup/rollup-linux-loong64-gnu@4.60.0':
optional: true
'@rollup/rollup-linux-loong64-musl@4.59.0':
'@rollup/rollup-linux-loong64-musl@4.60.0':
optional: true
'@rollup/rollup-linux-ppc64-gnu@4.59.0':
'@rollup/rollup-linux-ppc64-gnu@4.60.0':
optional: true
'@rollup/rollup-linux-ppc64-musl@4.59.0':
'@rollup/rollup-linux-ppc64-musl@4.60.0':
optional: true
'@rollup/rollup-linux-riscv64-gnu@4.59.0':
'@rollup/rollup-linux-riscv64-gnu@4.60.0':
optional: true
'@rollup/rollup-linux-riscv64-musl@4.59.0':
'@rollup/rollup-linux-riscv64-musl@4.60.0':
optional: true
'@rollup/rollup-linux-s390x-gnu@4.59.0':
'@rollup/rollup-linux-s390x-gnu@4.60.0':
optional: true
'@rollup/rollup-linux-x64-gnu@4.59.0':
'@rollup/rollup-linux-x64-gnu@4.60.0':
optional: true
'@rollup/rollup-linux-x64-musl@4.59.0':
'@rollup/rollup-linux-x64-musl@4.60.0':
optional: true
'@rollup/rollup-openbsd-x64@4.59.0':
'@rollup/rollup-openbsd-x64@4.60.0':
optional: true
'@rollup/rollup-openharmony-arm64@4.59.0':
'@rollup/rollup-openharmony-arm64@4.60.0':
optional: true
'@rollup/rollup-win32-arm64-msvc@4.59.0':
'@rollup/rollup-win32-arm64-msvc@4.60.0':
optional: true
'@rollup/rollup-win32-ia32-msvc@4.59.0':
'@rollup/rollup-win32-ia32-msvc@4.60.0':
optional: true
'@rollup/rollup-win32-x64-gnu@4.59.0':
'@rollup/rollup-win32-x64-gnu@4.60.0':
optional: true
'@rollup/rollup-win32-x64-msvc@4.59.0':
'@rollup/rollup-win32-x64-msvc@4.60.0':
optional: true
'@sentry-internal/browser-utils@10.36.0':
@ -9766,6 +9801,8 @@ snapshots:
ansi-regex@6.0.1: {}
ansi-regex@6.2.2: {}
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
@ -9823,7 +9860,7 @@ snapshots:
autoprefixer@10.4.27(postcss@8.5.8):
dependencies:
browserslist: 4.28.1
caniuse-lite: 1.0.30001780
caniuse-lite: 1.0.30001781
fraction.js: 5.3.4
picocolors: 1.1.1
postcss: 8.5.8
@ -9942,7 +9979,7 @@ snapshots:
browserslist@4.28.1:
dependencies:
baseline-browser-mapping: 2.9.4
caniuse-lite: 1.0.30001780
caniuse-lite: 1.0.30001781
electron-to-chromium: 1.5.266
node-releases: 2.0.27
update-browserslist-db: 1.2.2(browserslist@4.28.1)
@ -10004,7 +10041,7 @@ snapshots:
camelcase@8.0.0: {}
caniuse-lite@1.0.30001780: {}
caniuse-lite@1.0.30001781: {}
capture-website@4.2.0(typescript@5.9.3):
dependencies:
@ -10146,6 +10183,15 @@ snapshots:
optionalDependencies:
typescript: 5.9.3
cosmiconfig@9.0.1(typescript@5.9.3):
dependencies:
env-paths: 2.2.1
import-fresh: 3.3.0
js-yaml: 4.1.1
parse-json: 5.2.0
optionalDependencies:
typescript: 5.9.3
crelt@1.0.6: {}
cross-spawn@7.0.6:
@ -10199,6 +10245,11 @@ snapshots:
mdn-data: 2.12.2
source-map-js: 1.2.1
css-tree@3.2.1:
dependencies:
mdn-data: 2.27.1
source-map-js: 1.2.1
css-what@6.1.0: {}
cssdb@8.8.0: {}
@ -10212,7 +10263,7 @@ snapshots:
cssstyle@5.3.7:
dependencies:
'@asamuzakjp/css-color': 4.1.1
'@csstools/css-syntax-patches-for-csstree': 1.0.26
'@csstools/css-syntax-patches-for-csstree': 1.0.28
css-tree: 3.1.0
lru-cache: 11.2.4
@ -10857,6 +10908,8 @@ snapshots:
get-east-asian-width@1.4.0: {}
get-east-asian-width@1.5.0: {}
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
@ -10960,7 +11013,7 @@ snapshots:
slash: 5.1.0
unicorn-magic: 0.3.0
globby@16.1.0:
globby@16.1.1:
dependencies:
'@sindresorhus/merge-streams': 4.0.0
fast-glob: 3.3.3
@ -11640,9 +11693,11 @@ snapshots:
mdn-data@2.26.0: {}
mdn-data@2.27.1: {}
mdurl@2.0.0: {}
meow@14.0.0: {}
meow@14.1.0: {}
meow@7.1.1:
dependencies:
@ -12517,44 +12572,44 @@ snapshots:
rfdc@1.4.1: {}
rollup-plugin-visualizer@6.0.11(rollup@4.59.0):
rollup-plugin-visualizer@6.0.11(rollup@4.60.0):
dependencies:
open: 8.4.2
picomatch: 4.0.3
source-map: 0.7.4
yargs: 17.7.2
optionalDependencies:
rollup: 4.59.0
rollup: 4.60.0
rollup@4.59.0:
rollup@4.60.0:
dependencies:
'@types/estree': 1.0.8
optionalDependencies:
'@rollup/rollup-android-arm-eabi': 4.59.0
'@rollup/rollup-android-arm64': 4.59.0
'@rollup/rollup-darwin-arm64': 4.59.0
'@rollup/rollup-darwin-x64': 4.59.0
'@rollup/rollup-freebsd-arm64': 4.59.0
'@rollup/rollup-freebsd-x64': 4.59.0
'@rollup/rollup-linux-arm-gnueabihf': 4.59.0
'@rollup/rollup-linux-arm-musleabihf': 4.59.0
'@rollup/rollup-linux-arm64-gnu': 4.59.0
'@rollup/rollup-linux-arm64-musl': 4.59.0
'@rollup/rollup-linux-loong64-gnu': 4.59.0
'@rollup/rollup-linux-loong64-musl': 4.59.0
'@rollup/rollup-linux-ppc64-gnu': 4.59.0
'@rollup/rollup-linux-ppc64-musl': 4.59.0
'@rollup/rollup-linux-riscv64-gnu': 4.59.0
'@rollup/rollup-linux-riscv64-musl': 4.59.0
'@rollup/rollup-linux-s390x-gnu': 4.59.0
'@rollup/rollup-linux-x64-gnu': 4.59.0
'@rollup/rollup-linux-x64-musl': 4.59.0
'@rollup/rollup-openbsd-x64': 4.59.0
'@rollup/rollup-openharmony-arm64': 4.59.0
'@rollup/rollup-win32-arm64-msvc': 4.59.0
'@rollup/rollup-win32-ia32-msvc': 4.59.0
'@rollup/rollup-win32-x64-gnu': 4.59.0
'@rollup/rollup-win32-x64-msvc': 4.59.0
'@rollup/rollup-android-arm-eabi': 4.60.0
'@rollup/rollup-android-arm64': 4.60.0
'@rollup/rollup-darwin-arm64': 4.60.0
'@rollup/rollup-darwin-x64': 4.60.0
'@rollup/rollup-freebsd-arm64': 4.60.0
'@rollup/rollup-freebsd-x64': 4.60.0
'@rollup/rollup-linux-arm-gnueabihf': 4.60.0
'@rollup/rollup-linux-arm-musleabihf': 4.60.0
'@rollup/rollup-linux-arm64-gnu': 4.60.0
'@rollup/rollup-linux-arm64-musl': 4.60.0
'@rollup/rollup-linux-loong64-gnu': 4.60.0
'@rollup/rollup-linux-loong64-musl': 4.60.0
'@rollup/rollup-linux-ppc64-gnu': 4.60.0
'@rollup/rollup-linux-ppc64-musl': 4.60.0
'@rollup/rollup-linux-riscv64-gnu': 4.60.0
'@rollup/rollup-linux-riscv64-musl': 4.60.0
'@rollup/rollup-linux-s390x-gnu': 4.60.0
'@rollup/rollup-linux-x64-gnu': 4.60.0
'@rollup/rollup-linux-x64-musl': 4.60.0
'@rollup/rollup-openbsd-x64': 4.60.0
'@rollup/rollup-openharmony-arm64': 4.60.0
'@rollup/rollup-win32-arm64-msvc': 4.60.0
'@rollup/rollup-win32-ia32-msvc': 4.60.0
'@rollup/rollup-win32-x64-gnu': 4.60.0
'@rollup/rollup-win32-x64-msvc': 4.60.0
fsevents: 2.3.3
rope-sequence@1.3.4: {}
@ -12887,10 +12942,10 @@ snapshots:
get-east-asian-width: 1.4.0
strip-ansi: 7.1.0
string-width@8.1.1:
string-width@8.2.0:
dependencies:
get-east-asian-width: 1.4.0
strip-ansi: 7.1.0
get-east-asian-width: 1.5.0
strip-ansi: 7.2.0
string.prototype.matchall@4.0.11:
dependencies:
@ -12949,6 +13004,10 @@ snapshots:
dependencies:
ansi-regex: 6.0.1
strip-ansi@7.2.0:
dependencies:
ansi-regex: 6.2.2
strip-bom-string@1.0.0: {}
strip-comments@2.0.1: {}
@ -12969,62 +13028,62 @@ snapshots:
style-mod@4.1.2: {}
stylelint-config-html@1.1.0(postcss-html@1.8.0)(stylelint@17.4.0(typescript@5.9.3)):
stylelint-config-html@1.1.0(postcss-html@1.8.0)(stylelint@17.5.0(typescript@5.9.3)):
dependencies:
postcss-html: 1.8.0
stylelint: 17.4.0(typescript@5.9.3)
stylelint: 17.5.0(typescript@5.9.3)
stylelint-config-property-sort-order-smacss@10.0.0(stylelint@17.4.0(typescript@5.9.3)):
stylelint-config-property-sort-order-smacss@10.0.0(stylelint@17.5.0(typescript@5.9.3)):
dependencies:
css-property-sort-order-smacss: 2.2.0
stylelint: 17.4.0(typescript@5.9.3)
stylelint-order: 6.0.4(stylelint@17.4.0(typescript@5.9.3))
stylelint: 17.5.0(typescript@5.9.3)
stylelint-order: 6.0.4(stylelint@17.5.0(typescript@5.9.3))
stylelint-config-recommended-scss@17.0.0(postcss@8.5.8)(stylelint@17.4.0(typescript@5.9.3)):
stylelint-config-recommended-scss@17.0.0(postcss@8.5.8)(stylelint@17.5.0(typescript@5.9.3)):
dependencies:
postcss-scss: 4.0.9(postcss@8.5.8)
stylelint: 17.4.0(typescript@5.9.3)
stylelint-config-recommended: 18.0.0(stylelint@17.4.0(typescript@5.9.3))
stylelint-scss: 7.0.0(stylelint@17.4.0(typescript@5.9.3))
stylelint: 17.5.0(typescript@5.9.3)
stylelint-config-recommended: 18.0.0(stylelint@17.5.0(typescript@5.9.3))
stylelint-scss: 7.0.0(stylelint@17.5.0(typescript@5.9.3))
optionalDependencies:
postcss: 8.5.8
stylelint-config-recommended-vue@1.6.1(postcss-html@1.8.0)(stylelint@17.4.0(typescript@5.9.3)):
stylelint-config-recommended-vue@1.6.1(postcss-html@1.8.0)(stylelint@17.5.0(typescript@5.9.3)):
dependencies:
postcss-html: 1.8.0
semver: 7.7.1
stylelint: 17.4.0(typescript@5.9.3)
stylelint-config-html: 1.1.0(postcss-html@1.8.0)(stylelint@17.4.0(typescript@5.9.3))
stylelint-config-recommended: 17.0.0(stylelint@17.4.0(typescript@5.9.3))
stylelint: 17.5.0(typescript@5.9.3)
stylelint-config-html: 1.1.0(postcss-html@1.8.0)(stylelint@17.5.0(typescript@5.9.3))
stylelint-config-recommended: 17.0.0(stylelint@17.5.0(typescript@5.9.3))
stylelint-config-recommended@17.0.0(stylelint@17.4.0(typescript@5.9.3)):
stylelint-config-recommended@17.0.0(stylelint@17.5.0(typescript@5.9.3)):
dependencies:
stylelint: 17.4.0(typescript@5.9.3)
stylelint: 17.5.0(typescript@5.9.3)
stylelint-config-recommended@18.0.0(stylelint@17.4.0(typescript@5.9.3)):
stylelint-config-recommended@18.0.0(stylelint@17.5.0(typescript@5.9.3)):
dependencies:
stylelint: 17.4.0(typescript@5.9.3)
stylelint: 17.5.0(typescript@5.9.3)
stylelint-config-standard-scss@17.0.0(postcss@8.5.8)(stylelint@17.4.0(typescript@5.9.3)):
stylelint-config-standard-scss@17.0.0(postcss@8.5.8)(stylelint@17.5.0(typescript@5.9.3)):
dependencies:
stylelint: 17.4.0(typescript@5.9.3)
stylelint-config-recommended-scss: 17.0.0(postcss@8.5.8)(stylelint@17.4.0(typescript@5.9.3))
stylelint-config-standard: 40.0.0(stylelint@17.4.0(typescript@5.9.3))
stylelint: 17.5.0(typescript@5.9.3)
stylelint-config-recommended-scss: 17.0.0(postcss@8.5.8)(stylelint@17.5.0(typescript@5.9.3))
stylelint-config-standard: 40.0.0(stylelint@17.5.0(typescript@5.9.3))
optionalDependencies:
postcss: 8.5.8
stylelint-config-standard@40.0.0(stylelint@17.4.0(typescript@5.9.3)):
stylelint-config-standard@40.0.0(stylelint@17.5.0(typescript@5.9.3)):
dependencies:
stylelint: 17.4.0(typescript@5.9.3)
stylelint-config-recommended: 18.0.0(stylelint@17.4.0(typescript@5.9.3))
stylelint: 17.5.0(typescript@5.9.3)
stylelint-config-recommended: 18.0.0(stylelint@17.5.0(typescript@5.9.3))
stylelint-order@6.0.4(stylelint@17.4.0(typescript@5.9.3)):
stylelint-order@6.0.4(stylelint@17.5.0(typescript@5.9.3)):
dependencies:
postcss: 8.5.8
postcss-sorting: 8.0.2(postcss@8.5.8)
stylelint: 17.4.0(typescript@5.9.3)
stylelint: 17.5.0(typescript@5.9.3)
stylelint-scss@7.0.0(stylelint@17.4.0(typescript@5.9.3)):
stylelint-scss@7.0.0(stylelint@17.5.0(typescript@5.9.3)):
dependencies:
css-tree: 3.1.0
is-plain-object: 5.0.0
@ -13034,31 +13093,31 @@ snapshots:
postcss-resolve-nested-selector: 0.1.6
postcss-selector-parser: 7.1.1
postcss-value-parser: 4.2.0
stylelint: 17.4.0(typescript@5.9.3)
stylelint: 17.5.0(typescript@5.9.3)
stylelint-use-logical@2.1.3(stylelint@17.4.0(typescript@5.9.3)):
stylelint-use-logical@2.1.3(stylelint@17.5.0(typescript@5.9.3)):
dependencies:
stylelint: 17.4.0(typescript@5.9.3)
stylelint: 17.5.0(typescript@5.9.3)
stylelint@17.4.0(typescript@5.9.3):
stylelint@17.5.0(typescript@5.9.3):
dependencies:
'@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-syntax-patches-for-csstree': 1.0.28
'@csstools/css-syntax-patches-for-csstree': 1.1.1(css-tree@3.2.1)
'@csstools/css-tokenizer': 4.0.0
'@csstools/media-query-list-parser': 5.0.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/selector-resolve-nested': 4.0.0(postcss-selector-parser@7.1.1)
'@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.1)
colord: 2.9.3
cosmiconfig: 9.0.0(typescript@5.9.3)
cosmiconfig: 9.0.1(typescript@5.9.3)
css-functions-list: 3.3.3
css-tree: 3.1.0
css-tree: 3.2.1
debug: 4.4.3
fast-glob: 3.3.3
fastest-levenshtein: 1.0.16
file-entry-cache: 11.1.2
global-modules: 2.0.0
globby: 16.1.0
globby: 16.1.1
globjoin: 0.1.4
html-tags: 5.1.0
ignore: 7.0.5
@ -13066,7 +13125,7 @@ snapshots:
imurmurhash: 0.1.4
is-plain-object: 5.0.0
mathml-tag-names: 4.0.0
meow: 14.0.0
meow: 14.1.0
micromatch: 4.0.8
normalize-path: 3.0.0
picocolors: 1.1.1
@ -13074,7 +13133,7 @@ snapshots:
postcss-safe-parser: 7.0.1(postcss@8.5.8)
postcss-selector-parser: 7.1.1
postcss-value-parser: 4.2.0
string-width: 8.1.1
string-width: 8.2.0
supports-hyperlinks: 4.4.0
svg-tags: 1.0.0
table: 6.9.0
@ -13546,7 +13605,7 @@ snapshots:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
postcss: 8.5.8
rollup: 4.59.0
rollup: 4.60.0
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 24.12.0
@ -13761,10 +13820,10 @@ snapshots:
'@babel/core': 7.26.0
'@babel/preset-env': 7.26.0(@babel/core@7.26.0)
'@babel/runtime': 7.25.4
'@rollup/plugin-babel': 5.3.1(@babel/core@7.26.0)(rollup@4.59.0)
'@rollup/plugin-node-resolve': 15.2.3(rollup@4.59.0)
'@rollup/plugin-replace': 2.4.2(rollup@4.59.0)
'@rollup/plugin-terser': 0.4.4(rollup@4.59.0)
'@rollup/plugin-babel': 5.3.1(@babel/core@7.26.0)(rollup@4.60.0)
'@rollup/plugin-node-resolve': 15.2.3(rollup@4.60.0)
'@rollup/plugin-replace': 2.4.2(rollup@4.60.0)
'@rollup/plugin-terser': 0.4.4(rollup@4.60.0)
'@surma/rollup-plugin-off-main-thread': 2.2.3
ajv: 8.18.0
common-tags: 1.8.2
@ -13773,7 +13832,7 @@ snapshots:
glob: 11.1.0
lodash: 4.17.23
pretty-bytes: 5.6.0
rollup: 4.59.0
rollup: 4.60.0
source-map: 0.8.0-beta.0
stringify-object: 3.3.0
strip-comments: 2.0.1

View File

@ -184,7 +184,6 @@ import {canPreview} from '@/models/attachment'
import type {IAttachment} from '@/modelTypes/IAttachment'
import type {ITask} from '@/modelTypes/ITask'
import {useAttachmentStore} from '@/stores/attachments'
import {formatDisplayDate, formatDateLong} from '@/helpers/time/formatDate'
import {uploadFiles, generateAttachmentUrl} from '@/helpers/attachments'
import {getHumanSize} from '@/helpers/getHumanSize'
@ -201,9 +200,9 @@ const props = withDefaults(defineProps<{
editEnabled: true,
})
// FIXME: this should go through the store
const emit = defineEmits<{
'taskChanged': [ITask],
'update:attachments': [IAttachment[]],
}>()
const EDITOR_SELECTOR = '.tiptap, .tiptap__editor, [contenteditable]'
@ -232,8 +231,7 @@ const {t} = useI18n({useScope: 'global'})
const attachmentService = shallowReactive(new AttachmentService())
const attachmentStore = useAttachmentStore()
const attachments = computed(() => attachmentStore.attachments)
const attachments = computed(() => props.task.attachments ?? [])
const loading = computed(() => attachmentService.loading || taskStore.isLoading)
@ -335,7 +333,10 @@ function uploadNewAttachment() {
async function uploadFilesToTask(files: File[] | FileList) {
try {
await uploadFiles(attachmentService, props.task.id, files)
const uploaded = await uploadFiles(attachmentService, props.task.id, files)
if (uploaded.length > 0) {
emit('update:attachments', [...attachments.value, ...uploaded])
}
} catch (e) {
error(e)
}
@ -354,7 +355,8 @@ async function deleteAttachment() {
try {
const r = await attachmentService.delete(attachmentToDelete.value)
attachmentStore.removeById(attachmentToDelete.value.id)
const updated = attachments.value.filter(a => a.id !== attachmentToDelete.value!.id)
emit('update:attachments', updated)
success(r)
setAttachmentToDelete(null)
} catch (e) {

View File

@ -2,9 +2,8 @@ import AttachmentModel from '@/models/attachment'
import type {IAttachment} from '@/modelTypes/IAttachment'
import AttachmentService from '@/services/attachment'
import {useTaskStore} from '@/stores/tasks'
export async function uploadFile(taskId: number, file: File, onSuccess?: (url: string) => void) {
export async function uploadFile(taskId: number, file: File, onSuccess?: (url: string) => void): Promise<IAttachment[]> {
const attachmentService = new AttachmentService()
const files = [file]
@ -16,16 +15,14 @@ export async function uploadFiles(
taskId: number,
files: File[] | FileList,
onSuccess?: (attachmentUrl: string) => void,
) {
): Promise<IAttachment[]> {
const attachmentModel = new AttachmentModel({taskId})
const response = await attachmentService.create(attachmentModel, files)
console.debug(`Uploaded attachments for task ${taskId}, response was`, response)
const uploaded: IAttachment[] = []
response.success?.map((attachment: IAttachment) => {
useTaskStore().addTaskAttachment({
taskId,
attachment,
})
uploaded.push(attachment)
onSuccess?.(generateAttachmentUrl(taskId, attachment.id))
})
@ -33,6 +30,8 @@ export async function uploadFiles(
const messages = response.errors.map((e: {message: string}) => e.message)
throw new Error(messages.join('\n'))
}
return uploaded
}
export function generateAttachmentUrl(taskId: number, attachmentId: number) {

View File

@ -43,6 +43,7 @@
"forgotPassword": "Zapomenuté heslo?",
"resetPassword": "Obnovit heslo",
"resetPasswordAction": "Poslat odkaz na obnovení hesla",
"resetPasswordSuccess": "Zkontrolujte doručenou poštu! Měli byste obdržet email s pokyny, jak obnovit své heslo.",
"passwordsDontMatch": "Hesla se neshodují",
"confirmEmailSuccess": "Úspěšně jste potvrdili svůj e-mail! Nyní se můžete přihlásit.",
"totpTitle": "Kód dvoufaktorového ověření",
@ -60,43 +61,88 @@
"usernameMustNotLookLikeUrl": "Uživatelské jméno nesmí vypadat jako adresa URL.",
"passwordRequired": "Zadejte prosím heslo.",
"passwordNotMin": "Heslo musí mít nejméně 8 znaků.",
"passwordNotMax": "Heslo může mít maximálně 72 znaků.",
"showPassword": "Ukázat heslo",
"hidePassword": "Skrýt heslo",
"noAccountYet": "Ještě nemáte účet?",
"alreadyHaveAnAccount": "Už máte svůj účet?",
"remember": "Zůstat trvale přihlášen"
"remember": "Zůstat trvale přihlášen",
"registrationDisabled": "Registrace je vypnuta.",
"passwordResetTokenMissing": "Chybí token pro obnovení hesla.",
"registrationFailed": "Při registraci došlo k chybě. Zkontrolujte prosím zadané údaje a zkuste to znovu."
},
"settings": {
"title": "Nastavení",
"newPasswordTitle": "Aktualizujte své heslo",
"newPassword": "Nové heslo",
"newPasswordConfirm": "Potvrzení nového hesla",
"currentPassword": "Stávající heslo",
"currentPasswordPlaceholder": "Vaše současné heslo",
"passwordsDontMatch": "Nové heslo se neshoduje s potvrzením hesla.",
"passwordUpdateSuccess": "Heslo bylo úspěšně změněno.",
"updateEmailTitle": "Aktualizujte svou e-mailovou adresu",
"updateEmailNew": "Nová emailová adresa",
"updateEmailSuccess": "Vaše e-mailová adresa byla úspěšně aktualizována. Poslali jsme vám odkaz pro její potvrzení.",
"general": {
"title": "Obecná nastavení",
"name": "Moje jméno",
"newName": "Nové jméno",
"savedSuccess": "Nastavení bylo úspěšně aktualizováno.",
"emailReminders": "Posílat mi připomenutí pro úkoly emailem",
"overdueReminders": "Pošlete mi každý den shrnutí mých zpožděných úkolů",
"discoverableByName": "Umožnit ostatním uživatelům přidat mě jako člena do týmů nebo projektů, když hledají mé jméno",
"discoverableByEmail": "Umožnit ostatním uživatelům, aby mě přidali jako člena do týmů nebo projektů, když hledají můj úplný e-mail",
"playSoundWhenDone": "Přehrát zvuk při označení úkolů jako hotovo",
"allowIconChanges": "Zobrazit speciální loga v určitých časech",
"alwaysShowBucketTaskCount": "Vždy zobrazit počet úkolů ve sloupcích Kanbanu",
"defaultTaskRelationType": "Výchozí typ vztahu úkolů",
"weekStart": "První den týdne",
"weekStartSunday": "Neděle",
"weekStartMonday": "Pondělí",
"language": "Jazyk",
"defaultProject": "Výchozí projekt",
"defaultView": "Výchozí zobrazení",
"timezone": "Časové pásmo",
"overdueTasksRemindersTime": "Čas odeslání emailu o zpožděných úkolech",
"filterUsedOnOverview": "Uložený filtr použitý na stránce přehledu"
"filterUsedOnOverview": "Uložený filtr použitý na stránce přehledu",
"minimumPriority": "Minimální viditelná priorita úkolu",
"dateDisplay": "Formát zobrazení data",
"dateDisplayOptions": {
"relative": "Relativní (např. před 3 dny)",
"mm-dd-yyyy": "MM-DD-RRRR",
"dd-mm-yyyy": "DD-MM-RRRR",
"yyyy-mm-dd": "RRRR-MM-DD",
"mm/dd/yyyy": "MM/DD/RRRR",
"dd/mm/yyyy": "DD/MM/RRRR",
"yyyy/mm/dd": "RRRR/MM/DD"
},
"timeFormat": "Formát času",
"timeFormatOptions": {
"12h": "12 hodinový (AM/PM)",
"24h": "24 hodinový (HH:mm)"
},
"externalUserNameChange": "Vaše jméno je spravováno vaším poskytovatelem přihlášení ({provider}). Chcete-li ho změnit, aktualizujte ho tam."
},
"sections": {
"personalInformation": "Osobní údaje",
"taskAndNotifications": "Projekty a úkoly",
"privacy": "Ochrana soukromí",
"localization": "Lokalizace",
"appearance": "Vzhled a chování"
},
"totp": {
"title": "Dvoufaktorové ověření",
"enroll": "Zápis",
"finishSetupPart1": "Chcete-li dokončit nastavení, použijte tento tajný klíč ve vaší TOTP aplikaci (Google Authenticator nebo podobné):",
"finishSetupPart2": "Poté zadejte kód z vaší aplikace.",
"scanQR": "Případně můžete naskenovat tento QR kód:",
"passcode": "Přístupový kód",
"passcodePlaceholder": "Kód vygenerovaný vaší TOTP aplikací",
"confirmNotice": "Po povolení dvoufázového ověřování budete odhlášeni ze všech relací a musíte se znovu přihlásit.",
"setupSuccess": "Úspěšně jste nastavili dvoufaktorové ověření!",
"enterPassword": "Zadejte prosím heslo",
"disable": "Zakázat dvoufaktorové ověření",
"confirmSuccess": "Úspěšně jste povolili dvoufázové ověřování!",
"disableSuccess": "Dvoufázové ověření bylo úspěšně vypnuto."
},
"caldav": {
@ -119,7 +165,9 @@
"upload": "Nahrát",
"uploadAvatar": "Nahrát avatara",
"statusUpdateSuccess": "Stav avatara byl úspěšně aktualizován!",
"setSuccess": "Avatar byl úspěšně nastaven!"
"setSuccess": "Avatar byl úspěšně nastaven!",
"ldap": "Váš avatar je automaticky synchronizován s adresářovou službou vaší organizace (LDAP). Informujte se u vašeho IT týmu o tom, jak jej změnit.",
"openid": "Váš avatar je automaticky synchronizován s poskytovatelem přihlášení ({provider}). Chcete-li ho změnit, aktualizujte ho tam."
},
"quickAddMagic": {
"title": "Kouzelný režim pro rychlé přidání",
@ -136,6 +184,13 @@
"dark": "Tmavý"
}
},
"backgroundBrightness": {
"title": "Jas pozadí"
},
"webhooks": {
"title": "Webhook oznámení",
"description": "Nastavte URL webhooků, který přijme POST požadavek při překročení termínu nebo připomenutí. Tyto webhooky přijímají události ze všech vašci projektů."
},
"apiTokens": {
"title": "API Tokeny",
"general": "API tokeny umožňují používat Vikunja API bez uživatelských údajů.",
@ -147,6 +202,7 @@
"90d": "90 dní",
"permissionExplanation": "Oprávnění vám umožní nastavit, k čemu lze api token použít.",
"titleRequired": "Je vyžadován název",
"permissionRequired": "Vyberte prosím alespoň jedno oprávnění ze seznamu.",
"expired": "Platnost tohoto tokenu vypršela {ago}.",
"tokenCreatedSuccess": "Zde je tvůj nový api token: {token}",
"tokenCreatedNotSeeAgain": "Ulož jej na zabezpečeném místě, už ho znovu neuvidíš!",
@ -162,6 +218,20 @@
"expiresAt": "Vyprší v",
"permissions": "Oprávnění"
}
},
"sessions": {
"title": "Relace",
"description": "Toto jsou všechna zařízení aktuálně přihlášená k vašemu účtu. Můžete zrušit jakoukoli relaci a odhlásit zařízení. Ukončení platnosti může trvat až 10 minut.",
"deviceInfo": "Zařízení",
"ipAddress": "IP adresa",
"lastActive": "Poslední aktivita",
"current": "Tato relace",
"delete": {
"header": "Zrušit relaci",
"text": "Opravdu chcete zrušit tuto relaci? Zařízení bude odhlášeno. Úplné vypršení platnosti relace může trvat až 10 minut."
},
"deleteSuccess": "Relace byla úspěšně zrušena. Úplné vypršení platnosti relace může trvat až 10 minut.",
"noOtherSessions": "Žádné další aktivní relace."
}
},
"deletion": {
@ -186,13 +256,16 @@
"descriptionPasswordRequired": "Pokračujte zadáním vašeho hesla:",
"request": "Požádat o kopii mých dat",
"success": "Úspěšně jste požádali o svá data! Jakmile budou připravena ke stažení, pošleme Vám e-mail.",
"downloadTitle": "Stáhnout exportovaná Vikunja data"
"downloadTitle": "Stáhnout exportovaná Vikunja data",
"ready": "Váš export je připraven ke stažení. Můžete jej stáhnout do {0}.",
"requestNew": "Požádat o další export"
}
},
"project": {
"archivedMessage": "Tento projekt je archivován. Není možné vytvořit ani upravovat jeho úkoly.",
"archived": "Archivováno",
"showArchived": "Zobrazit archivované",
"title": "Název",
"color": "Barva",
"projects": "Projekty",
"parent": "Nadřazený projekt",
@ -201,6 +274,11 @@
"shared": "Sdílené projekty",
"noDescriptionAvailable": "Není k dispozici žádný popis projektu.",
"inboxTitle": "Inbox",
"favorite": "Označit tento projekt jako oblíbený",
"unfavorite": "Odstranit tento projekt z oblíbených",
"openSettingsMenu": "Otevřít nastavení projektu",
"description": "Popis projektu",
"favoriteDescription": "Tento projekt má všechny úkoly označené jako oblíbené.",
"create": {
"header": "Nový projekt",
"titlePlaceholder": "Název projektu přijde sem…",
@ -288,11 +366,20 @@
"addedSuccess": "{type} byl úspěšně přidán.",
"updatedSuccess": "{type} byl úspěšně přidán."
},
"permission": {
"title": "Oprávnění",
"read": "Pouze pro čtení",
"readWrite": "Čtení a zápis",
"admin": "Administrátor"
},
"attributes": {
"link": "Odkaz",
"delete": "Smazat"
}
},
"first": {
"title": "První pohled"
},
"list": {
"title": "Seznam",
"add": "Přidat",
@ -308,7 +395,27 @@
"month": "Měsíc",
"day": "Den",
"hour": "Hodina",
"range": "Časové období"
"range": "Časové období",
"chartLabel": "Projektový Ganttův diagram",
"taskBarsForRow": "Chlívky pro řádek {rowId}",
"taskBarLabel": "Úkol: {task}. Od {startDate} do {endDate}. {dateType}. Klikněte pro úpravu, přetáhněte pro přesun.",
"scheduledDates": "Plánovaná data",
"estimatedDates": "Předpokládaná data",
"resizeStartDate": "Změňte počáteční datum pro {task}",
"resizeEndDate": "Změňte koncové datum pro {task}",
"timelineHeader": "Hlavička časového plánu s měsíci a dny",
"monthsRow": "Řádek měsíců",
"daysRow": "Řádek dnů",
"monthLabel": "Měsíc: {month}",
"dayLabel": "Den: {date}, {weekday}",
"dayLabelToday": "Dnes: {date}, {weekday}",
"taskAriaLabel": "Úkol: {task}",
"taskAriaLabelById": "Úkol {id}",
"partialDatesStart": "Pouze datum zahájení (otevřený konec)",
"partialDatesEnd": "Pouze datum konce (otevřený konec)",
"expandGroup": "Rozbalit skupinu: {task}",
"collapseGroup": "Sbalit skupinu: {task}",
"toggleRelationArrows": "Přepnout šipky relací"
},
"table": {
"title": "Tabulka",
@ -337,7 +444,8 @@
"deleteBucketSuccess": "Sloupec byl úspěšně smazán.",
"bucketTitleSavedSuccess": "Název sloupce byl úspěšně uložen.",
"bucketLimitSavedSuccess": "Limit sloupce byl úspěšně uložen.",
"collapse": "Sbalit tento sloupec"
"collapse": "Sbalit tento sloupec",
"bucketLimitReached": "Dosáhli jste limitu sloupce. Odstraňte úkoly nebo zvýšte limit pro přidání nových úkolů."
},
"pseudo": {
"favorites": {
@ -349,12 +457,16 @@
"targetUrl": "Cílová URL",
"targetUrlInvalid": "Zadejte prosím platnou URL adresu.",
"events": "Události",
"eventsHint": "Vyberte všechny události, pro které by tento webhook měl dostávat aktualizace (v rámci aktuálního projektu).",
"mustSelectEvents": "Musíte zvolit alespoň jednu událost.",
"delete": "Smazat tento webhook",
"deleteText": "Opravdu chcete odstranit tento webhook? Externí cíle již nebudou informovány o jeho událostech.",
"deleteSuccess": "Webhook byl úspěšně odstraněn.",
"create": "Vytvořit webhook",
"secret": "Tajný klíč",
"basicauthuser": "Uživatel pro Basic Auth",
"basicauthpassword": "Heslo pro Basic Auth",
"basicauthlink": "Použít Basic Auth?",
"secretHint": "Pokud je zadáno, všechny požadavky na cílovou adresu URL webhooku budou podepsány pomocí HMAC.",
"secretDocs": "Další podrobnosti o používání tajných klíčů naleznete v dokumentaci."
},
@ -372,13 +484,18 @@
"titleRequired": "Zadejte prosím název.",
"delete": "Odstranit tento pohled",
"deleteText": "Jste si jisti, že chcete odstranit tento pohled? Nebude již možné jej použít k zobrazení úkolů v tomto projektu. Tato akce neodstraní žádné úkoly. Toto nelze vrátit zpět!",
"onlyAdminsCanEdit": "Pohledy mohou upravovat pouze správci projektu."
"deleteSuccess": "Pohled byl úspěšně odstraněn.",
"onlyAdminsCanEdit": "Pohledy mohou upravovat pouze správci projektu.",
"updateSuccess": "Pohled byl úspěšně upraven."
}
},
"filters": {
"title": "Filtry",
"clear": "Vymazat filtry",
"showResults": "Zobrazit výsledky",
"noResults": "Žádné výsledky",
"fromView": "Aktuální pohled má také nastavený filtr:",
"fromViewBoth": "Bude použito v kombinaci s tím, co zde zadáte.",
"attributes": {
"title": "Název",
"titlePlaceholder": "Název uloženého filtru přijde sem…",
@ -416,6 +533,7 @@
"help": {
"intro": "K filtrování úkolů můžete použít syntaxi dotazů podobnou SQL. Dostupná pole pro filtrování zahrnují:",
"link": "Jak to funguje?",
"canUseDatemath": "Můžete použít matematiku a nastavit relativní data. Pro více informací klikněte na datum v dotazu.",
"fields": {
"done": "Zda je úkol dokončen nebo ne",
"priority": "Úroveň priority úkolu (1-5)",
@ -426,7 +544,10 @@
"doneAt": "Datum a čas, kdy byl úkol dokončen",
"assignees": "Přiřazení uživatelé",
"labels": "Štítky přiřazené úkolu",
"project": "Projekt, do kterého úkol patří (k dispozici pouze pro uložené filtry, ne na úrovni projektu)"
"project": "Projekt, do kterého úkol patří (k dispozici pouze pro uložené filtry, ne na úrovni projektu)",
"reminders": "Připomenutí úlohy jako pole s datem, vrátí všechny úkoly s alespoň jednou upomínkou odpovídající dotazu",
"created": "Čas a datum, kdy byl úkol vytvořen",
"updated": "Čas a datum, kdy byl úkol naposledy změněn"
},
"operators": {
"intro": "Dostupné operátory pro filtrování zahrnují:",
@ -436,7 +557,9 @@
"greaterThanOrEqual": "Větší nebo rovno než",
"lessThan": "Menší než",
"lessThanOrEqual": "Menší nebo rovno než",
"like": "Odpovídá vzoru (s použitím zástupného znaku %)"
"like": "Odpovídá vzoru (s použitím zástupného znaku %)",
"in": "Odpovídá libovolné hodnotě v seznamu hodnot oddělených čárkou",
"notIn": "Odpovídá libovolné hodnotě nepřítomné v seznamu hodnot oddělených čárkou"
},
"logicalOperators": {
"intro": "Pro kombinování více podmínek můžete použít tyto logické operátory:",
@ -502,7 +625,11 @@
"authenticating": "Ověřování…",
"passwordRequired": "Tento sdílený projekt vyžaduje heslo. Zadejte jej níže:",
"error": "Došlo k chybě.",
"invalidPassword": "Neplatné heslo."
"invalidPassword": "Neplatné heslo.",
"accessDenied": "Přístup byl odepřen. Zkontrolujte svá oprávnění a zkuste to znovu.",
"serverError": "Došlo k chybě serveru. Opakujte akci později.",
"projectLoadError": "Nepodařilo se načíst informace o projektu.",
"retry": "Opakovat"
},
"navigation": {
"overview": "Přehled",
@ -543,7 +670,8 @@
"created": "Vytvořeno",
"createdBy": "Vytvořil(a) {0}",
"actions": "Akce",
"cannotBeUndone": "Toto nelze vrátit!"
"cannotBeUndone": "Toto nelze vrátit!",
"avatarOfUser": "Profilový obrázek {user}"
},
"input": {
"resetColor": "Obnovit barvu",
@ -700,6 +828,7 @@
"addReminder": "Přidat připomínku…",
"doneSuccess": "Úkol byl úspěšně označen jako dokončený.",
"undoneSuccess": "Úkol byl úspěšně znovu otevřen.",
"movedToProject": "Úkol byl přesunut do {project}.",
"undo": "Vrátit zpět",
"openDetail": "Otevřít zobrazení detailu úkolu",
"checklistTotal": "{checked} z {total} úkolů",
@ -707,10 +836,13 @@
"show": {
"titleCurrent": "Aktuální úkoly",
"titleDates": "Úkoly od {from} do {to}",
"noDates": "Zobrazit úkoly bez datumu",
"overdue": "Zobrazit zpožděné úkoly",
"fromuntil": "Úkoly od {from} do {until}",
"select": "Vyberte rozsah dat",
"noTasks": "Nic na práci - užij si pěkný den!"
"noTasks": "Nic na práci - užij si pěkný den!",
"filterByLabel": "Filtrování podle štítku {label}",
"clearLabelFilter": "Vymazat filtr štítků"
},
"detail": {
"chooseDueDate": "Klikněte zde pro nastavení termínu dokončení",
@ -724,9 +856,12 @@
"doneAt": "Dokončeno {0}",
"updateSuccess": "Úkol byl úspěšně uložen.",
"deleteSuccess": "Úkol byl úspěšně smazán.",
"duplicateSuccess": "Úkol byl úspěšně duplikován.",
"belongsToProject": "Tento úkol patří do projektu '{project}'",
"back": "Zpět k projektu",
"due": "Termín {at}",
"closePopup": "Zavřít vyskakovací okno",
"scrollToBottom": "Přejít na konec",
"organization": "Organizace",
"management": "Management",
"dateAndTime": "Datum a čas",
@ -748,6 +883,7 @@
"attachments": "Přidat přílohy",
"relatedTasks": "Přidat vztah",
"moveProject": "Přesunout",
"duplicate": "Duplikovat",
"color": "Nastavit barvu",
"delete": "Smazat",
"favorite": "Přidat do oblíbených",
@ -766,9 +902,12 @@
"labels": "Štítky",
"percentDone": "Průběh",
"priority": "Priorita",
"project": "Projekt",
"relatedTasks": "Související úkoly",
"reminders": "Připomínky",
"repeat": "Opakovat",
"comment": "{count} komentář | {count} komentáře",
"commentCount": "Počet komentářů",
"startDate": "Počáteční datum",
"title": "Název",
"updated": "Aktualizováno",
@ -813,7 +952,13 @@
"delete": "Smazat tento komentář",
"deleteText1": "Opravdu chcete smazat tento komentář?",
"deleteSuccess": "Komentář byl úspěšně odstraněn.",
"addedSuccess": "Komentář byl úspěšně přidán."
"addedSuccess": "Komentář byl úspěšně přidán.",
"permalink": "Kopírovat trvalý odkaz na tento komentář",
"sortNewestFirst": "Od nejnovějších",
"sortOldestFirst": "Od nejstarších"
},
"mention": {
"noUsersFound": "Nenalezeni žádní uživatelé"
},
"deferDueDate": {
"title": "Odložit datum dokončení",
@ -922,6 +1067,7 @@
"project4": "Například: {prefix}\"Projekt s mezerami\".",
"dateAndTime": "Datum a čas",
"date": "Jakékoliv datum bude použito jako datum dokončení nového úkolu. Můžete použít data v kterémkoli z těchto formátů:",
"dateWeekday": "jakýkoli pracovní den, použije se další datum s tímto dnem",
"dateCurrentYear": "použije aktuální rok",
"dateNth": "použije {day}. den aktuálního měsíce",
"dateTime": "Pro nastavení času zkombinujte libovolný formát data s \"{time}\" (nebo {timePM}).",
@ -1003,7 +1149,12 @@
"delete": "Smazat tento úkol",
"priority": "Změnit prioritu tohoto úkolu",
"favorite": "Označit tuto úlohu jako oblíbenou / odebrat oblíbené",
"save": "Uložit aktuální úkol"
"openProject": "Otevřít projekt tohoto úkolu",
"save": "Uložit aktuální úkol",
"copyIdentifier": "Kopírovat identifikátor úkolu do schránky",
"copyIdentifierAndTitle": "Kopírovat identifikátor úkolu a název do schránky",
"copyIdentifierTitleAndUrl": "Kopírovat identifikátor úkolu, název a URL do schránky",
"copyUrl": "Kopírovat URL úkolu do schránky"
},
"project": {
"title": "Zobrazení projektu",
@ -1019,6 +1170,21 @@
"labels": "Přejít na štítky",
"teams": "Přejít na týmy",
"projects": "Přejít na projekty"
},
"list": {
"title": "Seznam úkolů",
"navigateDown": "Zvýraznit další úkol",
"navigateUp": "Zvýraznit předchozí úkol",
"open": "Otevřít zvýrazněný úkol"
},
"gantt": {
"title": "Ganttův diagram",
"moveTaskLeft": "Přesunout úkol na dřívější datum",
"moveTaskRight": "Přesunout úkol na pozdější datum",
"expandTaskLeft": "Posuň počáteční datum na dříve",
"expandTaskRight": "Posuň koncové datum na později",
"shrinkTaskLeft": "Posuň počáteční datum na později",
"shrinkTaskRight": "Posuň koncové datum na dříve"
}
},
"update": {
@ -1056,6 +1222,7 @@
"notification": {
"title": "Oznámení",
"none": "Nemáte žádná oznámení. Mějte příjemný den!",
"explainer": "Upozornění se zobrazí zde, když proběhne akce na projektech nebo úkolech, ke kterým jste se přihlásili.",
"markAllRead": "Označit všechna oznámení za přečtená",
"markAllReadSuccess": "Všechna oznámení byla označena jako přečtená."
},
@ -1079,6 +1246,7 @@
}
},
"date": {
"altFormatLong": "j M Y, H:i",
"altFormatShort": "j M Y"
},
"reaction": {
@ -1100,15 +1268,28 @@
"1012": "E-mailová adresa uživatele nebyla potvrzena.",
"1013": "Nové heslo je prázdné.",
"1014": "Staré heslo je prázdné.",
"1015": "TOTP je již pro tohoto uživatele povoleno.",
"1016": "Totp není pro tohoto uživatele povoleno.",
"1017": "Kód TOTP je neplatný.",
"1018": "Nastavení typu avatara uživatele je neplatné.",
"1019": "Poskytovatel OpenID neposkytl žádnou e-mailovou adresu. Ujistěte se, že poskytovatel openID veřejně poskytuje e-mailovou adresu pro váš účet.",
"1020": "Tento účet je zakázán. Zkontrolujte své e-maily nebo se zeptejte správce.",
"1021": "Tento účet je spravován poskytovatelem ověření třetí strany.",
"1022": "Uživatelské jméno nesmí obsahovat mezery.",
"1023": "To nemůžete udělat se sdíleným odkazem.",
"1024": "Neplatná žádost o data pro pole {field} typu {type}.",
"1025": "Časové pásmo '{timezone}' je neplatné. Zvolte prosím platné časové pásmo ze seznamu.",
"2001": "ID nemůže být prázdné nebo 0.",
"2002": "Některé údaje požadavku byly neplatné.",
"2003": "Časové pásmo '{timezone}' je neplatné.",
"3001": "Projekt neexistuje.",
"3004": "Pro provedení této akce musíte mít oprávnění ke čtení k tomuto projektu.",
"3005": "Název projektu nemůže být prázdný.",
"3006": "Sdílení projektu neexistuje.",
"3007": "Projekt s tímto identifikátorem již existuje.",
"3008": "Projekt je archivován, a proto je přístupný pouze pro čtení. To platí i pro všechny úkoly spojené s tímto projektem.",
"4001": "Název úkolu nemůže být prázdný.",
"4002": "Tento úkol neexistuje.",
"4003": "Všechny úkoly pro hromadnou úpravu musí patřit do stejného projektu.",
"4004": "Při hromadných úpravách úkolů je potřeba alespoň jeden úkol.",
"4005": "Nemáte právo vidět tento úkol.",
@ -1126,12 +1307,18 @@
"4017": "Neplatný komparátor filtru úkolů.",
"4018": "Neplatné zřetězení filtru úkolů.",
"4019": "Neplatná hodnota filtru úkolů.",
"4020": "Tato příloha nepatří k tomu úkolu.",
"4021": "Tento uživatel je k tomuto úkolu již přiřazen.",
"4022": "Uveďte prosím datum, s nímž připomenutí souvisí.",
"4023": "Nelze vytvořit cyklus vztahů mezi úkoly.",
"6001": "Název týmu nemůže být prázdný.",
"6002": "Tým neexistuje.",
"6004": "Tým již má k tomuto projektu přístup.",
"6005": "Uživatel je již členem tohoto týmu.",
"6006": "Nelze odstranit posledního člena týmu.",
"6007": "Tým nemá přístup k seznamu pro provedení této akce.",
"6008": "Pro dané OIDC ID a vydavatele nebyl nalezen žádný tým.",
"6009": "Pro uživatele nebyly nalezeny žádné týmy s vlastností oidcId.",
"7002": "Uživatel již má přístup k tomuto projektu.",
"7003": "K tomuto projektu nemáte přístup.",
"8001": "Tento štítek již v tomto úkolu existuje.",
@ -1147,14 +1334,21 @@
"11002": "Uložené filtry nejsou k dispozici pro sdílení odkazů.",
"12001": "Typ předplatného je neplatný.",
"12002": "Již jste přihlášeni k odběru samotného subjektu nebo nadřazeného subjektu.",
"12003": "Musíte zadat uživatele pro načtení odběru.",
"13001": "Tento sdílený odkaz vyžaduje heslo k ověření, ale žádné nebylo poskytnuto.",
"13002": "Zadané heslo pro sdílený odkaz je neplatné.",
"13003": "Zadaný token pro sdílený odkaz je neplatný.",
"14001": "Zadaný api token je neplatný.",
"14002": "Oprávnění {permission} skupiny {group} je neplatné.",
"error": "Chyba",
"success": "Úspěch",
"0001": "Nejste oprávněni to udělat."
},
"about": {
"title": "O aplikaci",
"version": "Verze: {version}"
"version": "Verze: {version}",
"frontendVersion": "Verze frontendu: {version}",
"apiVersion": "Verze API: {version}"
},
"time": {
"units": {

View File

@ -138,9 +138,11 @@
"scanQR": "Alternativ kannst du auch diesen QR-Code scannen:",
"passcode": "Code",
"passcodePlaceholder": "Ein von deiner TOTP-App generierter Code",
"confirmNotice": "Nach der Aktivierung der Zwei-Faktor-Authentifizierung wirst du überall abgemeldet und musst dich erneut anmelden.",
"setupSuccess": "Du hast die Zwei-Faktor-Authentifizierung erfolgreich eingerichtet!",
"enterPassword": "Bitte gib dein Passwort ein",
"disable": "Zwei-Faktor-Authentifizierung deaktivieren",
"confirmSuccess": "Du hast die Zwei-Faktor-Authentifizierung erfolgreich eingerichtet!",
"disableSuccess": "Die Zwei-Faktor-Authentifizierung wurde erfolgreich deaktiviert."
},
"caldav": {

View File

@ -138,9 +138,11 @@
"scanQR": "Alternativ chasch au de QR Code scanne:",
"passcode": "Code",
"passcodePlaceholder": "Ein von deiner TOTP-App generierter Code",
"confirmNotice": "Nach der Aktivierung der Zwei-Faktor-Authentifizierung wirst du überall abgemeldet und musst dich erneut anmelden.",
"setupSuccess": "Du hast die Zwei-Faktor-Authentifizierung erfolgreich eingerichtet!",
"enterPassword": "Bitte gib diis Passwort iih",
"disable": "Zweifaktor Authentifizierig uusschalte",
"confirmSuccess": "Du hast die Zwei-Faktor-Authentifizierung erfolgreich eingerichtet!",
"disableSuccess": "Die Zwei-Faktor-Authentifizierung wurde erfolgreich deaktiviert."
},
"caldav": {

View File

@ -214,6 +214,13 @@
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
"selectAll": "Select all",
"presets": {
"title": "Quick presets",
"readOnly": "Read only",
"tasks": "Task management",
"projects": "Project management",
"fullAccess": "Full access"
},
"delete": {
"header": "Delete this token",
"text1": "Are you sure you want to delete the token \"{token}\"?",

View File

@ -68,7 +68,8 @@
"alreadyHaveAnAccount": "Уже есть аккаунт?",
"remember": "Оставаться в системе",
"registrationDisabled": "Регистрация отключена.",
"passwordResetTokenMissing": "Не указан токен сброса пароля."
"passwordResetTokenMissing": "Не указан токен сброса пароля.",
"registrationFailed": "Произошла ошибка при регистрации. Проверьте введённые данные и повторите попытку."
},
"settings": {
"title": "Настройки",
@ -93,6 +94,7 @@
"discoverableByEmail": "Разрешить другим пользователям добавлять меня в состав команд или проектов при поиске моего полного email",
"playSoundWhenDone": "Проигрывать звук, когда задача помечается завершённой",
"allowIconChanges": "Показывать специальные логотипы в определённое время",
"alwaysShowBucketTaskCount": "Всегда показывать количество задач в колонках Канбана",
"defaultTaskRelationType": "Тип связанной задачи по умолчанию",
"weekStart": "Первый день недели",
"weekStartSunday": "Воскресенье",
@ -136,9 +138,11 @@
"scanQR": "Или вы можете отсканировать этот QR-код:",
"passcode": "Код",
"passcodePlaceholder": "Код, который сгенерировало приложение TOTP",
"confirmNotice": "После включения двухфакторной аутентификации все ваши сеансы будут завершены и вам нужно будет войти заново.",
"setupSuccess": "Двухфакторная аутентификация успешно подключена!",
"enterPassword": "Введите свой пароль",
"disable": "Отключить двухфакторную аутентификацию",
"confirmSuccess": "Двухфакторная аутентификация успешно подключена!",
"disableSuccess": "Двухфакторная аутентификация отключена."
},
"caldav": {
@ -183,6 +187,10 @@
"backgroundBrightness": {
"title": "Яркость фона"
},
"webhooks": {
"title": "Уведомления через вебхуки",
"description": "Настройка адресов, которые будут получать POST-запросы при достижении времени напоминания или истечении срока задачи. Эти вебхуки будут получать события из всех ваших проектов."
},
"apiTokens": {
"title": "Токены API",
"general": "Токены API позволяют использовать Vikunja API без использования данных для входа пользователя.",
@ -210,6 +218,20 @@
"expiresAt": "Срок действия",
"permissions": "Разрешения"
}
},
"sessions": {
"title": "Сеансы",
"description": "Список всех устройств, на которых выполнен вход в ваш аккаунт. Вы можете отозвать любой сеанс, чтобы выйти из аккаунта на указанном устройстве. Это может занять до 10 минут.",
"deviceInfo": "Устройство",
"ipAddress": "IP-адрес",
"lastActive": "Последняя активность",
"current": "Текущий сеанс",
"delete": {
"header": "Отозвать сеанс",
"text": "Вы уверены, что хотите отозвать этот сеанс? Будет выполнен выход из аккаунта на этом устройстве. Это может занять до 10 минут."
},
"deleteSuccess": "Сеанс отозван. Полное завершение его действия может занять до 10 минут.",
"noOtherSessions": "Нет других активных сеансов."
}
},
"deletion": {
@ -388,7 +410,11 @@
"dayLabel": "День: {date}, {weekday}",
"dayLabelToday": "Сегодня: {date}, {weekday}",
"taskAriaLabel": "Задача: {task}",
"taskAriaLabelById": "Задача {id}"
"taskAriaLabelById": "Задача {id}",
"partialDatesStart": "Только дата начала (без окончания)",
"partialDatesEnd": "Только дата окончания (без начала)",
"expandGroup": "Развернуть группу: {task}",
"collapseGroup": "Свернуть группу: {task}"
},
"table": {
"title": "Таблица",
@ -437,6 +463,9 @@
"deleteSuccess": "Вебхук успешно удалён.",
"create": "Создать вебхук",
"secret": "Секрет",
"basicauthuser": "Basic Auth пользователь",
"basicauthpassword": "Basic Auth пароль",
"basicauthlink": "Использовать HTTP Basic аутентификацию?",
"secretHint": "Если указан, все запросы к URL обработчика будут подписаны с помощью HMAC.",
"secretDocs": "Подробнее об использовании секретов в документации."
},
@ -797,6 +826,7 @@
"addReminder": "Добавить напоминание…",
"doneSuccess": "Задача отмечена как завершённая.",
"undoneSuccess": "Задача отмечена как незавершённая.",
"movedToProject": "Задача перемещена в проект «{project}».",
"undo": "Отменить",
"openDetail": "Открыть подробный просмотр задачи",
"checklistTotal": "{checked} из {total} задач",
@ -824,10 +854,12 @@
"doneAt": "Завершено {0}",
"updateSuccess": "Задача сохранена.",
"deleteSuccess": "Задача удалена.",
"duplicateSuccess": "Задача продублирована.",
"belongsToProject": "Задача принадлежит проекту «{project}»",
"back": "Вернуться к проекту",
"due": "Истекает {at}",
"closePopup": "Закрыть всплывающее окно",
"scrollToBottom": "Прокрутить до конца страницы",
"organization": "Организация",
"management": "Управление",
"dateAndTime": "Дата и время",
@ -849,6 +881,7 @@
"attachments": "Добавить вложения",
"relatedTasks": "Добавить связь",
"moveProject": "Переместить",
"duplicate": "Создать копию",
"color": "Выбрать цвет",
"delete": "Удалить",
"favorite": "Добавить в избранное",
@ -867,6 +900,7 @@
"labels": "Метки",
"percentDone": "Прогресс",
"priority": "Приоритет",
"project": "Проект",
"relatedTasks": "Связанные задачи",
"reminders": "Напоминания",
"repeat": "Повтор",
@ -917,7 +951,9 @@
"deleteText1": "Удалить этот комментарий?",
"deleteSuccess": "Комментарий удалён.",
"addedSuccess": "Комментарий добавлен.",
"permalink": "Скопировать постоянную ссылку на комментарий"
"permalink": "Скопировать постоянную ссылку на комментарий",
"sortNewestFirst": "Сначала новые",
"sortOldestFirst": "Сначала старые"
},
"mention": {
"noUsersFound": "Пользователи не найдены"
@ -1112,7 +1148,11 @@
"priority": "Изменить приоритет задачи",
"favorite": "Добавить задачу в избранное или удалить из избранного",
"openProject": "Открыть проект с этой задачей",
"save": "Сохранить текущую задачу"
"save": "Сохранить текущую задачу",
"copyIdentifier": "Скопировать идентификатор задачи в буфер обмена",
"copyIdentifierAndTitle": "Скопировать идентификатор и заголовок задачи в буфер обмена",
"copyIdentifierTitleAndUrl": "Скопировать идентификатор, заголовок и URL задачи в буфер обмена",
"copyUrl": "Скопировать URL задачи в буфер обмена"
},
"project": {
"title": "Просмотр проекта",

View File

@ -1,306 +0,0 @@
import {parseDate} from '../helpers/time/parseDate'
import {PRIORITIES} from '@/constants/priorities'
import {REPEAT_TYPES, type IRepeatAfter, type IRepeatType} from '@/types/IRepeatAfter'
const VIKUNJA_PREFIXES: Prefixes = {
label: '*',
project: '+',
priority: '!',
assignee: '@',
}
const TODOIST_PREFIXES: Prefixes = {
label: '@',
project: '#',
priority: '!',
assignee: '+',
}
export enum PrefixMode {
Disabled = 'disabled',
Default = 'vikunja',
Todoist = 'todoist',
}
export const PREFIXES = {
[PrefixMode.Disabled]: undefined,
[PrefixMode.Default]: VIKUNJA_PREFIXES,
[PrefixMode.Todoist]: TODOIST_PREFIXES,
}
interface repeatParsedResult {
textWithoutMatched: string,
repeats: IRepeatAfter | null,
}
export interface ParsedTaskText {
text: string,
date: Date | null,
labels: string[],
project: string | null,
priority: number | null,
assignees: string[],
repeats: IRepeatAfter | null,
}
interface Prefixes {
label: string,
project: string,
priority: string,
assignee: string,
}
/**
* Parses task text for dates, assignees, labels, projects, priorities and returns an object with all found intents.
*
* @param text
*/
export const parseTaskText = (text: string, prefixesMode: PrefixMode = PrefixMode.Default, now: Date = new Date()): ParsedTaskText => {
const result: ParsedTaskText = {
text: text,
date: null,
labels: [],
project: null,
priority: null,
assignees: [],
repeats: null,
}
const prefixes = PREFIXES[prefixesMode]
if (prefixes === undefined) {
return result
}
result.labels = getLabelsFromPrefix(text, prefixesMode) ?? []
result.text = cleanupItemText(result.text, result.labels, prefixes.label)
result.project = getProjectFromPrefix(result.text, prefixesMode)
result.text = result.project !== null ? cleanupItemText(result.text, [result.project], prefixes.project) : result.text
result.priority = getPriority(result.text, prefixes.priority)
result.text = result.priority !== null ? cleanupItemText(result.text, [String(result.priority)], prefixes.priority) : result.text
result.assignees = getItemsFromPrefix(result.text, prefixes.assignee)
const {textWithoutMatched, repeats} = getRepeats(result.text)
result.text = textWithoutMatched
result.repeats = repeats
const {newText, date} = parseDate(result.text, now)
result.text = newText
result.date = date
return cleanupResult(result, prefixes)
}
const getItemsFromPrefix = (text: string, prefix: string): string[] => {
const items: string[] = []
const itemParts = text.split(' ' + prefix)
if (text.startsWith(prefix)) {
const firstItem = text.split(prefix)[1]
itemParts.unshift(firstItem)
}
itemParts.forEach((p, index) => {
// First part contains the rest
if (index < 1) {
return
}
if (p.startsWith(prefix)) {
p = p.substring(1)
}
let itemText
if (p.charAt(0) === '\'') {
itemText = p.split('\'')[1]
} else if (p.charAt(0) === '"') {
itemText = p.split('"')[1]
} else {
// Only until the next space
itemText = p.split(' ')[0]
}
if (itemText !== '') {
items.push(itemText)
}
})
return Array.from(new Set(items))
}
export const getProjectFromPrefix = (text: string, prefixMode: PrefixMode): string | null => {
const projectPrefix = PREFIXES[prefixMode]?.project
if(typeof projectPrefix === 'undefined') {
return null
}
const projects: string[] = getItemsFromPrefix(text, projectPrefix)
return projects.length > 0 ? projects[0] : null
}
export const getLabelsFromPrefix = (text: string, prefixMode: PrefixMode): string[] | null => {
const labelsPrefix = PREFIXES[prefixMode]?.label
if(typeof labelsPrefix === 'undefined') {
return null
}
return getItemsFromPrefix(text, labelsPrefix)
}
const getPriority = (text: string, prefix: string): number | null => {
const ps = getItemsFromPrefix(text, prefix)
if (ps.length === 0) {
return null
}
for (const p of ps) {
for (const pi of Object.values(PRIORITIES)) {
if (pi === parseInt(p)) {
return parseInt(p)
}
}
}
return null
}
const getRepeats = (text: string): repeatParsedResult => {
const regex = /(^| )(((every|each) (([0-9]+|one|two|three|four|five|six|seven|eight|nine|ten) )?(hours?|days?|weeks?|months?|years?))|(annually|biannually|semiannually|biennially|daily|hourly|monthly|weekly|yearly))($| )/ig
const results = regex.exec(text)
if (results === null) {
return {
textWithoutMatched: text,
repeats: null,
}
}
let amount = 1
switch (results[5] ? results[5].trim() : undefined) {
case 'one':
amount = 1
break
case 'two':
amount = 2
break
case 'three':
amount = 3
break
case 'four':
amount = 4
break
case 'five':
amount = 5
break
case 'six':
amount = 6
break
case 'seven':
amount = 7
break
case 'eight':
amount = 8
break
case 'nine':
amount = 9
break
case 'ten':
amount = 10
break
default:
amount = results[5] ? parseInt(results[5]) : 1
}
let type: IRepeatType = REPEAT_TYPES.Hours
switch (results[2]) {
case 'biennially':
type = REPEAT_TYPES.Years
amount = 2
break
case 'biannually':
case 'semiannually':
type = REPEAT_TYPES.Months
amount = 6
break
case 'yearly':
case 'annually':
type = REPEAT_TYPES.Years
break
case 'daily':
type = REPEAT_TYPES.Days
break
case 'hourly':
type = REPEAT_TYPES.Hours
break
case 'monthly':
type = REPEAT_TYPES.Months
break
case 'weekly':
type = REPEAT_TYPES.Weeks
break
default:
switch (results[7]) {
case 'hour':
case 'hours':
type = REPEAT_TYPES.Hours
break
case 'day':
case 'days':
type = REPEAT_TYPES.Days
break
case 'week':
case 'weeks':
type = REPEAT_TYPES.Weeks
break
case 'month':
case 'months':
type = REPEAT_TYPES.Months
break
case 'year':
case 'years':
type = REPEAT_TYPES.Years
break
}
}
let matchedText = results[0]
if(matchedText.endsWith(' ')) {
matchedText = matchedText.substring(0, matchedText.length - 1)
}
return {
textWithoutMatched: text.replace(matchedText, ''),
repeats: {
amount,
type,
},
}
}
const escapeRegExp = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
export const cleanupItemText = (text: string, items: string[], prefix: string): string => {
items.forEach(l => {
if (l === '') {
return
}
const escaped = escapeRegExp(l)
text = text
.replace(new RegExp(`\\${prefix}'${escaped}' `, 'ig'), '')
.replace(new RegExp(`\\${prefix}'${escaped}'`, 'ig'), '')
.replace(new RegExp(`\\${prefix}"${escaped}" `, 'ig'), '')
.replace(new RegExp(`\\${prefix}"${escaped}"`, 'ig'), '')
.replace(new RegExp(`\\${prefix}${escaped} `, 'ig'), '')
.replace(new RegExp(`\\${prefix}${escaped}`, 'ig'), '')
})
return text
}
const cleanupResult = (result: ParsedTaskText, prefixes: Prefixes): ParsedTaskText => {
result.text = cleanupItemText(result.text, result.labels, prefixes.label)
result.text = result.project !== null ? cleanupItemText(result.text, [result.project], prefixes.project) : result.text
result.text = result.priority !== null ? cleanupItemText(result.text, [String(result.priority)], prefixes.priority) : result.text
// Not removing assignees to avoid removing @text where the user does not exist
result.text = result.text.trim()
return result
}

View File

@ -1,6 +1,6 @@
import {calculateDayInterval} from './calculateDayInterval'
import {calculateNearestHours} from './calculateNearestHours'
import {replaceAll} from '../replaceAll'
import {calculateDayInterval} from '@/helpers/time/calculateDayInterval'
import {calculateNearestHours} from '@/helpers/time/calculateNearestHours'
import {replaceAll} from '@/helpers/replaceAll'
export interface dateParseResult {
newText: string,
@ -196,7 +196,7 @@ export const getDateFromText = (text: string, now: Date = new Date()) => {
result = `${month}/${day}/${tmp_year}`
result = !isNaN(new Date(result).getTime()) ? result : `${day}/${month}/${tmp_year}`
result = !isNaN(new Date(result).getTime()) ? result : null
if(result !== null){
foundText = found
break
@ -212,7 +212,7 @@ export const getDateFromText = (text: string, now: Date = new Date()) => {
foundText = results === null ? '' : results[0].trim()
containsYear = false
}
if (result === null) {
return {
foundText,
@ -328,7 +328,7 @@ const getDateFromWeekday = (text: string, date: Date = new Date()): dateFoundRes
const distance: number = (day + 7 - currentDay) % 7
date.setDate(date.getDate() + distance)
// This a space at the end of the found text to not break parsing suffix strings like "at 14:00" in cases where the
// This a space at the end of the found text to not break parsing suffix strings like "at 14:00" in cases where the
// matched string comes with a space at the end (last part of the regex).
let foundText = results[0]
if (foundText.endsWith(' ')) {
@ -357,9 +357,9 @@ const getDayFromText = (text: string, now: Date = new Date()) => {
const day = parseInt(results[0])
date.setDate(day)
// If the parsed day is the 31st (or 29+ and the next month is february) but the next month only has 30 days,
// If the parsed day is the 31st (or 29+ and the next month is february) but the next month only has 30 days,
// setting the day to 31 will "overflow" the date to the next month, but the first.
// This would look like a very weired bug. Now, to prevent that, we check if the day is the same as parsed after
// This would look like a very weired bug. Now, to prevent that, we check if the day is the same as parsed after
// setting it for the first time and set it again if it isn't - that would mean the month overflowed.
while (date < now) {
date.setMonth(date.getMonth() + 1)

View File

@ -0,0 +1,5 @@
export {parseTaskText} from './parseTaskText'
export {PrefixMode, PREFIXES} from './prefixes'
export {getLabelsFromPrefix, getProjectFromPrefix} from './prefixParser'
export {cleanupItemText} from './textCleanup'
export type {ParsedTaskText} from './types'

View File

@ -1,8 +1,9 @@
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
import {ParsedTaskText, parseTaskText, PrefixMode} from './parseTaskText'
import {parseDate} from '../helpers/time/parseDate'
import {calculateDayInterval} from '../helpers/time/calculateDayInterval'
import {parseTaskText, PrefixMode} from '.'
import type {ParsedTaskText} from '.'
import {parseDate} from './dateParser'
import {calculateDayInterval} from '@/helpers/time/calculateDayInterval'
import {PRIORITIES} from '@/constants/priorities'
import {MILLISECONDS_A_DAY} from '@/constants/date'
import type {IRepeatAfter} from '@/types/IRepeatAfter'

View File

@ -0,0 +1,50 @@
import {parseDate} from './dateParser'
import {PREFIXES, PrefixMode} from './prefixes'
import {getItemsFromPrefix, getLabelsFromPrefix, getProjectFromPrefix} from './prefixParser'
import {getPriority} from './priorityParser'
import {getRepeats} from './repeatParser'
import {cleanupItemText, cleanupResult} from './textCleanup'
import type {ParsedTaskText} from './types'
/**
* Parses task text for dates, assignees, labels, projects, priorities and returns an object with all found intents.
*
* @param text
*/
export const parseTaskText = (text: string, prefixesMode: PrefixMode = PrefixMode.Default, now: Date = new Date()): ParsedTaskText => {
const result: ParsedTaskText = {
text: text,
date: null,
labels: [],
project: null,
priority: null,
assignees: [],
repeats: null,
}
const prefixes = PREFIXES[prefixesMode]
if (prefixes === undefined) {
return result
}
result.labels = getLabelsFromPrefix(text, prefixesMode) ?? []
result.text = cleanupItemText(result.text, result.labels, prefixes.label)
result.project = getProjectFromPrefix(result.text, prefixesMode)
result.text = result.project !== null ? cleanupItemText(result.text, [result.project], prefixes.project) : result.text
result.priority = getPriority(result.text, prefixes.priority)
result.text = result.priority !== null ? cleanupItemText(result.text, [String(result.priority)], prefixes.priority) : result.text
result.assignees = getItemsFromPrefix(result.text, prefixes.assignee)
const {textWithoutMatched, repeats} = getRepeats(result.text)
result.text = textWithoutMatched
result.repeats = repeats
const {newText, date} = parseDate(result.text, now)
result.text = newText
result.date = date
return cleanupResult(result, prefixes)
}

View File

@ -0,0 +1,55 @@
import {PREFIXES, PrefixMode} from './prefixes'
export const getItemsFromPrefix = (text: string, prefix: string): string[] => {
const items: string[] = []
const itemParts = text.split(' ' + prefix)
if (text.startsWith(prefix)) {
const firstItem = text.split(prefix)[1]
itemParts.unshift(firstItem)
}
itemParts.forEach((p, index) => {
// First part contains the rest
if (index < 1) {
return
}
if (p.startsWith(prefix)) {
p = p.substring(1)
}
let itemText
if (p.charAt(0) === '\'') {
itemText = p.split('\'')[1]
} else if (p.charAt(0) === '"') {
itemText = p.split('"')[1]
} else {
// Only until the next space
itemText = p.split(' ')[0]
}
if (itemText !== '') {
items.push(itemText)
}
})
return Array.from(new Set(items))
}
export const getProjectFromPrefix = (text: string, prefixMode: PrefixMode): string | null => {
const projectPrefix = PREFIXES[prefixMode]?.project
if(typeof projectPrefix === 'undefined') {
return null
}
const projects: string[] = getItemsFromPrefix(text, projectPrefix)
return projects.length > 0 ? projects[0] : null
}
export const getLabelsFromPrefix = (text: string, prefixMode: PrefixMode): string[] | null => {
const labelsPrefix = PREFIXES[prefixMode]?.label
if(typeof labelsPrefix === 'undefined') {
return null
}
return getItemsFromPrefix(text, labelsPrefix)
}

View File

@ -0,0 +1,27 @@
import type {Prefixes} from './types'
const VIKUNJA_PREFIXES: Prefixes = {
label: '*',
project: '+',
priority: '!',
assignee: '@',
}
const TODOIST_PREFIXES: Prefixes = {
label: '@',
project: '#',
priority: '!',
assignee: '+',
}
export enum PrefixMode {
Disabled = 'disabled',
Default = 'vikunja',
Todoist = 'todoist',
}
export const PREFIXES = {
[PrefixMode.Disabled]: undefined,
[PrefixMode.Default]: VIKUNJA_PREFIXES,
[PrefixMode.Todoist]: TODOIST_PREFIXES,
}

View File

@ -0,0 +1,19 @@
import {PRIORITIES} from '@/constants/priorities'
import {getItemsFromPrefix} from './prefixParser'
export const getPriority = (text: string, prefix: string): number | null => {
const ps = getItemsFromPrefix(text, prefix)
if (ps.length === 0) {
return null
}
for (const p of ps) {
for (const pi of Object.values(PRIORITIES)) {
if (pi === parseInt(p)) {
return parseInt(p)
}
}
}
return null
}

View File

@ -0,0 +1,114 @@
import {REPEAT_TYPES, type IRepeatType} from '@/types/IRepeatAfter'
import type {repeatParsedResult} from './types'
export const getRepeats = (text: string): repeatParsedResult => {
const regex = /(^| )(((every|each) (([0-9]+|one|two|three|four|five|six|seven|eight|nine|ten) )?(hours?|days?|weeks?|months?|years?))|(annually|biannually|semiannually|biennially|daily|hourly|monthly|weekly|yearly))($| )/ig
const results = regex.exec(text)
if (results === null) {
return {
textWithoutMatched: text,
repeats: null,
}
}
let amount = 1
switch (results[5] ? results[5].trim() : undefined) {
case 'one':
amount = 1
break
case 'two':
amount = 2
break
case 'three':
amount = 3
break
case 'four':
amount = 4
break
case 'five':
amount = 5
break
case 'six':
amount = 6
break
case 'seven':
amount = 7
break
case 'eight':
amount = 8
break
case 'nine':
amount = 9
break
case 'ten':
amount = 10
break
default:
amount = results[5] ? parseInt(results[5]) : 1
}
let type: IRepeatType = REPEAT_TYPES.Hours
switch (results[2]) {
case 'biennially':
type = REPEAT_TYPES.Years
amount = 2
break
case 'biannually':
case 'semiannually':
type = REPEAT_TYPES.Months
amount = 6
break
case 'yearly':
case 'annually':
type = REPEAT_TYPES.Years
break
case 'daily':
type = REPEAT_TYPES.Days
break
case 'hourly':
type = REPEAT_TYPES.Hours
break
case 'monthly':
type = REPEAT_TYPES.Months
break
case 'weekly':
type = REPEAT_TYPES.Weeks
break
default:
switch (results[7]) {
case 'hour':
case 'hours':
type = REPEAT_TYPES.Hours
break
case 'day':
case 'days':
type = REPEAT_TYPES.Days
break
case 'week':
case 'weeks':
type = REPEAT_TYPES.Weeks
break
case 'month':
case 'months':
type = REPEAT_TYPES.Months
break
case 'year':
case 'years':
type = REPEAT_TYPES.Years
break
}
}
let matchedText = results[0]
if(matchedText.endsWith(' ')) {
matchedText = matchedText.substring(0, matchedText.length - 1)
}
return {
textWithoutMatched: text.replace(matchedText, ''),
repeats: {
amount,
type,
},
}
}

View File

@ -0,0 +1,30 @@
import type {ParsedTaskText, Prefixes} from './types'
const escapeRegExp = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
export const cleanupItemText = (text: string, items: string[], prefix: string): string => {
items.forEach(l => {
if (l === '') {
return
}
const escaped = escapeRegExp(l)
text = text
.replace(new RegExp(`\\${prefix}'${escaped}' `, 'ig'), '')
.replace(new RegExp(`\\${prefix}'${escaped}'`, 'ig'), '')
.replace(new RegExp(`\\${prefix}"${escaped}" `, 'ig'), '')
.replace(new RegExp(`\\${prefix}"${escaped}"`, 'ig'), '')
.replace(new RegExp(`\\${prefix}${escaped} `, 'ig'), '')
.replace(new RegExp(`\\${prefix}${escaped}`, 'ig'), '')
})
return text
}
export const cleanupResult = (result: ParsedTaskText, prefixes: Prefixes): ParsedTaskText => {
result.text = cleanupItemText(result.text, result.labels, prefixes.label)
result.text = result.project !== null ? cleanupItemText(result.text, [result.project], prefixes.project) : result.text
result.text = result.priority !== null ? cleanupItemText(result.text, [String(result.priority)], prefixes.priority) : result.text
// Not removing assignees to avoid removing @text where the user does not exist
result.text = result.text.trim()
return result
}

View File

@ -0,0 +1,23 @@
import type {IRepeatAfter} from '@/types/IRepeatAfter'
export interface repeatParsedResult {
textWithoutMatched: string,
repeats: IRepeatAfter | null,
}
export interface ParsedTaskText {
text: string,
date: Date | null,
labels: string[],
project: string | null,
priority: number | null,
assignees: string[],
repeats: IRepeatAfter | null,
}
export interface Prefixes {
label: string,
project: string,
priority: string,
assignee: string,
}

View File

@ -1,40 +0,0 @@
import {ref, computed, readonly} from 'vue'
import {defineStore, acceptHMRUpdate} from 'pinia'
import {findIndexById} from '@/helpers/utils'
import type {IAttachment} from '@/modelTypes/IAttachment'
export const useAttachmentStore = defineStore('attachment', () => {
const attachments = ref<IAttachment[]>([])
function set(newAttachments: IAttachment[]) {
console.debug('Set attachments', newAttachments)
attachments.value = newAttachments
}
function add(attachment: IAttachment) {
console.debug('Add attachement', attachment)
attachments.value.push(attachment)
}
function removeById(id: IAttachment['id']) {
const attachmentIndex = findIndexById(attachments.value, id)
attachments.value.splice(attachmentIndex, 1)
console.debug('Remove attachement', id)
}
const hasAttachments = computed(() => attachments.value.length > 0)
return {
attachments: readonly(attachments),
set,
add,
removeById,
hasAttachments,
}
})
// support hot reloading
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useAttachmentStore, import.meta.hot))
}

View File

@ -87,12 +87,13 @@ export const useConfigStore = defineStore('config', () => {
const migratorsEnabled = computed(() => state.availableMigrators?.length > 0)
const apiBase = computed(() => {
const {host, protocol, href} = parseURL(window.API_URL)
const {host, protocol, pathname} = parseURL(window.API_URL)
const cleanHref = href ? (href.endsWith('/')
? href.slice(0, -1)
: href) : ''
return `${protocol}//${host}${cleanHref ? `/${cleanHref}` : ''}`
// Strip the /api/v1 suffix (and optional trailing slash) to get the deployment base.
const basePath = pathname
.replace(/\/api\/v1\/?$/, '')
.replace(/\/+$/, '')
return `${protocol}//${host}${basePath}`
})
function setConfig(config: ConfigState) {

View File

@ -25,7 +25,6 @@ import type {IProject} from '@/modelTypes/IProject'
import {setModuleLoading} from '@/stores/helper'
import {useLabelStore} from '@/stores/labels'
import {useProjectStore} from '@/stores/projects'
import {useAttachmentStore} from '@/stores/attachments'
import {useKanbanStore} from '@/stores/kanban'
import {useBaseStore} from '@/stores/base'
import ProjectUserService from '@/services/projectUsers'
@ -108,7 +107,6 @@ async function findAssignees(parsedTaskAssignees: string[], projectId: number):
export const useTaskStore = defineStore('task', () => {
const baseStore = useBaseStore()
const kanbanStore = useKanbanStore()
const attachmentStore = useAttachmentStore()
const labelStore = useLabelStore()
const projectStore = useProjectStore()
const authStore = useAuthStore()
@ -205,7 +203,6 @@ export const useTaskStore = defineStore('task', () => {
}
kanbanStore.setTaskInBucketByIndex(newTask)
}
attachmentStore.add(attachment)
}
async function addAssignee({

View File

@ -355,6 +355,7 @@
:edit-enabled="canWrite"
:task="task"
@taskChanged="({coverImageAttachmentId}) => task.coverImageAttachmentId = coverImageAttachmentId"
@update:attachments="onAttachmentsUpdated"
/>
</div>
@ -622,7 +623,6 @@
<script lang="ts" setup>
import {ref, reactive, shallowReactive, computed, watch, nextTick, onMounted} from 'vue'
import {useRouter, useRoute, type RouteLocation, onBeforeRouteLeave} from 'vue-router'
import {storeToRefs} from 'pinia'
import {useI18n} from 'vue-i18n'
import {unrefElement, useDebounceFn, useElementSize, useIntersectionObserver, useMutationObserver} from '@vueuse/core'
import {klona} from 'klona/lite'
@ -631,6 +631,7 @@ import TaskService from '@/services/task'
import TaskModel from '@/models/task'
import type {ITask} from '@/modelTypes/ITask'
import type {IAttachment} from '@/modelTypes/IAttachment'
import type {IProject} from '@/modelTypes/IProject'
import {PRIORITIES, type Priority} from '@/constants/priorities'
@ -667,7 +668,6 @@ import {scrollIntoView} from '@/helpers/scrollIntoView'
import {TASK_REPEAT_MODES} from '@/types/IRepeatMode'
import {playPopSound} from '@/helpers/playPop'
import {useAttachmentStore} from '@/stores/attachments'
import {useTaskStore} from '@/stores/tasks'
import {useKanbanStore} from '@/stores/kanban'
import {useProjectStore} from '@/stores/projects'
@ -694,14 +694,13 @@ const route = useRoute()
const {t} = useI18n({useScope: 'global'})
const projectStore = useProjectStore()
const attachmentStore = useAttachmentStore()
const {hasAttachments} = storeToRefs(attachmentStore)
const taskStore = useTaskStore()
const kanbanStore = useKanbanStore()
const authStore = useAuthStore()
const baseStore = useBaseStore()
const task = ref<ITask>(new TaskModel())
const hasAttachments = computed(() => (task.value.attachments?.length ?? 0) > 0)
const taskNotFound = ref(false)
const taskTitle = computed(() => task.value.title)
useTitle(taskTitle)
@ -789,8 +788,20 @@ const color = computed(() => {
const isModal = computed(() => Boolean(props.backdropView))
function attachmentUpload(file: File, onSuccess?: (url: string) => void) {
return uploadFile(props.taskId, file, onSuccess)
async function attachmentUpload(file: File, onSuccess?: (url: string) => void) {
const uploaded = await uploadFile(props.taskId, file, onSuccess)
if (uploaded.length > 0) {
onAttachmentsUpdated([...task.value.attachments, ...uploaded])
}
return uploaded
}
function onAttachmentsUpdated(attachments: IAttachment[]) {
task.value.attachments = attachments
kanbanStore.setTaskInBucket({
...task.value,
attachments,
})
}
const heading = ref<HTMLElement | null>(null)
@ -890,7 +901,6 @@ watch(
try {
const loaded = await taskService.get({id}, {expand: ['reactions', 'comments', 'is_unread']})
Object.assign(task.value, loaded)
attachmentStore.set(task.value.attachments)
taskColor.value = task.value.hexColor
setActiveFields()

View File

@ -41,6 +41,86 @@ const route = useRoute()
const now = new Date()
interface TokenPreset {
id: string
groups: Record<string, string[] | '*'>
}
const presets: TokenPreset[] = [
{
id: 'readOnly',
groups: {
'*': ['read_one', 'read_all'],
},
},
{
id: 'tasks',
groups: {
'tasks': '*',
'tasks_attachments': '*',
'tasks_assignees': '*',
'tasks_labels': '*',
'tasks_comments': '*',
'tasks_relations': '*',
'labels': ['read_one', 'read_all', 'create'],
'projects': ['read_one', 'read_all', 'views_buckets_tasks'],
'projects_views': ['read_one', 'read_all'],
'projects_views_tasks': ['read_one', 'read_all'],
},
},
{
id: 'projects',
groups: {
'projects': '*',
'projects_views': '*',
'projects_teams': '*',
'projects_users': '*',
'projects_shares': '*',
'projects_webhooks': '*',
'projects_buckets': '*',
'projects_views_tasks': '*',
'tasks': ['read_one', 'read_all'],
'teams': ['read_one', 'read_all'],
},
},
{
id: 'fullAccess',
groups: {
'*': '*',
},
},
]
function applyPreset(preset: TokenPreset) {
resetPermissions()
for (const [groupKey, permissions] of Object.entries(preset.groups)) {
if (groupKey === '*') {
// Apply to all groups
for (const group of Object.keys(availableRoutes.value)) {
applyPermissionsToGroup(group, permissions)
}
} else if (availableRoutes.value[groupKey]) {
applyPermissionsToGroup(groupKey, permissions)
}
}
}
function applyPermissionsToGroup(group: string, permissions: string[] | '*') {
if (permissions === '*') {
// Select all permissions in this group
selectPermissionGroup(group, true)
newTokenPermissionsGroup.value[group] = true
} else {
for (const perm of permissions) {
if (newTokenPermissions.value[group]?.[perm] !== undefined) {
newTokenPermissions.value[group][perm] = true
}
}
toggleGroupPermissionsFromChild(group, true)
}
}
const flatPickerConfig = computed(() => ({
altFormat: t('date.altFormatLong'),
altInput: true,
@ -334,6 +414,26 @@ function toggleGroupPermissionsFromChild(group: string, checked: boolean) {
<div class="field">
<label class="label">{{ $t('user.settings.apiTokens.attributes.permissions') }}</label>
<p>{{ $t('user.settings.apiTokens.permissionExplanation') }}</p>
<!-- Presets -->
<div class="preset-buttons mbe-4">
<label class="label">{{ $t('user.settings.apiTokens.presets.title') }}</label>
<div
class="is-flex"
style="gap: .5rem; flex-wrap: wrap;"
>
<XButton
v-for="preset in presets"
:key="preset.id"
variant="secondary"
type="button"
@click="applyPreset(preset)"
>
{{ $t(`user.settings.apiTokens.presets.${preset.id}`) }}
</XButton>
</div>
</div>
<div
v-for="(routes, group) in availableRoutes"
:key="group"

13
go.mod
View File

@ -19,6 +19,7 @@ module code.vikunja.io/api
go 1.25.7
require (
code.dny.dev/ssrf v0.2.0
dario.cat/mergo v1.0.2
github.com/ThreeDotsLabs/watermill v1.5.1
github.com/adlio/trello v1.12.0
@ -28,15 +29,14 @@ require (
github.com/aws/aws-sdk-go-v2/config v1.32.10
github.com/aws/aws-sdk-go-v2/credentials v1.19.10
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2
github.com/aws/smithy-go v1.24.1
github.com/bbrks/go-blurhash v1.1.1
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500
github.com/coreos/go-oidc/v3 v3.17.0
github.com/cweill/gotests v1.9.0
github.com/d4l3k/messagediff v1.2.1
github.com/disintegration/imaging v1.6.2
github.com/dustinkirkland/golang-petname v0.0.0-20240422154211-76c06c4bde6b
github.com/fatih/color v1.18.0
github.com/fclairamb/afero-s3 v0.4.0
github.com/gabriel-vasile/mimetype v1.4.12
github.com/ganigeorgiev/fexpr v0.5.0
github.com/getsentry/sentry-go v0.41.0
@ -66,7 +66,6 @@ require (
github.com/robfig/cron/v3 v3.0.1
github.com/samedi/caldav-go v3.0.0+incompatible
github.com/schollz/progressbar/v3 v3.19.0
github.com/spf13/afero v1.15.0
github.com/spf13/cobra v1.10.2
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
@ -85,14 +84,12 @@ require (
golang.org/x/text v0.33.0
gopkg.in/d4l3k/messagediff.v1 v1.2.1
mvdan.cc/xurls/v2 v2.6.0
src.techknowlogick.com/xgo v1.9.0
src.techknowlogick.com/xormigrate v1.7.1
xorm.io/builder v0.3.13
xorm.io/xorm v1.3.11
)
require (
code.dny.dev/ssrf v0.2.0 // indirect
filippo.io/edwards25519 v1.1.1 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
@ -100,7 +97,6 @@ require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
@ -113,7 +109,6 @@ require (
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect
github.com/aws/smithy-go v1.24.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/beevik/etree v1.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
@ -181,6 +176,7 @@ require (
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sony/gobreaker v1.0.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
@ -203,6 +199,7 @@ require (
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
src.techknowlogick.com/xgo v1.9.0 // indirect
)
tool (
@ -212,5 +209,3 @@ tool (
)
replace github.com/samedi/caldav-go => github.com/kolaente/caldav-go v3.0.1-0.20190610114120-2a4eb8b5dcc9+incompatible // Branch: feature/dynamic-supported-components, PR: https://github.com/samedi/caldav-go/pull/6 and https://github.com/samedi/caldav-go/pull/7
replace github.com/fclairamb/afero-s3 => github.com/maggch97/afero-s3 v0.0.0-20260227031452-4db589f0d189

156
go.sum
View File

@ -1,5 +1,3 @@
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
code.dny.dev/ssrf v0.2.0 h1:wCBP990rQQ1CYfRpW+YK1+8xhwUjv189AQ3WMo1jQaI=
code.dny.dev/ssrf v0.2.0/go.mod h1:B+91l25OnyaLIeCx0WRJN5qfJ/4/ZTZxRXgm0lj/2w8=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
@ -8,42 +6,28 @@ filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw=
filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
gitee.com/travelliu/dm v1.8.11192 h1:aqJT0xhodZjRutIfEXxKYv0CxqmHUHzsbz6SFaRL6OY=
gitee.com/travelliu/dm v1.8.11192/go.mod h1:DHTzyhCrM843x9VdKVbZ+GKXGRbKM2sJ4LxihRxShkE=
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0 h1:lhSJz9RMbJcTgxifR1hUNJnn6CNYtbgEDtQV22/9RBA=
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0 h1:OYa9vmRX2XC5GXRAzeggG12sF/z5D9Ahtdm9EJ00WN4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0=
github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0 h1:v9p9TfTbf7AwNb5NYQt7hI41IfPoLFiFkLtb+bmGjT0=
github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=
github.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=
github.com/adlio/trello v1.12.0 h1:JqOE2GFHQ9YtEviRRRSnicSxPbt4WFOxhqXzjMOw8lw=
github.com/adlio/trello v1.12.0/go.mod h1:I4Lti4jf2KxjTNgTqs5W3lLuE78QZZdYbbPnQQGwjOo=
github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY=
github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
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/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
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/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
@ -58,8 +42,6 @@ github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iK
github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE=
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/s3/manager v1.22.4 h1:s8fbFscel8NLpnz+ggR7ncW+lqhXIkmyHbgbPeT8yyM=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.4/go.mod h1:BazuWe/q/mMJ/NrSJBTbNBJiLq6u8reodbEZ4giRms4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ=
@ -103,8 +85,6 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA=
github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500 h1:6lhrsTEnloDPXyeZBvSYvQf8u86jbKehZPVDDlkgDl4=
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
@ -113,13 +93,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chzyer/logex v1.2.0 h1:+eqR0HfOetur4tgnC8ftU5imRnhi4te+BadWS95c5AM=
github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=
github.com/chzyer/readline v1.5.0 h1:lSwwFrbNviGePhkewF1az4oLmcwqCZijQ2/Wi3BGHAI=
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
github.com/chzyer/test v0.0.0-20210722231415-061457976a23 h1:dZ0/VyGgQdVGAss6Ju0dt5P0QltE0SFY5Woh6hbIfiQ=
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo=
github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
@ -127,18 +102,14 @@ github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfa
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/typeurl/v2 v2.2.0 h1:6NBDbQzr7I5LHgp34xAXYF5DOTQDn05X58lsPEmzLso=
github.com/containerd/typeurl/v2 v2.2.0/go.mod h1:8XOOxnyatxSWuG8OfsZXVnAF4iZfedjS/8UHSPJnX4g=
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-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f h1:JOrtw2xFKzlg+cbHpyrpLDmnN1HqhBfnX7WDiW7eG2c=
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.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
@ -147,8 +118,6 @@ github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7Do
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/cweill/gotests v1.9.0 h1:2B0mA22tbAZemMvOzbRzxehXecRrc6Y2j4GDsmoz23U=
github.com/cweill/gotests v1.9.0/go.mod h1:ec4OTmXWVUEIznSTBJcO5s9df8C+4NGiEaUuVJW1pL0=
github.com/d4l3k/messagediff v1.2.1 h1:ZcAIMYsUg0EAp9X+tt8/enBE/Q8Yd5kzPynLyKptt9U=
github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -163,7 +132,6 @@ github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
@ -190,10 +158,6 @@ github.com/ganigeorgiev/fexpr v0.5.0 h1:XA9JxtTE/Xm+g/JFI6RfZEHSiQlk+1glLvRK1Lpv
github.com/ganigeorgiev/fexpr v0.5.0/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
github.com/getsentry/sentry-go v0.41.0 h1:q/dQZOlEIb4lhxQSjJhQqtRr3vwrJ6Ahe1C9zv+ryRo=
github.com/getsentry/sentry-go v0.41.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
@ -202,11 +166,9 @@ github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxI
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-kit/log v0.1.0 h1:DGJh0Sm43HbOeYDNnVZFl8BvcYVvjD5bqYJvp0REbwQ=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
@ -226,18 +188,10 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
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 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
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/go.mod h1:4/hVAuX2As0/ej3fLuAd+IvoCXV7/h2cj5nInI11uxM=
@ -251,10 +205,7 @@ 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-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
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-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
@ -271,8 +222,6 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
@ -285,11 +234,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@ -303,7 +249,6 @@ github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bP
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hhsnopek/etag v0.0.0-20171206181245-aea95f647346 h1:Odeq5rB6OZSkib5gqTG+EM1iF0bUVjYYd33XB1ULv00=
github.com/hhsnopek/etag v0.0.0-20171206181245-aea95f647346/go.mod h1:4ggHM2qnyyZjenBb7RpwVzIj+JMsu9kHCVxMjB30hGs=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huandu/go-assert v1.1.5 h1:fjemmA7sSfYHJD7CUqs9qTwwfdNAx7/j2/ZlHXzNB3c=
github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U=
@ -313,14 +258,11 @@ github.com/huandu/go-clone/generic v1.7.3 h1:hXbg87J1nWwwHO2vGD5PAnBohQCfltl+fOE
github.com/huandu/go-clone/generic v1.7.3/go.mod h1:GnX7Bo9qvXKm2f3OJybqcbgj6YmA/kuEd/pw5JbsB/I=
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2 h1:rcanfLhLDA8nozr/K289V1zcntHr3V+SHlXwzz1ZI2g=
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
@ -328,17 +270,12 @@ github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsU
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgconn v1.14.0 h1:vrbA9Ud87g6JdFWkHTJXppVce58qPIdP7N8y0Ml/A7Q=
github.com/jackc/pgconn v1.14.0/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
@ -346,33 +283,23 @@ github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvW
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.3.2 h1:7eY55bdBeCz1F2fTzSz69QC+pG46jYq9/jtSPiJ5nn0=
github.com/jackc/pgproto3/v2 v2.3.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw=
github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
github.com/jackc/pgx/v4 v4.18.0 h1:Ltaa1ePvc7msFGALnCrqKJVEByu/qYh5jJBYcDtAno4=
github.com/jackc/pgx/v4 v4.18.0/go.mod h1:FydWkUyadDmdNH/mHnGob881GawxeEm7TcMCzkb+qQE=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0=
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jaswdr/faker/v2 v2.9.1 h1:J0Rjqb2/FquZnoZplzkGVL5LmhNkeIpvsSMoJKzn+8E=
github.com/jaswdr/faker/v2 v2.9.1/go.mod h1:jZq+qzNQr8/P+5fHd9t3txe2GNPnthrTfohtnJ7B+68=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
@ -393,34 +320,22 @@ github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6 h1:SwcnSwBR7X/5EHJQlXBockkJVIMRVt5yKaesBPMtyZQ=
github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6/go.mod h1:WrYiIuiXUMIvTDAQw97C+9l0CnBmCcvosPjN3XDqS/o=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/kolaente/caldav-go v3.0.1-0.20190610114120-2a4eb8b5dcc9+incompatible h1:q7DbyV+sFjEoTuuUdRDNl2nlyfztkZgxVVCV7JhzIkY=
github.com/kolaente/caldav-go v3.0.1-0.20190610114120-2a4eb8b5dcc9+incompatible/go.mod h1:y1UhTNI4g0hVymJrI6yJ5/ohy09hNBeU8iJEZjgdDOw=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@ -433,8 +348,6 @@ github.com/labstack/echo/v5 v5.0.3 h1:Jql8sDtCYXrhh2Mbs6jKwjR6r7X8FSQQmch+w6QS7k
github.com/labstack/echo/v5 v5.0.3/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo=
github.com/laurent22/ical-go v0.1.1-0.20181107184520-7e5d6ade8eef h1:RZnRnSID1skF35j/15KJ6hKZkdIC/teQClJK5wP5LU4=
github.com/laurent22/ical-go v0.1.1-0.20181107184520-7e5d6ade8eef/go.mod h1:4LATl0uhhtytR6p9n1AlktDyIz4u2iUnWEdI3L/hXiw=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@ -446,8 +359,6 @@ github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJV
github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/maggch97/afero-s3 v0.0.0-20260227031452-4db589f0d189 h1:HUcTa93zAphy5hA8akgJtMdTXHodWLBFblyutyvlhek=
github.com/maggch97/afero-s3 v0.0.0-20260227031452-4db589f0d189/go.mod h1:8Shhk3YMlD41DAC5bWRyjQFSsO4RxUKOj99Oxc8/HuU=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
@ -473,8 +384,6 @@ github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K7
github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
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/microsoft/go-mssqldb v1.8.2 h1:236sewazvC8FvG6Dr3bszrVhMkAl4KYImryLkRMCd0I=
github.com/microsoft/go-mssqldb v1.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
@ -486,19 +395,13 @@ github.com/moby/moby/client v0.2.2/go.mod h1:2EkIPVNCqR05CMIzL1mfA07t0HvVUUOl85p
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5 h1:8Q0qkMVC/MmWkpIdlvZgcv2o2jrlF6zqVOh7W5YHdMA=
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
@ -513,8 +416,6 @@ github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 h1:jrYnow5+hy3WRDC
github.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew=
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/ts v0.0.0-20171002115256-78ecb04241c0 h1:LiZB1h0GIcudcDci2bxbqI6DXV8bF8POAnArqvRrIyw=
github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0/go.mod h1:F/7q8/HZz+TXjlsoZQQKVYvXTZaFH4QRa3y+j1p7MS0=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
@ -533,7 +434,6 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 h1:49lOXmGaUpV9Fz3gd7TFZY106KVlPVa5jcYD1gaQf98=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@ -561,39 +461,25 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a h1:w3tdWGKbLGBPtR/8/oO74W6hmz0qE5q0z9aqSAewaaM=
github.com/rogpeppe/go-internal v1.13.2-0.20241226121412-a5dc8ff20d0a/go.mod h1:S8kfXMp+yh77OxPD4fdM6YUknrZpQxLhvxzS4gDHENY=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0 h1:uPRuwkWF4J6fGsJ2R0Gn2jB1EQiav9k3S6CSdygQJXY=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc=
github.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
@ -614,8 +500,6 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@ -638,31 +522,19 @@ github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk=
github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk=
github.com/tkuchiki/go-timezone v0.2.3 h1:D3TVdIPrFsu9lxGxqNX2wsZwn1MZtTqTW0mdevMozHc=
github.com/tkuchiki/go-timezone v0.2.3/go.mod h1:oFweWxYl35C/s7HMVZXiA19Jr9Y0qJHMaG/J2TES4LY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU=
github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/ulule/limiter/v3 v3.11.2 h1:P4yOrxoEMJbOTfRJR2OzjL90oflzYPPmWg+dvwN2tHA=
github.com/ulule/limiter/v3 v3.11.2/go.mod h1:QG5GnFOCV+k7lrL5Y8kgEeeflPH3+Cviqlqa8SVSQxI=
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/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.47.0 h1:y7moDoxYzMooFpT5aHgNgVOQDrS3qlkfiP9mDtGGK9c=
github.com/valyala/fasthttp v1.47.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA=
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/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4 h1:0sw0nJM544SpsihWx1bkXdYLQDlzRflMgFJQ4Yih9ts=
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4/go.mod h1:+ccdNT0xMY1dtc5XBxumbYfOUhmduiGudqaDgD2rVRE=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/zenazn/goji v0.9.0 h1:RSQQAbXGArQ0dIDEq+PI6WqN6if+5KHu6x2Cx/GXLTQ=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
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/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
@ -681,26 +553,20 @@ go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTq
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@ -721,7 +587,6 @@ golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9/go.mod h1:CxIveKay+FTh1D0yPZ
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.35.0 h1:LKjiHdgMtO8z7Fh18nGY6KDcoEtVfsgLDPeLyguqb7I=
golang.org/x/image v0.35.0/go.mod h1:MwPLTVgvxSASsxdLzKrl8BRFuyqMyGhLwmC+TO1Sybk=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
@ -781,18 +646,12 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc h1:bH6xUXay0AIFMElXG2rQ4uiE+7ncwtiOdPfYK1NK2XA=
golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -826,12 +685,7 @@ golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw=
google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@ -849,11 +703,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/d4l3k/messagediff.v1 v1.2.1 h1:70AthpjunwzUiarMHyED52mj9UwtAnE89l1Gmrt3EU0=
gopkg.in/d4l3k/messagediff.v1 v1.2.1/go.mod h1:EUzikiKadqXWcD1AzJLagx0j/BeeWGtn++04Xniyg44=
gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec h1:RlWgLqCMMIYYEVcAR5MDsuHlVkaIPDAF+5Dehzg8L5A=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
@ -873,7 +724,6 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
@ -887,9 +737,7 @@ modernc.org/ccgo/v3 v3.0.0-20220910160915-348f15de615a/go.mod h1:8p47QxPkdugex9J
modernc.org/ccgo/v3 v3.16.13-0.20221017192402-261537637ce8/go.mod h1:fUB3Vn0nVPReA+7IG7yZDfjv1TMWjhQP8gCxrFAtL5g=
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
modernc.org/libc v1.17.4/go.mod h1:WNg2ZH56rDEwdropAJeZPQkXmDwh+JCA1s/htl6r2fA=
modernc.org/libc v1.18.0/go.mod h1:vj6zehR5bfc98ipowQOM2nIDUZnVew/wNC/2tOGS+q0=
@ -914,12 +762,10 @@ modernc.org/sqlite v1.20.4/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/tcl v1.15.0 h1:oY+JeD11qVVSgVvodMJsu7Edf8tr5E/7tuhF5cNYz34=
modernc.org/tcl v1.15.0/go.mod h1:xRoGotBZ6dU+Zo2tca+2EqVEeMmOUBzHnhIwq4YrVnE=
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE=
modernc.org/z v1.7.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ=
mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI=
mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk=
@ -927,8 +773,6 @@ pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
src.techknowlogick.com/xgo v1.8.1-0.20241105013731-313dedef864f h1:Dy7qQ31o3z4EV+0ISDH1IldkPxzubOAvV7DW+9HnbNg=
src.techknowlogick.com/xgo v1.8.1-0.20241105013731-313dedef864f/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU=
src.techknowlogick.com/xgo v1.9.0 h1:IlAoK9uRnvniCjX/Mruydonm3XtAe/dgjSV2kBww370=
src.techknowlogick.com/xgo v1.9.0/go.mod h1:gMKej7rx5ksiGKwmKpVA4keweoAoFQyRC0rAZKO0vlw=
src.techknowlogick.com/xormigrate v1.7.1 h1:RKGLLUAqJ+zO8iZ7eOc7oLH7f0cs2gfXSZSvBRBHnlY=

View File

@ -70,6 +70,8 @@ const (
ServiceEnablePublicTeams Key = `service.enablepublicteams`
ServiceBcryptRounds Key = `service.bcryptrounds`
ServiceEnableOpenIDTeamUserOnlySearch Key = `service.enableopenidteamusersearch`
ServiceIPExtractionMethod Key = `service.ipextractionmethod`
ServiceTrustedProxies Key = `service.trustedproxies`
SentryEnabled Key = `sentry.enabled`
SentryDsn Key = `sentry.dsn`
@ -353,6 +355,8 @@ func InitDefaultConfig() {
ServiceEnablePublicTeams.setDefault(false)
ServiceBcryptRounds.setDefault(11)
ServiceEnableOpenIDTeamUserOnlySearch.setDefault(false)
ServiceIPExtractionMethod.setDefault("direct")
ServiceTrustedProxies.setDefault("")
// Sentry
SentryDsn.setDefault("https://440eedc957d545a795c17bbaf477497c@o1047380.ingest.sentry.io/4504254983634944")

11
pkg/db/fixtures/totp.yml Normal file
View File

@ -0,0 +1,11 @@
- id: 1
user_id: 10
secret: 'JBSWY3DPEHPK3PXP'
enabled: true
url: 'otpauth://totp/Vikunja:user10?secret=JBSWY3DPEHPK3PXP&issuer=Vikunja'
# user1 has TOTP enrolled but not yet enabled — used by existing TOTP enrollment tests
- id: 2
user_id: 1
secret: 'HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ'
enabled: false
url: 'otpauth://totp/Vikunja:user1?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=Vikunja'

View File

@ -22,3 +22,15 @@
token: 'deletiontesttoken'
kind: 3
created: 2021-07-12 00:00:14
-
id: 5
user_id: 17
token: 'disableduserpasswordresettoken'
kind: 1
created: 2024-01-01 00:00:00
-
id: 6
user_id: 10
token: '$2a$04$DT./vBYVmwfc8KlWOyLmNOF4KJLahG31L1eSzg45RRZKPhfjojSDa'
kind: 4
created: 2024-01-01 00:00:00

View File

@ -127,3 +127,12 @@
default_project_id: 37
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
# Disabled user for security tests
- id: 17
username: 'user17'
password: '$2a$04$X4aRMEt0ytgPwMIgv36cI..7X9.nhY/.tYwxpqSi0ykRHx2CwQ0S6' # 12345678
email: 'user17@example.com'
status: 2
issuer: local
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12

View File

@ -112,3 +112,9 @@
permission: 0
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12
- id: 20
user_id: 15
project_id: 35
permission: 0
updated: 2018-12-02 15:13:12
created: 2018-12-01 15:13:12

View File

@ -22,7 +22,6 @@ import (
"errors"
"fmt"
"os"
"path/filepath"
"testing"
"time"
@ -36,22 +35,11 @@ import (
awsconfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
aferos3 "github.com/fclairamb/afero-s3"
"github.com/spf13/afero"
"github.com/stretchr/testify/require"
)
// This file handles storing and retrieving a file for different backends
var fs afero.Fs
var afs *afero.Afero
// S3 client and bucket for direct uploads with Content-Length
type s3PutObjectClient interface {
PutObject(ctx context.Context, input *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error)
}
var s3Client s3PutObjectClient
var s3Bucket string
var storage FileStorage
func setDefaultLocalConfig() {
config.FilesBasePath.Set(config.ResolvePath(config.FilesBasePath.GetString()))
@ -97,23 +85,15 @@ func initS3FileHandler() error {
}
})
// Initialize S3 filesystem using afero-s3
fs = aferos3.NewFsFromClient(bucket, client)
afs = &afero.Afero{Fs: fs}
// Store S3 client and bucket for direct uploads with Content-Length
s3Client = client
s3Bucket = bucket
storage = newS3Storage(bucket, config.FilesBasePath.GetString(), client)
return nil
}
// initLocalFileHandler initializes the local filesystem backend
func initLocalFileHandler() {
fs = afero.NewOsFs()
afs = &afero.Afero{Fs: fs}
s3Client = nil
setDefaultLocalConfig()
storage = newLocalStorage(config.FilesBasePath.GetString())
}
// InitFileHandler creates a new file handler for the file backend we want to use
@ -140,9 +120,8 @@ func InitFileHandler() error {
// InitTestFileHandler initializes a new memory file system for testing
func InitTestFileHandler() {
fs = afero.NewMemMapFs()
afs = &afero.Afero{Fs: fs}
setDefaultLocalConfig()
storage = newMemStorage()
}
func initFixtures(t *testing.T) {
@ -157,7 +136,7 @@ func initFixtures(t *testing.T) {
// InitTestFileFixtures initializes file fixtures
func InitTestFileFixtures(t *testing.T) {
testfile := &File{ID: 1}
err := afero.WriteFile(afs, testfile.getAbsoluteFilePath(), []byte("testfile1"), 0644)
err := storage.Write(testfile.fileID(), bytes.NewReader([]byte("testfile1")), 9)
require.NoError(t, err)
}
@ -184,9 +163,9 @@ func InitTests() {
keyvalue.InitStorage()
}
// FileStat stats a file. This is an exported function to be able to test this from outide of the package
// FileStat stats a file. This is an exported function to be able to test this from outside of the package
func FileStat(file *File) (os.FileInfo, error) {
return afs.Stat(file.getAbsoluteFilePath())
return storage.Stat(file.fileID())
}
// ValidateFileStorage checks that the configured file storage is writable
@ -201,36 +180,31 @@ func ValidateFileStorage() error {
// For local filesystem, ensure the base directory exists
if config.FilesType.GetString() == "local" {
// Check if directory exists
info, err := afs.Stat(basePath)
info, err := os.Stat(basePath)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
// Error other than "file doesn't exist"
return fmt.Errorf("failed to access file storage directory at %s: %w%s", basePath, err, diag)
}
// Directory doesn't exist, try to create it
err = afs.MkdirAll(basePath, 0755)
err = os.MkdirAll(basePath, 0755)
if err != nil {
return fmt.Errorf("failed to create file storage directory at %s: %w%s", basePath, err, diag)
}
} else if !info.IsDir() {
// Path exists but is not a directory
return fmt.Errorf("file storage path exists but is not a directory: %s", basePath)
}
}
filename := fmt.Sprintf(".vikunja-check-%d", time.Now().UnixNano())
path := filepath.Join(basePath, filename)
err := writeToStorage(path, bytes.NewReader([]byte{}), 0)
err := storage.Write(filename, bytes.NewReader([]byte{}), 0)
if err != nil {
return fmt.Errorf("failed to create test file at %s: %w%s", path, err, diag)
return fmt.Errorf("failed to create test file: %w%s", err, diag)
}
err = afs.Remove(path)
err = storage.Remove(filename)
if err != nil {
return fmt.Errorf("failed to remove test file at %s: %w", path, err)
return fmt.Errorf("failed to remove test file: %w", err)
}
return nil

View File

@ -17,13 +17,10 @@
package files
import (
"context"
"errors"
"fmt"
"io"
"math"
"os"
"path/filepath"
"strconv"
"time"
@ -34,11 +31,8 @@ import (
"code.vikunja.io/api/pkg/modules/keyvalue"
"code.vikunja.io/api/pkg/web"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/c2h5oh/datasize"
"github.com/gabriel-vasile/mimetype"
"github.com/spf13/afero"
"xorm.io/xorm"
)
@ -52,7 +46,7 @@ type File struct {
Created time.Time `xorm:"created" json:"created"`
CreatedByID int64 `xorm:"bigint not null" json:"-"`
File afero.File `xorm:"-" json:"-"`
File io.ReadCloser `xorm:"-" json:"-"`
// This ReadCloser is only used for migration purposes. Use with care!
// There is currentlc no better way of doing this.
FileContent []byte `xorm:"-" json:"-"`
@ -63,16 +57,13 @@ func (*File) TableName() string {
return "files"
}
func (f *File) getAbsoluteFilePath() string {
return filepath.Join(
config.FilesBasePath.GetString(),
strconv.FormatInt(f.ID, 10),
)
func (f *File) fileID() string {
return strconv.FormatInt(f.ID, 10)
}
// LoadFileByID returns a file by its ID
func (f *File) LoadFileByID() (err error) {
f.File, err = afs.Open(f.getAbsoluteFilePath())
f.File, err = storage.Open(f.fileID())
return
}
@ -157,7 +148,7 @@ func (f *File) Delete(s *xorm.Session) (err error) {
return ErrFileDoesNotExist{FileID: f.ID}
}
err = afs.Remove(f.getAbsoluteFilePath())
err = storage.Remove(f.fileID())
if err != nil {
var perr *os.PathError
if errors.As(err, &perr) {
@ -172,55 +163,11 @@ func (f *File) Delete(s *xorm.Session) (err error) {
return keyvalue.DecrBy(metrics.FilesCountKey, 1)
}
// writeToStorage writes content to the given path, handling both local and S3 backends.
func writeToStorage(path string, content io.ReadSeeker, size uint64) error {
if s3Client == nil {
if _, err := content.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("failed to seek to start of content: %w", err)
}
return afs.WriteReader(path, content)
}
contentLength, err := contentLengthFromReadSeeker(content, size)
if err != nil {
return fmt.Errorf("failed to determine S3 upload content length: %w", err)
}
if _, err = content.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("failed to seek to start before S3 upload: %w", err)
}
_, err = s3Client.PutObject(context.Background(), &s3.PutObjectInput{
Bucket: aws.String(s3Bucket),
Key: aws.String(path),
Body: content,
ContentLength: aws.Int64(contentLength),
})
if err != nil {
return fmt.Errorf("failed to upload file to S3: %w", err)
}
return nil
}
// Save saves a file to storage
func (f *File) Save(fcontent io.ReadSeeker) error {
err := writeToStorage(f.getAbsoluteFilePath(), fcontent, f.Size)
err := storage.Write(f.fileID(), fcontent, f.Size)
if err != nil {
return fmt.Errorf("failed to save file: %w", err)
}
return keyvalue.IncrBy(metrics.FilesCountKey, 1)
}
// contentLengthFromReadSeeker determines the content length by seeking to the end.
func contentLengthFromReadSeeker(seeker io.ReadSeeker, expectedSize uint64) (int64, error) {
endOffset, err := seeker.Seek(0, io.SeekEnd)
if err != nil {
return 0, err
}
if expectedSize > 0 && expectedSize <= uint64(math.MaxInt64) && endOffset != int64(expectedSize) {
log.Warningf("File size mismatch for S3 upload: expected %d bytes but reader reports %d bytes", expectedSize, endOffset)
}
return endOffset, nil
}

View File

@ -20,6 +20,7 @@ import (
"bytes"
"image"
"image/png"
"io"
"os"
"testing"
@ -130,6 +131,30 @@ func TestFile_LoadFileByID(t *testing.T) {
})
}
func TestFileSave_UsesStorage(t *testing.T) {
originalStorage := storage
t.Cleanup(func() {
storage = originalStorage
})
mem := newMemStorage()
storage = mem
content := []byte("test-content")
file := &File{ID: 123, Size: uint64(len(content))}
err := file.Save(bytes.NewReader(content))
require.NoError(t, err)
rc, err := mem.Open(file.fileID())
require.NoError(t, err)
defer rc.Close()
written, err := io.ReadAll(rc)
require.NoError(t, err)
assert.Equal(t, content, written)
}
func TestFile_LoadFileMetaByID(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
initFixtures(t)

View File

@ -54,7 +54,7 @@ func RepairFileMimeTypes(s *xorm.Session, dryRun bool) (*RepairMimeTypesResult,
bar := progressbar.Default(int64(len(files)), "Detecting MIME types")
for _, f := range files {
file, err := afs.Open(f.getAbsoluteFilePath())
file, err := storage.Open(f.fileID())
if err != nil {
msg := fmt.Sprintf("file %d: failed to open: %s", f.ID, err)
log.Errorf("file %d: failed to open: %s", f.ID, err)

View File

@ -18,15 +18,12 @@ package files
import (
"bytes"
"context"
"errors"
"io"
"os"
"testing"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/db"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -49,7 +46,7 @@ func TestFileStorageIntegration(t *testing.T) {
t.Run("Initialize file handler with s3", func(t *testing.T) {
err := InitFileHandler()
require.NoError(t, err, "Failed to initialize file handler with type: s3")
assert.NotNil(t, afs, "File system should be initialized")
assert.NotNil(t, storage, "File storage should be initialized")
})
t.Run("Create and retrieve file with s3", func(t *testing.T) {
@ -304,88 +301,6 @@ func TestInitFileHandler_LocalFilesystem(t *testing.T) {
err := InitFileHandler()
require.NoError(t, err)
// Verify that afs is initialized
assert.NotNil(t, afs)
}
type fakeS3PutObjectClient struct {
lastInput *s3.PutObjectInput
err error
}
func (f *fakeS3PutObjectClient) PutObject(_ context.Context, input *s3.PutObjectInput, _ ...func(*s3.Options)) (*s3.PutObjectOutput, error) {
f.lastInput = input
if f.err != nil {
return nil, f.err
}
return &s3.PutObjectOutput{}, nil
}
func TestFileSave_S3_UsesSeekableReader(t *testing.T) {
originalClient := s3Client
originalBucket := s3Bucket
t.Cleanup(func() {
s3Client = originalClient
s3Bucket = originalBucket
})
client := &fakeS3PutObjectClient{}
s3Client = client
s3Bucket = "test-bucket"
content := []byte("seekable-content")
file := &File{ID: 123, Size: uint64(len(content))}
err := file.Save(bytes.NewReader(content))
require.NoError(t, err)
require.NotNil(t, client.lastInput)
assert.Equal(t, "test-bucket", *client.lastInput.Bucket)
assert.Equal(t, file.getAbsoluteFilePath(), *client.lastInput.Key)
require.NotNil(t, client.lastInput.ContentLength)
assert.Equal(t, int64(len(content)), *client.lastInput.ContentLength)
assert.IsType(t, &bytes.Reader{}, client.lastInput.Body)
}
func TestFileSave_S3_ReturnsErrorOnPutObjectFailure(t *testing.T) {
originalClient := s3Client
originalBucket := s3Bucket
t.Cleanup(func() {
s3Client = originalClient
s3Bucket = originalBucket
})
client := &fakeS3PutObjectClient{err: errors.New("boom")}
s3Client = client
s3Bucket = "test-bucket"
content := []byte("test-content")
file := &File{ID: 789, Size: uint64(len(content))}
err := file.Save(bytes.NewReader(content))
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to upload file to S3")
}
func TestFileSave_S3_UsesActualReaderSizeOnMismatch(t *testing.T) {
originalClient := s3Client
originalBucket := s3Bucket
t.Cleanup(func() {
s3Client = originalClient
s3Bucket = originalBucket
})
client := &fakeS3PutObjectClient{}
s3Client = client
s3Bucket = "test-bucket"
content := []byte("mismatch-content")
file := &File{ID: 999, Size: uint64(len(content) + 10)}
err := file.Save(bytes.NewReader(content))
require.NoError(t, err)
require.NotNil(t, client.lastInput)
require.NotNil(t, client.lastInput.ContentLength)
assert.Equal(t, int64(len(content)), *client.lastInput.ContentLength)
// Verify that storage is initialized
assert.NotNil(t, storage)
}

31
pkg/files/storage.go Normal file
View File

@ -0,0 +1,31 @@
// 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 files
import (
"io"
"os"
)
// FileStorage abstracts file storage operations across local, S3, and in-memory backends.
type FileStorage interface {
Open(path string) (io.ReadCloser, error)
Write(path string, content io.ReadSeeker, size uint64) error
Stat(path string) (os.FileInfo, error)
Remove(path string) error
MkdirAll(path string, perm os.FileMode) error
}

View File

@ -0,0 +1,68 @@
// 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 files
import (
"io"
"os"
"path/filepath"
)
// localStorage implements FileStorage using the OS filesystem.
// All paths are resolved relative to basePath.
type localStorage struct {
basePath string
}
func newLocalStorage(basePath string) *localStorage {
return &localStorage{basePath: basePath}
}
func (l *localStorage) path(name string) string {
return filepath.Join(l.basePath, name)
}
func (l *localStorage) Open(name string) (io.ReadCloser, error) {
return os.Open(l.path(name))
}
func (l *localStorage) Write(name string, content io.ReadSeeker, _ uint64) error {
if _, err := content.Seek(0, io.SeekStart); err != nil {
return err
}
f, err := os.Create(l.path(name))
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, content)
return err
}
func (l *localStorage) Stat(name string) (os.FileInfo, error) {
return os.Stat(l.path(name))
}
func (l *localStorage) Remove(name string) error {
return os.Remove(l.path(name))
}
func (l *localStorage) MkdirAll(p string, perm os.FileMode) error {
return os.MkdirAll(l.path(p), perm)
}

View File

@ -0,0 +1,89 @@
// 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 files
import (
"bytes"
"io"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestLocalStorage_WriteAndOpen(t *testing.T) {
dir := t.TempDir()
s := newLocalStorage(dir)
content := []byte("hello local")
err := s.Write("testfile", bytes.NewReader(content), uint64(len(content)))
require.NoError(t, err)
rc, err := s.Open("testfile")
require.NoError(t, err)
defer rc.Close()
got, err := io.ReadAll(rc)
require.NoError(t, err)
assert.Equal(t, content, got)
}
func TestLocalStorage_Stat(t *testing.T) {
dir := t.TempDir()
s := newLocalStorage(dir)
content := []byte("stat me")
err := s.Write("statfile", bytes.NewReader(content), uint64(len(content)))
require.NoError(t, err)
info, err := s.Stat("statfile")
require.NoError(t, err)
assert.Equal(t, int64(len(content)), info.Size())
}
func TestLocalStorage_Remove(t *testing.T) {
dir := t.TempDir()
s := newLocalStorage(dir)
content := []byte("remove me")
err := s.Write("removefile", bytes.NewReader(content), uint64(len(content)))
require.NoError(t, err)
err = s.Remove("removefile")
require.NoError(t, err)
_, err = s.Stat("removefile")
require.Error(t, err)
assert.True(t, os.IsNotExist(err))
}
func TestLocalStorage_MkdirAll(t *testing.T) {
dir := t.TempDir()
s := newLocalStorage(dir)
err := s.MkdirAll(filepath.Join("a", "b", "c"), 0755)
require.NoError(t, err)
info, err := os.Stat(filepath.Join(dir, "a", "b", "c"))
require.NoError(t, err)
assert.True(t, info.IsDir())
}

103
pkg/files/storage_mem.go Normal file
View File

@ -0,0 +1,103 @@
// 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 files
import (
"bytes"
"io"
"os"
"path"
"sync"
"time"
)
// memStorage is an in-memory FileStorage for tests.
type memStorage struct {
mu sync.RWMutex
files map[string][]byte
}
func newMemStorage() *memStorage {
return &memStorage{files: make(map[string][]byte)}
}
func (m *memStorage) Open(name string) (io.ReadCloser, error) {
m.mu.RLock()
defer m.mu.RUnlock()
data, ok := m.files[name]
if !ok {
return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrNotExist}
}
return io.NopCloser(bytes.NewReader(data)), nil
}
func (m *memStorage) Write(name string, content io.ReadSeeker, _ uint64) error {
if _, err := content.Seek(0, io.SeekStart); err != nil {
return err
}
data, err := io.ReadAll(content)
if err != nil {
return err
}
m.mu.Lock()
defer m.mu.Unlock()
m.files[name] = data
return nil
}
func (m *memStorage) Stat(name string) (os.FileInfo, error) {
m.mu.RLock()
defer m.mu.RUnlock()
data, ok := m.files[name]
if !ok {
return nil, &os.PathError{Op: "stat", Path: name, Err: os.ErrNotExist}
}
return &memFileInfo{
name: path.Base(name),
size: int64(len(data)),
}, nil
}
func (m *memStorage) Remove(name string) error {
m.mu.Lock()
defer m.mu.Unlock()
if _, ok := m.files[name]; !ok {
return &os.PathError{Op: "remove", Path: name, Err: os.ErrNotExist}
}
delete(m.files, name)
return nil
}
func (*memStorage) MkdirAll(string, os.FileMode) error {
return nil
}
type memFileInfo struct {
name string
size int64
}
func (fi *memFileInfo) Name() string { return fi.name }
func (fi *memFileInfo) Size() int64 { return fi.size }
func (fi *memFileInfo) Mode() os.FileMode { return 0644 }
func (fi *memFileInfo) ModTime() time.Time { return time.Time{} }
func (fi *memFileInfo) IsDir() bool { return false }
func (fi *memFileInfo) Sys() interface{} { return nil }

View File

@ -0,0 +1,103 @@
// 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 files
import (
"bytes"
"io"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMemStorage_WriteAndOpen(t *testing.T) {
s := newMemStorage()
content := []byte("hello world")
err := s.Write("test/file.txt", bytes.NewReader(content), uint64(len(content)))
require.NoError(t, err)
rc, err := s.Open("test/file.txt")
require.NoError(t, err)
defer rc.Close()
got, err := io.ReadAll(rc)
require.NoError(t, err)
assert.Equal(t, content, got)
}
func TestMemStorage_Stat(t *testing.T) {
s := newMemStorage()
content := []byte("hello world")
err := s.Write("test/file.txt", bytes.NewReader(content), uint64(len(content)))
require.NoError(t, err)
info, err := s.Stat("test/file.txt")
require.NoError(t, err)
assert.Equal(t, "file.txt", info.Name())
assert.Equal(t, int64(len(content)), info.Size())
assert.False(t, info.IsDir())
}
func TestMemStorage_StatNotFound(t *testing.T) {
s := newMemStorage()
_, err := s.Stat("nonexistent")
require.Error(t, err)
assert.True(t, os.IsNotExist(err))
}
func TestMemStorage_Remove(t *testing.T) {
s := newMemStorage()
content := []byte("hello")
err := s.Write("test/file.txt", bytes.NewReader(content), uint64(len(content)))
require.NoError(t, err)
err = s.Remove("test/file.txt")
require.NoError(t, err)
_, err = s.Open("test/file.txt")
require.Error(t, err)
assert.True(t, os.IsNotExist(err))
}
func TestMemStorage_OpenNotFound(t *testing.T) {
s := newMemStorage()
_, err := s.Open("nonexistent")
require.Error(t, err)
assert.True(t, os.IsNotExist(err))
}
func TestMemStorage_RemoveNotFound(t *testing.T) {
s := newMemStorage()
err := s.Remove("nonexistent")
require.Error(t, err)
assert.True(t, os.IsNotExist(err))
}
func TestMemStorage_MkdirAll(t *testing.T) {
s := newMemStorage()
// Should be a no-op, no error
err := s.MkdirAll("/some/path", 0755)
require.NoError(t, err)
}

165
pkg/files/storage_s3.go Normal file
View File

@ -0,0 +1,165 @@
// 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 files
import (
"context"
"errors"
"fmt"
"io"
"os"
"path"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
smithyhttp "github.com/aws/smithy-go/transport/http"
"code.vikunja.io/api/pkg/log"
)
// s3Storage implements FileStorage backed by S3.
// All paths are prefixed with basePath to form S3 object keys.
type s3Storage struct {
client *s3.Client
bucket string
basePath string
}
func newS3Storage(bucket, basePath string, client *s3.Client) *s3Storage {
return &s3Storage{bucket: bucket, basePath: basePath, client: client}
}
func (s *s3Storage) key(name string) string {
return path.Join(s.basePath, name)
}
func (s *s3Storage) Open(name string) (io.ReadCloser, error) {
key := s.key(name)
out, err := s.client.GetObject(context.Background(), &s3.GetObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(key),
})
if err != nil {
return nil, s3ToPathError("open", name, err)
}
return out.Body, nil
}
func (s *s3Storage) Write(name string, content io.ReadSeeker, size uint64) error {
contentLength, err := contentLengthFromReadSeeker(content, size)
if err != nil {
return fmt.Errorf("failed to determine S3 upload content length: %w", err)
}
if _, err = content.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("failed to seek to start before S3 upload: %w", err)
}
_, err = s.client.PutObject(context.Background(), &s3.PutObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(s.key(name)),
Body: content,
ContentLength: aws.Int64(contentLength),
})
if err != nil {
return fmt.Errorf("failed to upload file to S3: %w", err)
}
return nil
}
func (s *s3Storage) Stat(name string) (os.FileInfo, error) {
key := s.key(name)
head, err := s.client.HeadObject(context.Background(), &s3.HeadObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(key),
})
if err != nil {
return nil, s3ToPathError("stat", name, err)
}
var size int64
if head.ContentLength != nil {
size = *head.ContentLength
}
var modTime time.Time
if head.LastModified != nil {
modTime = *head.LastModified
}
return &s3FileInfo{
name: path.Base(name),
size: size,
modTime: modTime,
}, nil
}
func (s *s3Storage) Remove(name string) error {
// Check existence first for proper error on missing files
if _, err := s.Stat(name); err != nil {
return err
}
_, err := s.client.DeleteObject(context.Background(), &s3.DeleteObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(s.key(name)),
})
return err
}
func (*s3Storage) MkdirAll(string, os.FileMode) error {
return nil // S3 has no directories
}
// s3ToPathError converts S3 SDK errors into os-compatible path errors.
func s3ToPathError(op, name string, err error) error {
var respErr *smithyhttp.ResponseError
if errors.As(err, &respErr) && respErr.HTTPStatusCode() == 404 {
return &os.PathError{Op: op, Path: name, Err: os.ErrNotExist}
}
return &os.PathError{Op: op, Path: name, Err: err}
}
// s3FileInfo implements os.FileInfo for S3 objects.
type s3FileInfo struct {
name string
size int64
modTime time.Time
}
func (fi *s3FileInfo) Name() string { return fi.name }
func (fi *s3FileInfo) Size() int64 { return fi.size }
func (fi *s3FileInfo) Mode() os.FileMode { return 0664 }
func (fi *s3FileInfo) ModTime() time.Time { return fi.modTime }
func (fi *s3FileInfo) IsDir() bool { return false }
func (fi *s3FileInfo) Sys() interface{} { return nil }
// contentLengthFromReadSeeker determines the content length by seeking to the end.
func contentLengthFromReadSeeker(seeker io.ReadSeeker, expectedSize uint64) (int64, error) {
endOffset, err := seeker.Seek(0, io.SeekEnd)
if err != nil {
return 0, err
}
if expectedSize > 0 && expectedSize <= uint64(maxInt64) && endOffset != int64(expectedSize) {
log.Warningf("File size mismatch for S3 upload: expected %d bytes but reader reports %d bytes", expectedSize, endOffset)
}
return endOffset, nil
}
const maxInt64 = 1<<63 - 1

View File

@ -71,6 +71,8 @@
"message": "Это напоминание о задаче «%[1]s» (%[2]s)."
},
"comment": {
"subject": "Re: %[1]s (%[2]s)",
"mentioned_subject": "%[1]s упомянул вас в комментарии в «%[2]s» (%[3]s)",
"mentioned_message": "Пользователь **%[1]s** упомянул вас в комментарии:"
},
"assigned": {
@ -86,6 +88,8 @@
"message": "Пользователь %[1]s удалил задачу «%[2]s» (%[3]s)"
},
"mentioned": {
"subject_new": "%[1]s упомянул вас в новой задаче «%[2]s» (%[3]s)",
"subject": "%[1]s упомянул вас в задаче «%[2]s» (%[3]s)",
"message": "Пользователь **%[1]s** упомянул вас в задаче:"
},
"overdue": {
@ -132,6 +136,7 @@
"have_nice_day": "Хорошего дня!",
"copy_url": "Если ссылка выше не работает, скопируйте и вставьте в адресную строку ссылку отсюда:",
"actions": {
"open_task": "Открыть задачу в Vikunja",
"open_vikunja": "Открыть Vikunja",
"open_project": "Открыть проект",
"open_team": "Открыть команду",
@ -141,7 +146,14 @@
"confirm_email": "Подтвердить email",
"abort_deletion": "Отменить удаление",
"confirm_account_deletion": "Подтвердить удаление аккаунта",
"change_notification_settings_link": "Вы можете изменить настройки уведомлений [здесь](%[1]s)."
"change_notification_settings_link": "Вы можете изменить настройки уведомлений [здесь](%[1]s).",
"left_comment": "Пользователь %[1]s оставил комментарий",
"mentioned_you_comment": "Пользователь %[1]s упомянул вас в комментарии",
"mentioned_you": "Пользователь %[1]s упомянул вас",
"mentioned_you_new_task": "Пользователь %[1]s упомянул вас в новой задаче",
"assigned_you": "Пользователь %[1]s назначил вас",
"assigned_themselves": "Пользователь %[1]s назначил себя",
"assigned_user": "Пользователь %[1]s назначил %[2]s"
}
}
},

View File

@ -17,6 +17,9 @@
package models
import (
"bytes"
"io"
"code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/utils"
@ -301,7 +304,12 @@ func duplicateProjectBackground(s *xorm.Session, pd *ProjectDuplicate, doer web.
}
defer f.File.Close()
file, err := files.CreateWithSession(s, f.File, f.Name, f.Size, doer)
buf, err := io.ReadAll(f.File)
if err != nil {
return err
}
file, err := files.CreateWithSession(s, bytes.NewReader(buf), f.Name, f.Size, doer)
if err != nil {
return err
}
@ -388,15 +396,19 @@ func duplicateTasks(s *xorm.Session, doer web.Auth, ld *ProjectDuplicate) (newTa
return nil, err
}
err := attachment.NewAttachment(s, attachment.File.File, attachment.File.Name, attachment.File.Size, doer)
buf, err := io.ReadAll(attachment.File.File)
if err != nil {
return nil, err
}
if attachment.File.File != nil {
_ = attachment.File.File.Close()
}
err = attachment.NewAttachment(s, bytes.NewReader(buf), attachment.File.Name, attachment.File.Size, doer)
if err != nil {
return nil, err
}
log.Debugf("Duplicated attachment %d into %d from project %d into %d", oldAttachmentID, attachment.ID, ld.ProjectID, ld.Project.ID)
}

View File

@ -76,6 +76,7 @@ func SetupTests() {
"task_buckets",
"sessions",
"webhooks",
"totp",
)
if err != nil {
log.Fatal(err)

View File

@ -18,6 +18,7 @@ package models
import (
"bytes"
"fmt"
"image"
"image/png"
"io"
@ -221,7 +222,25 @@ func (ta *TaskAttachment) GetPreview(previewSize PreviewSize) []byte {
cacheKey := cacheKeyForTaskAttachmentPreview(ta.ID, previewSize)
result, err := keyvalue.Remember(cacheKey, func() (any, error) {
img, _, err := image.Decode(ta.File.File)
// Read all bytes up front so we can inspect dimensions without seeking.
// The file is an io.ReadCloser (no Seek), so we buffer it once.
data, err := io.ReadAll(ta.File.File)
if err != nil {
return nil, err
}
// Check image dimensions before full decode to prevent DoS
// from decompression bombs (small file, huge pixel dimensions)
const maxPixels = 50_000_000 // 50 megapixels
cfg, _, err := image.DecodeConfig(bytes.NewReader(data))
if err != nil {
return nil, err
}
if cfg.Width*cfg.Height > maxPixels {
return nil, fmt.Errorf("image dimensions %dx%d exceed maximum of %d pixels", cfg.Width, cfg.Height, maxPixels)
}
img, _, err := image.Decode(bytes.NewReader(data))
if err != nil {
return nil, err
}

View File

@ -18,11 +18,12 @@ package models
import (
"bytes"
"image"
"image/png"
"io"
"os"
"path/filepath"
"testing"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/user"
@ -51,7 +52,12 @@ func TestTaskAttachment_ReadOne(t *testing.T) {
// Load the actual attachment file and check its content
err = ta.File.LoadFileByID()
require.NoError(t, err)
assert.Equal(t, filepath.Join(config.ServiceRootpath.GetString(), "files", "1"), ta.File.File.Name())
// Validate the file exists at the expected storage path
stat, err := files.FileStat(ta.File)
require.NoError(t, err)
assert.NotNil(t, stat)
content := make([]byte, 9)
read, err := ta.File.File.Read(content)
require.NoError(t, err)
@ -181,6 +187,25 @@ func TestTaskAttachment_Delete(t *testing.T) {
})
}
func TestAttachmentPreviewRejectsLargeImages(t *testing.T) {
// Create a 10000x10000 pixel image (100M pixels, well above the 50M limit)
// As a PNG this is small on disk but huge when decoded into memory
img := image.NewNRGBA(image.Rect(0, 0, 10000, 10000))
var buf bytes.Buffer
err := png.Encode(&buf, img)
require.NoError(t, err)
attachment := &TaskAttachment{
ID: 999999,
File: &files.File{
File: io.NopCloser(bytes.NewReader(buf.Bytes())),
},
}
result := attachment.GetPreview(PreviewMedium)
assert.Nil(t, result, "Preview should be nil for images exceeding max pixel count")
}
func TestTaskAttachment_Permissions(t *testing.T) {
u := &user.User{ID: 1}
t.Run("Can Read", func(t *testing.T) {

View File

@ -194,10 +194,17 @@ func (tc *TaskComment) Update(s *xorm.Session, _ web.Auth) error {
}
func getTaskCommentSimple(s *xorm.Session, tc *TaskComment) error {
exists, err := s.
query := s.
Where("id = ?", tc.ID).
NoAutoCondition().
Get(tc)
NoAutoCondition()
// When TaskID is provided (e.g. from URL parameters), verify the comment
// belongs to that task to prevent IDOR attacks.
if tc.TaskID != 0 {
query = query.And("task_id = ?", tc.TaskID)
}
exists, err := query.Get(tc)
if err != nil {
return err
}

View File

@ -17,6 +17,9 @@
package models
import (
"bytes"
"io"
"code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/web"
@ -131,7 +134,11 @@ func (td *TaskDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
sourceFile := attachment.File.File
defer sourceFile.Close()
err := attachment.NewAttachment(s, sourceFile, attachment.File.Name, attachment.File.Size, doer)
buf, err := io.ReadAll(sourceFile)
if err != nil {
return err
}
err = attachment.NewAttachment(s, bytes.NewReader(buf), attachment.File.Name, attachment.File.Size, doer)
if err != nil {
return err
}

View File

@ -58,7 +58,7 @@ func TestListUsers(t *testing.T) {
all, err := user.ListAllUsers(s)
require.NoError(t, err)
assert.Len(t, all, 16)
assert.Len(t, all, 17)
})
t.Run("no search term", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
@ -171,7 +171,7 @@ func TestListUsers(t *testing.T) {
MatchFuzzily: true,
})
require.NoError(t, err)
assert.Len(t, all, 16)
assert.Len(t, all, 17)
})
// External team discoverability bypass tests

View File

@ -328,6 +328,32 @@ func checkProjectBackgroundRights(s *xorm.Session, c *echo.Context) (project *mo
return
}
func checkProjectBackgroundWritePermissions(s *xorm.Session, c *echo.Context) (project *models.Project, auth web.Auth, err error) {
auth, err = auth2.GetAuthFromClaims(c)
if err != nil {
return nil, auth, echo.NewHTTPError(http.StatusBadRequest, "Invalid auth token: "+err.Error()).Wrap(err)
}
projectID, err := strconv.ParseInt(c.Param("project"), 10, 64)
if err != nil {
return nil, auth, echo.NewHTTPError(http.StatusBadRequest, "Invalid project ID: "+err.Error()).Wrap(err)
}
project = &models.Project{ID: projectID}
can, err := project.CanUpdate(s, auth)
if err != nil {
_ = s.Rollback()
return nil, auth, err
}
if !can {
_ = s.Rollback()
log.Infof("Tried to modify project background of project %d while not having the permissions for it (User: %v)", projectID, auth)
return nil, auth, echo.NewHTTPError(http.StatusForbidden, "Forbidden")
}
return
}
// GetProjectBackground serves a previously set background from a project
// It has no knowledge of the provider that was responsible for setting the background.
// @Summary Get the project background
@ -364,7 +390,7 @@ func GetProjectBackground(c *echo.Context) error {
_ = s.Rollback()
return err
}
stat, err := bgFile.File.Stat()
stat, err := files.FileStat(bgFile)
if err != nil {
_ = s.Rollback()
return err
@ -417,7 +443,7 @@ func RemoveProjectBackground(c *echo.Context) error {
s := db.NewSession()
defer s.Close()
project, auth, err := checkProjectBackgroundRights(s, c)
project, auth, err := checkProjectBackgroundWritePermissions(s, c)
if err != nil {
_ = s.Rollback()
return err

View File

@ -71,7 +71,7 @@ func Login(c *echo.Context) (err error) {
}
}
if user.Status == user2.StatusDisabled {
if user.Status == user2.StatusDisabled || user.Status == user2.StatusAccountLocked {
_ = s.Rollback()
return &user2.ErrAccountDisabled{UserID: user.ID}
}
@ -244,7 +244,7 @@ func RefreshToken(c *echo.Context) (err error) {
return err
}
if u.Status == user2.StatusDisabled {
if u.Status == user2.StatusDisabled || u.Status == user2.StatusAccountLocked {
if _, err := s.Where("id = ?", session.ID).Delete(&models.Session{}); err != nil {
_ = s.Rollback()
return err

View File

@ -236,6 +236,6 @@ func GetTaskAttachment(c *echo.Context) error {
return err
}
http.ServeContent(c.Response(), c.Request(), taskAttachment.File.Name, taskAttachment.File.Created, taskAttachment.File.File)
http.ServeContent(c.Response(), c.Request(), taskAttachment.File.Name, taskAttachment.File.Created, taskAttachment.File.File.(io.ReadSeeker))
return nil
}

View File

@ -17,10 +17,13 @@
package v1
import (
"io"
"net/http"
"os"
"strconv"
"time"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/files"
@ -147,7 +150,16 @@ func DownloadUserDataExport(c *echo.Context) error {
return err
}
http.ServeContent(c.Response(), c.Request(), exportFile.Name, exportFile.Created, exportFile.File)
if config.FilesType.GetString() == "s3" {
c.Response().Header().Set("Content-Disposition", "attachment; filename=\""+exportFile.Name+"\"")
c.Response().Header().Set("Content-Type", exportFile.Mime)
c.Response().Header().Set("Content-Length", strconv.FormatUint(exportFile.Size, 10))
c.Response().Header().Set("Last-Modified", exportFile.Created.UTC().Format(http.TimeFormat))
_, err = io.Copy(c.Response(), exportFile.File)
return err
}
http.ServeContent(c.Response(), c.Request(), exportFile.Name, exportFile.Created, exportFile.File.(io.ReadSeeker))
return nil
}

View File

@ -47,6 +47,18 @@ func BasicAuth(c *echo.Context, username, password string) (bool, error) {
log.Errorf("Error during basic auth for caldav: %v", err)
return false, nil
}
// If the user has TOTP enabled, reject password-based basic auth.
// They must use a CalDAV token instead.
totpEnabled, err := user.TOTPEnabledForUser(s, u)
if err != nil {
log.Errorf("Error checking TOTP status for caldav basic auth: %v", err)
return false, nil
}
if totpEnabled {
log.Warningf("CalDAV basic auth rejected for user %d: TOTP is enabled, a CalDAV token is required", u.ID)
return false, nil
}
}
if u != nil && err == nil {
c.Set("userBasicAuth", u)

View File

@ -82,7 +82,7 @@ func ProjectHandler(c *echo.Context) error {
log.Debugf("[CALDAV] Request Headers: %v\n", c.Request().Header)
caldav.SetupStorage(storage)
caldav.SetupUser("dav/projects")
caldav.SetupUser(strings.TrimPrefix(ProjectHomeSetPath, "/"))
caldav.SetupSupportedComponents([]string{lib.VCALENDAR, lib.VTODO})
response := caldav.HandleRequest(c.Request())
response.Write(c.Response())
@ -137,7 +137,7 @@ func PrincipalHandler(c *echo.Context) error {
log.Debugf("[CALDAV] Request Headers: %v\n", c.Request().Header)
caldav.SetupStorage(storage)
caldav.SetupUser("dav/principals/" + u.Username)
caldav.SetupUser(strings.TrimPrefix(principalPathForUser(u.Username), "/"))
caldav.SetupSupportedComponents([]string{lib.VCALENDAR, lib.VTODO})
response := caldav.HandleRequest(c.Request())
@ -166,7 +166,7 @@ func EntryHandler(c *echo.Context) error {
log.Debugf("[CALDAV] Request Headers: %v\n", c.Request().Header)
caldav.SetupStorage(storage)
caldav.SetupUser("dav/principals/" + u.Username)
caldav.SetupUser(strings.TrimPrefix(principalPathForUser(u.Username), "/"))
caldav.SetupSupportedComponents([]string{lib.VCALENDAR, lib.VTODO})
response := caldav.HandleRequest(c.Request())

View File

@ -35,10 +35,16 @@ import (
)
// DavBasePath is the base url path
const DavBasePath = `/dav/`
const DavBasePath = `/dav`
// ProjectBasePath is the base path for all projects resources
const ProjectBasePath = DavBasePath + `projects`
const ProjectBasePath = DavBasePath + `/projects`
// PrincipalBasePath is the base path for all principal resources
const PrincipalBasePath = DavBasePath + `/principals`
// ProjectHomeSetPath is the CalDAV home-set path Apple clients use after discovery.
const ProjectHomeSetPath = ProjectBasePath + `/`
// VikunjaCaldavProjectStorage represents a project storage
type VikunjaCaldavProjectStorage struct {
@ -68,7 +74,7 @@ func (vcls *VikunjaCaldavProjectStorage) GetResources(rpath string, withChildren
// and not /dav/projects. I'm not sure if thats a bug in the client or in caldav-go.
if vcls.isEntry {
r := data.NewResource(rpath, &VikunjaProjectResourceAdapter{
r := data.NewResource(withTrailingSlash(rpath), &VikunjaProjectResourceAdapter{
isPrincipal: true,
isCollection: true,
})
@ -77,7 +83,7 @@ func (vcls *VikunjaCaldavProjectStorage) GetResources(rpath string, withChildren
// If the request wants the principal url, we'll return that and nothing else
if vcls.isPrincipal {
r := data.NewResource(DavBasePath+`/projects/`, &VikunjaProjectResourceAdapter{
r := data.NewResource(ProjectHomeSetPath, &VikunjaProjectResourceAdapter{
isPrincipal: true,
isCollection: true,
})
@ -128,6 +134,14 @@ func (vcls *VikunjaCaldavProjectStorage) GetResources(rpath string, withChildren
}
projects := theprojects.([]*models.Project)
if !withChildren {
r := data.NewResource(withTrailingSlash(rpath), &VikunjaProjectResourceAdapter{
isPrincipal: true,
isCollection: true,
})
return []data.Resource{r}, nil
}
var resources []data.Resource
for _, l := range projects {
rr := VikunjaProjectResourceAdapter{
@ -144,6 +158,20 @@ func (vcls *VikunjaCaldavProjectStorage) GetResources(rpath string, withChildren
return resources, nil
}
func withTrailingSlash(path string) string {
if path == "" {
return ProjectHomeSetPath
}
if strings.HasSuffix(path, "/") {
return path
}
return path + "/"
}
func principalPathForUser(username string) string {
return withTrailingSlash(PrincipalBasePath + `/` + username)
}
// GetResourcesByList fetches a list of resources from a slice of paths
func (vcls *VikunjaCaldavProjectStorage) GetResourcesByList(rpaths []string) (resources []data.Resource, err error) {

View File

@ -54,6 +54,7 @@ package routes
import (
"context"
"log/slog"
"net"
"strings"
"time"
@ -125,6 +126,24 @@ func NewEcho() *echo.Echo {
}),
})
// Configure IP extraction to prevent rate limit bypass via spoofed headers.
// Echo's default RealIP() trusts X-Forwarded-For and X-Real-IP unconditionally,
// which allows attackers to bypass IP-based rate limits.
// See: https://echo.labstack.com/docs/ip-address
switch config.ServiceIPExtractionMethod.GetString() {
case "xff":
trustOptions := parseTrustedProxies(config.ServiceTrustedProxies.GetString())
e.IPExtractor = echo.ExtractIPFromXFFHeader(trustOptions...)
log.Debugf("IP extraction: X-Forwarded-For with %d trusted proxy ranges", len(trustOptions))
case "realip":
trustOptions := parseTrustedProxies(config.ServiceTrustedProxies.GetString())
e.IPExtractor = echo.ExtractIPFromRealIPHeader(trustOptions...)
log.Debugf("IP extraction: X-Real-IP with %d trusted proxy ranges", len(trustOptions))
default:
e.IPExtractor = echo.ExtractIPDirect()
log.Debugf("IP extraction: direct (TCP remote address)")
}
e.Logger = log.NewEchoLogger(config.LogEnabled.GetBool(), config.LogHTTP.GetString(), config.LogFormat.GetString())
// Logger
@ -181,6 +200,27 @@ func NewEcho() *echo.Echo {
return e
}
func parseTrustedProxies(proxies string) []echo.TrustOption {
if proxies == "" {
return nil
}
var options []echo.TrustOption
for _, cidr := range strings.Split(proxies, ",") {
cidr = strings.TrimSpace(cidr)
if cidr == "" {
continue
}
_, ipNet, err := net.ParseCIDR(cidr)
if err != nil {
log.Warningf("Invalid trusted proxy CIDR %q: %v", cidr, err)
continue
}
options = append(options, echo.TrustIPRange(ipNet))
}
return options
}
func setupSentry(e *echo.Echo) {
if !config.SentryEnabled.GetBool() {
return
@ -806,6 +846,7 @@ func registerCalDavRoutes(c *echo.Group) {
// THIS is the entry point for caldav clients, otherwise projects will show up double
c.Any("", caldav.EntryHandler)
c.Any("/", caldav.EntryHandler)
c.Any("/principals/*", caldav.PrincipalHandler)
c.Any("/principals/*/", caldav.PrincipalHandler)
c.Any("/projects", caldav.ProjectHandler)
c.Any("/projects/", caldav.ProjectHandler)

View File

@ -373,6 +373,32 @@ func (err ErrInvalidTOTPPasscode) HTTPError() web.HTTPError {
}
}
// ErrTOTPPasscodeUsed represents a "TOTPPasscodeUsed" kind of error.
// This is returned when a TOTP passcode has already been used within its validity window.
type ErrTOTPPasscodeUsed struct{}
// IsErrTOTPPasscodeUsed checks if an error is a ErrTOTPPasscodeUsed.
func IsErrTOTPPasscodeUsed(err error) bool {
_, ok := err.(ErrTOTPPasscodeUsed)
return ok
}
func (err ErrTOTPPasscodeUsed) Error() string {
return "This totp passcode has already been used"
}
// ErrCodeTOTPPasscodeUsed holds the unique world-error code of this error
const ErrCodeTOTPPasscodeUsed = 1025
// HTTPError holds the http error description
func (err ErrTOTPPasscodeUsed) HTTPError() web.HTTPError {
return web.HTTPError{
HTTPCode: http.StatusPreconditionFailed,
Code: ErrCodeTOTPPasscodeUsed,
Message: "This totp passcode has already been used.",
}
}
// ErrInvalidAvatarProvider represents a "InvalidAvatarProvider" kind of error.
type ErrInvalidAvatarProvider struct {
AvatarProvider string

View File

@ -37,7 +37,7 @@ func InitTests() {
log.Fatal(err)
}
err = db.InitTestFixtures("users", "user_tokens")
err = db.InitTestFixtures("users", "user_tokens", "totp")
if err != nil {
log.Fatal(err)
}

View File

@ -17,7 +17,10 @@
package user
import (
"fmt"
"image"
"strconv"
"time"
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/log"
@ -136,9 +139,50 @@ func ValidateTOTPPasscode(s *xorm.Session, passcode *TOTPPasscode) (t *TOTP, err
return nil, ErrInvalidTOTPPasscode{Passcode: passcode.Passcode}
}
// Prevent passcode reuse within the validity window.
// Store the timestamp when the passcode was used; treat entries older than
// 90 seconds (30s TOTP window + clock skew) as expired.
const totpUsedTTL = 90 * time.Second
usedKey := fmt.Sprintf("totp_used_%s_%s", strconv.FormatInt(passcode.User.ID, 10), passcode.Passcode)
val, exists, err := keyvalue.Get(usedKey)
if err != nil {
return nil, err
}
if exists {
if usedAt, ok := val.(int64); ok && time.Since(time.Unix(usedAt, 0)) < totpUsedTTL {
return nil, ErrTOTPPasscodeUsed{}
}
// Entry expired — allow reuse, overwrite below
}
// Mark this passcode as used with the current timestamp
err = keyvalue.Put(usedKey, time.Now().Unix())
if err != nil {
return nil, err
}
// Lazily clean up expired entries to prevent unbounded growth
go cleanupExpiredTOTPKeys(totpUsedTTL)
return
}
func cleanupExpiredTOTPKeys(ttl time.Duration) {
keys, err := keyvalue.ListKeys("totp_used_")
if err != nil {
return
}
for _, key := range keys {
val, exists, err := keyvalue.Get(key)
if err != nil || !exists {
continue
}
if usedAt, ok := val.(int64); ok && time.Since(time.Unix(usedAt, 0)) >= ttl {
_ = keyvalue.Del(key)
}
}
}
// GetTOTPQrCodeForUser returns a qrcode for a user's totp setting
func GetTOTPQrCodeForUser(s *xorm.Session, user *User) (qrcode image.Image, err error) {
t, err := GetTOTPForUser(s, user)
@ -196,7 +240,7 @@ func HandleFailedTOTPAuth(s *xorm.Session, user *User) {
log.Errorf("Could send password information mail to user %d after 10 failed TOTP attempts: %s", user.ID, err)
return
}
err = user.SetStatus(s, StatusDisabled)
err = user.SetStatus(s, StatusAccountLocked)
if err != nil {
log.Errorf("Could not disable user %d: %s", user.ID, err)
}

57
pkg/user/totp_test.go Normal file
View File

@ -0,0 +1,57 @@
// 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 user
import (
"testing"
"time"
"code.vikunja.io/api/pkg/db"
"github.com/pquerna/otp/totp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTOTPPasscodeCannotBeReused(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
// Generate a valid TOTP passcode for user10's secret from the fixture
// user10 has TOTP enabled with this secret in pkg/db/fixtures/totp.yml
secret := "JBSWY3DPEHPK3PXP" //nolint:gosec
passcode, err := totp.GenerateCode(secret, time.Now())
require.NoError(t, err)
user := &User{ID: 10}
// First use should succeed
_, err = ValidateTOTPPasscode(s, &TOTPPasscode{
User: user,
Passcode: passcode,
})
require.NoError(t, err)
// Second use of the same passcode should fail
_, err = ValidateTOTPPasscode(s, &TOTPPasscode{
User: user,
Passcode: passcode,
})
require.Error(t, err)
assert.True(t, IsErrTOTPPasscodeUsed(err), "expected ErrTOTPPasscodeUsed, got: %v", err)
}

View File

@ -61,6 +61,8 @@ func (s Status) String() string {
return "Email Confirmation required"
case StatusDisabled:
return "Disabled"
case StatusAccountLocked:
return "Locked"
}
return "Unknown"
@ -70,6 +72,7 @@ const (
StatusActive Status = iota
StatusEmailConfirmationRequired
StatusDisabled
StatusAccountLocked
)
// User holds information about an user
@ -153,7 +156,7 @@ func (u *User) ShouldNotify(sessions ...*xorm.Session) (bool, error) {
return false, err
}
return user.Status != StatusDisabled, err
return user.Status != StatusDisabled && user.Status != StatusAccountLocked, err
}
func (u *User) Lang() string {

View File

@ -47,6 +47,10 @@ func ConfirmEmail(s *xorm.Session, c *EmailConfirm) (err error) {
return
}
if user.Status == StatusDisabled {
return &ErrAccountDisabled{UserID: user.ID}
}
user.Status = StatusActive
err = removeTokens(s, user, TokenEmailConfirm)
if err != nil {

View File

@ -70,7 +70,13 @@ func ResetPassword(s *xorm.Session, reset *PasswordReset) (userID int64, err err
return
}
user.Status = StatusActive
if user.Status == StatusDisabled {
return 0, &ErrAccountDisabled{UserID: user.ID}
}
if user.Status == StatusAccountLocked || user.Status == StatusEmailConfirmationRequired {
user.Status = StatusActive
}
_, err = s.
Cols("password", "status").
Where("id = ?", user.ID).
@ -110,6 +116,10 @@ func RequestUserPasswordResetTokenByEmail(s *xorm.Session, tr *PasswordTokenRequ
return
}
if user.Status == StatusDisabled {
return &ErrAccountDisabled{UserID: user.ID}
}
return RequestUserPasswordResetToken(s, user)
}

View File

@ -470,6 +470,33 @@ func TestUserPasswordReset(t *testing.T) {
require.Error(t, err)
assert.True(t, IsErrInvalidPasswordResetToken(err))
})
t.Run("disabled user cannot reset password", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
reset := &PasswordReset{
Token: "disableduserpasswordresettoken",
NewPassword: "12345678",
}
_, err := ResetPassword(s, reset)
require.Error(t, err)
assert.True(t, IsErrAccountDisabled(err))
})
}
func TestRequestPasswordResetTokenDisabledUser(t *testing.T) {
t.Run("disabled user cannot request password reset token", func(t *testing.T) {
db.LoadAndAssertFixtures(t)
s := db.NewSession()
defer s.Close()
err := RequestUserPasswordResetTokenByEmail(s, &PasswordTokenRequest{
Email: "user17@example.com",
})
require.Error(t, err)
assert.True(t, IsErrAccountDisabled(err))
})
}
func TestCleanupOldTokens(t *testing.T) {
@ -490,9 +517,10 @@ func TestCleanupOldTokens(t *testing.T) {
deleted, err := CleanupOldTokens(s)
require.NoError(t, err)
// Fixtures have two old tokens that should be cleaned up:
// id=1 (kind=1, TokenPasswordReset, created 2021) and id=4 (kind=3, TokenAccountDeletion, created 2021)
assert.Equal(t, int64(2), deleted)
// Fixtures have three old tokens that should be cleaned up:
// id=1 (kind=1, TokenPasswordReset, created 2021), id=4 (kind=3, TokenAccountDeletion, created 2021),
// and id=5 (kind=1, TokenPasswordReset for disabled user, created 2024)
assert.Equal(t, int64(3), deleted)
err = s.Commit()
require.NoError(t, err)

View File

@ -0,0 +1,48 @@
// 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 webtests
import (
"net/http"
"testing"
bgHandler "code.vikunja.io/api/pkg/modules/background/handler"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestProjectBackgroundDeletePermission(t *testing.T) {
t.Run("Read-only user cannot delete project background", func(t *testing.T) {
// testuser15 has read-only access (permission: 0) to project 35,
// which has background_file_id: 1.
// Deleting the background should require write access.
_, err := newTestRequestWithUser(
t,
http.MethodDelete,
bgHandler.RemoveProjectBackground,
&testuser15,
"",
nil,
map[string]string{"project": "35"},
)
// Should be forbidden for a read-only user
require.Error(t, err)
assert.Equal(t, http.StatusForbidden, getHTTPErrorCode(err))
})
}

View File

@ -27,6 +27,7 @@ import (
"code.vikunja.io/api/pkg/routes/caldav"
ics "github.com/arran4/golang-ical"
"github.com/labstack/echo/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@ -85,6 +86,96 @@ END:VCALENDAR`
})
}
func TestCaldavDiscovery(t *testing.T) {
t.Run("Project home set depth 1 includes child projects but not itself", func(t *testing.T) {
e, _ := setupTestEnv()
propfindBody := `<?xml version="1.0" encoding="utf-8" ?>
<A:propfind xmlns:A="DAV:" xmlns:B="urn:ietf:params:xml:ns:caldav">
<A:prop>
<A:current-user-principal />
<B:calendar-home-set />
<A:resourcetype />
</A:prop>
</A:propfind>`
c, rec := createRequest(e, "PROPFIND", propfindBody, nil, nil)
c.Request().Header.Set(echo.HeaderContentType, echo.MIMETextXML)
c.Request().Header.Set("Depth", "1")
c.Request().URL.Path = caldav.ProjectBasePath + "/"
c.Request().RequestURI = caldav.ProjectBasePath + "/"
result, _ := caldav.BasicAuth(c, testuser15.Username, "12345678")
require.True(t, result)
err := caldav.ProjectHandler(c)
require.NoError(t, err)
assert.Equal(t, 207, rec.Result().StatusCode)
responseBody := rec.Body.String()
assert.Contains(t, responseBody, "/dav/projects/36")
assert.NotContains(t, responseBody, "<d:href>/dav/projects/</d:href>")
assert.NotContains(t, responseBody, "<D:href>/dav/projects/</D:href>")
})
t.Run("Project home set depth 0 returns the home set itself", func(t *testing.T) {
e, _ := setupTestEnv()
propfindBody := `<?xml version="1.0" encoding="utf-8" ?>
<A:propfind xmlns:A="DAV:" xmlns:B="urn:ietf:params:xml:ns:caldav">
<A:prop>
<A:current-user-principal />
<B:calendar-home-set />
<A:resourcetype />
</A:prop>
</A:propfind>`
c, rec := createRequest(e, "PROPFIND", propfindBody, nil, nil)
c.Request().Header.Set(echo.HeaderContentType, echo.MIMETextXML)
c.Request().Header.Set("Depth", "0")
c.Request().URL.Path = caldav.ProjectBasePath + "/"
c.Request().RequestURI = caldav.ProjectBasePath + "/"
result, _ := caldav.BasicAuth(c, testuser15.Username, "12345678")
require.True(t, result)
err := caldav.ProjectHandler(c)
require.NoError(t, err)
assert.Equal(t, 207, rec.Result().StatusCode)
responseBody := rec.Body.String()
assert.Contains(t, responseBody, "/dav/projects/")
})
t.Run("Principal discovery points to normalized project home set path", func(t *testing.T) {
e, _ := setupTestEnv()
propfindBody := `<?xml version="1.0" encoding="utf-8" ?>
<A:propfind xmlns:A="DAV:" xmlns:B="urn:ietf:params:xml:ns:caldav">
<A:prop>
<A:current-user-principal />
<B:calendar-home-set />
</A:prop>
</A:propfind>`
c, rec := createRequest(e, "PROPFIND", propfindBody, nil, nil)
c.Request().Header.Set(echo.HeaderContentType, echo.MIMETextXML)
c.Request().URL.Path = caldav.PrincipalBasePath + "/user15/"
c.Request().RequestURI = caldav.PrincipalBasePath + "/user15/"
result, _ := caldav.BasicAuth(c, testuser15.Username, "12345678")
require.True(t, result)
err := caldav.PrincipalHandler(c)
require.NoError(t, err)
assert.Equal(t, 207, rec.Result().StatusCode)
responseBody := rec.Body.String()
assert.Contains(t, responseBody, "<D:href>/dav/projects</D:href>")
assert.NotContains(t, responseBody, "/dav//projects/")
})
}
func TestCaldavSubtasks(t *testing.T) {
const vtodoHeader = `BEGIN:VCALENDAR
VERSION:2.0
@ -643,3 +734,28 @@ func TestCaldavProjectReport(t *testing.T) {
}
})
}
func TestCaldavTOTPBlocksBasicAuth(t *testing.T) {
t.Run("Basic auth with password is rejected when TOTP is enabled", func(t *testing.T) {
e, _ := setupTestEnv()
c, _ := createRequest(e, http.MethodGet, "", nil, nil)
// testuser10 has TOTP enabled via fixtures.
// "12345678" is the plaintext password for all test users.
result, err := caldav.BasicAuth(c, testuser10.Username, "12345678")
require.NoError(t, err)
assert.False(t, result, "BasicAuth should reject password login when user has TOTP enabled")
})
t.Run("Basic auth with caldav token still works when TOTP is enabled", func(t *testing.T) {
e, _ := setupTestEnv()
c, _ := createRequest(e, http.MethodGet, "", nil, nil)
// testuser10 has TOTP enabled AND a CalDAV token (kind=4) in fixtures.
// "caldavtesttoken" is the plaintext of the bcrypt hash in user_tokens.yml.
// CalDAV token auth should bypass the TOTP check.
result, err := caldav.BasicAuth(c, testuser10.Username, "caldavtesttoken")
require.NoError(t, err)
assert.True(t, result, "BasicAuth with CalDAV token should succeed even when TOTP is enabled")
})
}

View File

@ -311,3 +311,26 @@ func TestTaskComments(t *testing.T) {
})
})
}
func TestTaskCommentIDOR(t *testing.T) {
t.Run("Cannot read comment from inaccessible task via accessible task ID", func(t *testing.T) {
// Comment 18 belongs to task 34 (owned by user 13, inaccessible to testuser1).
// Task 1 is accessible to testuser1.
// Requesting GET /tasks/1/comments/18 should fail because the comment
// does not belong to task 1.
testHandler := webHandlerTest{
user: &testuser1,
strFunc: func() handler.CObject {
return &models.TaskComment{}
},
t: t,
}
_, err := testHandler.testReadOneWithUser(nil, map[string]string{
"task": "1", // task accessible to testuser1
"commentid": "18", // comment belonging to task 34, NOT accessible to testuser1
})
require.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeTaskCommentDoesNotExist)
})
}

View File

@ -65,4 +65,12 @@ func TestUserPasswordReset(t *testing.T) {
require.Error(t, err)
assert.Equal(t, http.StatusPreconditionFailed, getHTTPErrorCode(err))
})
t.Run("Disabled user cannot reset password", func(t *testing.T) {
_, err := newTestRequest(t, http.MethodPost, apiv1.UserResetPassword, `{
"new_password": "12345678",
"token": "disableduserpasswordresettoken"
}`, nil, nil)
require.Error(t, err)
assertHandlerErrorCode(t, err, user.ErrCodeAccountDisabled)
})
}

View File

@ -28,7 +28,8 @@ import (
func TestUserTOTPLocalUser(t *testing.T) {
t.Run("Enroll TOTP for local user", func(t *testing.T) {
rec, err := newTestRequestWithUser(t, http.MethodPost, apiv1.UserTOTPEnroll, &testuser1, "", nil, nil)
// Use testuser15 who has no TOTP enrollment in fixtures
rec, err := newTestRequestWithUser(t, http.MethodPost, apiv1.UserTOTPEnroll, &testuser15, "", nil, nil)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), `"secret"`)
@ -37,6 +38,7 @@ func TestUserTOTPLocalUser(t *testing.T) {
})
t.Run("Get TOTP QR Code for enrolled local user", func(t *testing.T) {
// user1 has TOTP enrolled (but not enabled) via fixtures
rec, err := newTestRequestWithUser(t, http.MethodGet, apiv1.UserTOTPQrCode, &testuser1, "", nil, nil)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, rec.Code)
@ -44,6 +46,7 @@ func TestUserTOTPLocalUser(t *testing.T) {
})
t.Run("Get TOTP settings for enrolled local user", func(t *testing.T) {
// user1 has TOTP enrolled (but not enabled) via fixtures
rec, err := newTestRequestWithUser(t, http.MethodGet, apiv1.UserTOTP, &testuser1, "", nil, nil)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, rec.Code)