Commit Graph
3068 Commits
Author SHA1 Message Date
Frederick [Bot] d6bb209512 [skip ci] Updated swagger docs 2026-09-30 09:09:47 +00:00
kolaenteandTink e2f32c0b32 refactor(client): delete shared legacy infrastructure and Axios 2026-09-30 10:42:35 +02:00
kolaenteandTink 94791debe4 fix(bots): allow owners to manage disabled bots 2026-09-30 10:42:35 +02:00
kolaenteandTink 1f8a45f6a3 test(bots): reproduce disabled bot management failures 2026-09-30 10:42:35 +02:00
kolaenteandTink 515df2092f fix(api): describe public feature names as strings 2026-09-30 10:42:33 +02:00
kolaenteandTink d15795067c test(config): cover generated bootstrap and feature name schema 2026-09-30 10:42:33 +02:00
kolaenteandTink e61cf15617 refactor(auth): migrate session operations to the v2 SDK 2026-09-30 10:42:33 +02:00
Frederick [Bot] 3221474307 [skip ci] Updated swagger docs 2026-09-30 00:37:12 +00:00
kolaente eba8a33d81 feat(auth): return error code 16005 when the refresh cookie is missing
Both refresh endpoints answered a missing cookie with a code-less 401, so
clients couldn't tell it apart from a rotated-away token, which is worth one
retry. ErrNoRefreshToken gives it its own code.

Refs #4023
2026-09-29 22:54:20 +02:00
kolaente cf92339788 fix(auth): add an iat claim to issued JWTs 2026-09-29 17:12:42 +02:00
kolaente 739d364f40 test(auth): require an iat claim on issued JWTs 2026-09-29 17:12:42 +02:00
kolaente 18f3572ed4 test(migration): build the orphan test data from the real label models
The test declared its own minimal label struct. With
VIKUNJA_TESTS_USE_CONFIG the test engine is the database every other test
package shares, so syncing that partial struct created `labels` without
`created_by_id` and left a row in it. Every package running afterwards
then failed to add the column as NOT NULL.
2026-09-28 15:21:55 +02:00
kolaente 2ed1e5299f fix(migration): clean up label_tasks rows orphaned by deleted labels
Removes the rows left behind by the label deletion bug fixed in the
previous commit.
2026-09-28 15:21:55 +02:00
kolaente 44d137aff1 fix(labels): remove task associations when deleting a label
Label.Delete only deleted the row in `labels`. Since `label_tasks` has
no foreign key cascade and no soft-delete column, every association of
the deleted label stayed behind as an orphaned row.
2026-09-28 15:21:55 +02:00
kolaenteandTink dd52c36dd1 fix(utils): exempt the proxy dial from the SSRF guard
Outgoing proxies usually live on a private network, so the SSRF guard
refused to connect to them unless allownonroutableips was enabled,
which disabled the guard for direct connections too. Only the dial to
the configured proxy skips the guard now; proxied targets are resolved
by the proxy.
2026-09-24 18:06:12 +02:00
kolaenteandTink a741f93f22 fix(utils): honor proxy env vars for outgoing requests
The SSRF-safe client used a bare http.Transport, so HTTP_PROXY,
HTTPS_PROXY and NO_PROXY were ignored. Clone the default transport and
read the proxy from the environment when outgoingrequests.proxyurl is
not set.
2026-09-24 18:06:12 +02:00
06c451da40 fix(auth): link OIDC users by email when username fallback misses (#4012)
Both `usernamefallback` + `emailfallback` on: lookup required username
AND email match, so local user with different username never linked,
duplicate account created. Now verified-email-only lookup always tried
last.

Fixes #2022

## How to verify

1. Create a local user `localuser` with email `me@example.com`.
2. Configure an OIDC provider with `usernamefallback: true` and
`emailfallback: true`. In the IdP, create a user whose email is
`me@example.com` (sent with `email_verified: true`) and whose
username/sub is different from `localuser`.
3. Log in to Vikunja through that OIDC provider.
4. **Expected:** You are logged in as `localuser`, and `vikunja user
list` shows no new user for the provider's issuer.

**Before this PR:** a second user with a random or IdP-provided username
was created.

---------

Co-authored-by: kolaente <k@knt.li>
2026-09-24 06:27:43 +00:00
Frederick [Bot] c65edc42c1 chore(i18n): update translations via Crowdin 2026-09-24 00:16:17 +00:00
kolaente 277c97f682 fix(api): respect service.maxitemsperpage in v2 list endpoints
ListParams defaulted per_page to 50 and allowed up to 1000 regardless of the instance setting. per_page now defaults to and is silently capped at service.maxitemsperpage, like /api/v1, and the response envelope reports the effective value.
2026-09-21 18:23:31 +02:00
Frederick [Bot] 8fb91c4e10 chore(i18n): update translations via Crowdin 2026-09-19 00:10:20 +00:00
kolaente d341e88574 test(ratelimit): align basic auth rate limit webtests to a fresh window 2026-09-18 15:59:07 +02:00
kolaenteandTink 33142cd8e6 fix(saved-filters): delete by id instead of every loaded field
CanDelete loads the whole filter into the struct. xorm turns every non-empty
field into a delete condition, so a filters JSON that does not re-serialize
byte for byte silently matches nothing while still returning success.
2026-09-16 17:23:51 +02:00
kolaente 3a50cc1e5a fix(feeds): set target on task assigned notifications in atom feed
Target is not persisted, so rendering the title of a stored
TaskAssignedNotification dereferenced a nil pointer and the feed
returned a 500.

Fixes #3914
2026-09-16 15:33:11 +02:00
Frederick [Bot] 5f35048279 chore(i18n): update translations via Crowdin 2026-09-16 00:10:12 +00:00
kolaente 5b08e0486a test(web): catch ErrorCode-prefixed constants in error code uniqueness check
The regex only matched ErrCode..., so the 1025 and 1026 collisions with ErrorCodeInvalidTimezone and ErrorCodeUsernameReserved went unnoticed.
2026-09-15 18:38:59 +02:00
kolaente d727c61f54 fix(user)!: give ErrCodeAccountLocked a unique error code
1026 was already taken by ErrorCodeUsernameReserved, which is older, so a
locked account was reported as a reserved username in clients.

BREAKING CHANGE: a locked account now returns 1040 instead of 1026.
2026-09-15 18:38:59 +02:00
kolaente 0336497d94 fix(user)!: give ErrCodeTOTPPasscodeUsed a unique error code
1025 was already taken by ErrorCodeInvalidTimezone, which is older, so
reusing a TOTP passcode (e.g. logging in right after enabling 2FA) showed
"The timezone '' is invalid." in the frontend.

Refs #3900

BREAKING CHANGE: a reused TOTP passcode now returns 1039 instead of 1025.
2026-09-15 18:38:59 +02:00
Anand HegdeandGitHub b3e9580e23 fix(caldav): parse Z-suffixed timestamps as UTC (#3883)
Credit to @jnsvgt for the root-cause analysis and suggested fix in
#3871. This PR implements that fix and adds regression tests.

`caldavTimeToTimestamp()` used the `Z` suffix only to pick the layout,
where it matches as a literal character. The value was then parsed in
the server timezone. With a non-UTC `service.timezone`, every UTC value
without a `TZID` was shifted by the server's offset. Absolute VALARM
triggers are always UTC, so reminders were stored hours early.

Fix: when the value ends in `Z`, parse it in UTC and ignore any `TZID`
(RFC 5545 doesn't allow combining the two). Floating values and `TZID`
values work as before. DTSTART, DUE, DTEND, DTSTAMP and COMPLETED all go
through the same helper, so they get the fix too.

Tests, with the server timezone set to Europe/Berlin:
- `TestCaldavTimeToTimestamp_NonUTCServerTimezone`: covers UTC,
floating, TZID, and UTC combined with TZID
- `TestParseTaskFromVTODO_UTCAlarmNonUTCServerTimezone`: the VTODO from
the issue; the reminder should match the due date

Before the fix, the UTC cases and the alarm test fail (`05:00 CEST`
instead of `05:00 UTC`). After the fix, they pass. `go test
./pkg/caldav`, `mage test:caldav` and `mage lint` also pass.

I wrote this with help from Claude Code, then reviewed and tested it
myself.

Note on #1068: that change made floating times (no `Z`, no `TZID`) use
the configured timezone, which this PR keeps. It also routed
`Z`-suffixed UTC times through the same path; this PR only restores UTC
parsing for those.

Fixes #3871
2026-09-15 16:25:31 +00:00
kolaenteandTink 80a9f31cb3 fix(migration): remove the partial blob of a failed import upload write
The files row is inserted before the blob is written, and the local backend creates the file before copying into it, so a failed copy left a partial blob behind once the row was rolled back.
2026-09-15 17:57:00 +02:00
kolaenteandTink c5685e6130 fix(migration): keep a queued import upload out of the storage quota
The vikunja-file import checks the user's stored files against
migration.vikunjafile.maxuserstorage. The upload is now one of those
files, so an import counted its own zip against the quota. Exclude files
referenced as a migration upload.
2026-09-15 17:57:00 +02:00
kolaenteandTink fa9fc4e06f feat(migration): keep queued import uploads in the file storage
The upload was spooled to the receiving instance's temp dir and only its
name travelled with the event. That holds while events stay in-process,
but it ties the job to the instance that accepted the upload.

Store it through the files package on the configured backend and record
its id on the migration status, so the event carries only the status id
and any instance can run the import. The job copies it to a local temp
file first: the zip importer needs random access, which an S3 object body
cannot give.

The upload is removed once the import is over. An hourly cron deletes
leftovers from events lost on restart or instances that died mid-import.
2026-09-15 17:57:00 +02:00
Frederick [Bot] 6696e2419a [skip ci] Updated swagger docs 2026-09-15 15:39:27 +00:00
kolaenteandTink 9c374843af refactor(migration): tidy the ticktick, wekan and csv read paths
Drop comments restating the code and inline single-use variables. No
behaviour change.
2026-09-15 16:59:37 +02:00
kolaenteandTink 3dc5c72904 refactor(migration): parse the csv import config once per request
The config was unmarshalled up front purely to produce a 400, then parsed
again by SetOptions, and a third time in the background job. Drop the
throwaway parse and give SetOptions a domain error that maps to 400 on
its own.
2026-09-15 16:59:37 +02:00
kolaenteandTink e06345e2d3 fix(migration): run file imports in the background
A file import ran inside the upload request. A big export takes minutes,
far longer than a reverse proxy holds a request open, so the client got a
502 while the import kept running and kept the user's migration slot, and
there was no way to cancel or retry it.

The upload request now only validates what is cheap to validate, claims
the slot, spools the upload into a per-process temp directory and queues a
FileMigrationRequestedEvent. FileMigrationListener runs the import through
the lifecycle it now shares with the OAuth migrations: stale event skip,
panic recovery, heartbeat, outcome and notifications. The vikunja-file
migrator rejects a non-zip or structurally broken export before queueing;
ticktick, wekan and csv would have to parse the whole upload, so they fail
in the job and report through the status and failure mail. The csv
migrator receives its column mapping as options carried with the event.
2026-09-15 16:59:37 +02:00
kolaenteandTink c7b37b0af8 refactor(migration): scan the vikunja-file archive once before importing
Split opening the zip and indexing its entries out of Migrate, so every
structural check that doesn't need entry contents runs in one place before
anything is imported. The missing VERSION check moves into that scan.
2026-09-15 16:59:37 +02:00
kolaenteandTink 84804e71f5 feat(migration): record whether a migration succeeded
A finished migration_status row looked the same whether the import worked
or failed, so the client could only guess. Rows now carry error_kind, a
closed set the client translates (reported, interrupted, credentials,
queue, upload, detail), and error_message, set only for "detail": the
error caused by the user's own data, the same text the failure mail sends.
A failure we report to Sentry stores just the kind, so server internals
never reach the status.
2026-09-15 16:59:37 +02:00
kolaenteandTink b5a235b0fd fix(migration): don't report a failed success mail as a failed migration
The migration was already finished and its claim released when the done
notification failed, yet the error was returned and the listener then sent
a failure mail and overwrote the finished row. Log it instead. The failure
mail's own log line also printed the migration error instead of the
notification error.
2026-09-15 16:59:37 +02:00
kolaenteandTink 7f9f3e091f fix(migration): log migration start and completion at info level
A background import that never finished left no trace in a default log
setup, since start and completion were only logged at debug.
2026-09-15 16:59:37 +02:00
kolaenteandTink 68ac9eaa99 fix(migration): judge an abandoned claim by a heartbeat, not by start time
The claim timeout was measured from started_at, so a long import either
kept a dead claim for a day (the old 24h default) or had its slot taken
over while still running. A running migration now refreshes heartbeat_at
at a tenth of the timeout, clamped to 1s..30s, and a claim only counts as
stale once its last beat is older than the timeout. Rows that never beat
fall back to started_at. The default timeout drops to 5m accordingly.

Also bind the stale cutoff in the configured timezone: xorm writes times in
the engine's zone but binds query args as-is, which broke the comparison on
SQLite.
2026-09-15 16:59:37 +02:00
kolaenteandTink 86289e36e3 fix(migration): answer 412 instead of 500 when a migration claim collides
A second import sent while the first claim was still being written could
fail the insert with whatever the driver reports under contention, which
surfaced as a 500. The claim now reads the running row before writing so a
losing request stays read-only, re-reads the live claim whenever the insert
fails, retries lock collisions a few times, and probes for a stale claim
before taking a write lock to release it.
2026-09-15 16:59:37 +02:00
kolaenteandTink 8bf29a1e98 fix(api/v2): bound the body read on upload routes instead of huma's 5s default
Huma's default BodyReadTimeout is a 5s socket read deadline spanning the
whole body, so any upload slower than that is cut off and a reverse proxy
answers 502. Every multipart route now goes through withUploadLimits, which
sets the existing max body size and a finite 15 minute read bound.
2026-09-15 16:59:37 +02:00
kolaenteandTink 308c057618 fix(restore): convert numeric bools when restoring into postgres
SQLite and MySQL dumps store bools as 0/1. Since switching to pgx, restoring
such a dump into PostgreSQL fails with "unable to encode 0 into binary format
for bool".
2026-09-15 14:25:44 +02:00
kolaenteandTink d1ca12f3e0 fix(restore): restore empty json column values as NULL
SQLite dumps can contain empty strings in json columns (e.g. project_views.filter),
which PostgreSQL rejects with "invalid input syntax for type json".

Fixes #3895
2026-09-15 14:25:43 +02:00
kolaente 5cb2838024 feat: add /.well-known/change-password redirect
Implements https://www.w3.org/TR/change-password-url/ so password managers can link users to the password settings page.
2026-09-15 14:12:54 +02:00
Frederick [Bot] 3f924eb78b [skip ci] Updated yaegi symbols 2026-09-15 10:50:55 +00:00
kolaenteandTink d390a7c374 feat(mcp): expose connection settings and token presets
(cherry picked from commit a796db73475469bba18e3c20c8d7d1b818abcc5a)
(cherry picked from commit 0e47ccb001b8bf385f2688126bef8f4918917b45)
2026-09-15 11:41:34 +02:00
kolaenteandTink 84e5d2cd62 feat(routes): serve the MCP endpoint at /api/v2/mcp
Registered after apiv2.RegisterAll so the tool index sees the AutoPatch
operations. Routes under the prefix skip the token route check: the
endpoint checks mcp:access itself and every loopback call is checked on
its own route.
2026-09-15 11:41:34 +02:00
kolaenteandTink bba48e40bc feat(mcp): add the module and streamable-http transport
mcp.New builds the tool index from the v2 document and serves a stateless
per-request server: JWTs are rejected, mcp:access is required, tools/list is
filtered by the caller's token, and cross-origin browser requests are
refused unless the CORS configuration trusts the origin.
2026-09-15 11:41:34 +02:00
kolaenteandTink 4ccf95bf83 feat(mcp): add catalog discovery and execution meta-tools
find_action lists the catalog actions the token authorizes, with a
description generated per request; do_action invokes one.
2026-09-15 11:41:34 +02:00