Commit Graph

2249 Commits

Author SHA1 Message Date
kolaente 848a4e7f07 test: remove redundant webhook SSRF tests
The SSRF protection is now tested at the shared utility level in
pkg/utils/httpclient_test.go. The webhook-specific SSRF tests were
duplicating the same checks since getWebhookHTTPClient() delegates
to NewSSRFSafeHTTPClient().
2026-03-23 16:34:22 +00:00
kolaente d4d88c0f59 test: use new outgoingrequests config keys in SSRF tests 2026-03-23 16:34:22 +00:00
kolaente e5a1c05771 refactor: use shared SSRF-safe HTTP client in webhook code 2026-03-23 16:34:22 +00:00
kolaente a94109e1be fix: prevent SSRF via Unsplash background image download 2026-03-23 16:34:22 +00:00
kolaente 73edbb6d46 fix: prevent SSRF via Microsoft Todo migration pagination links 2026-03-23 16:34:22 +00:00
kolaente 9329774223 fix: prevent SSRF via migration file attachment URLs (GHSA-g66v-54v9-52pr) 2026-03-23 16:34:22 +00:00
kolaente 363aa66423 fix: prevent SSRF via OpenID Connect avatar download (GHSA-g9xj-752q-xh63) 2026-03-23 16:34:22 +00:00
kolaente 0266fffad2 feat: add shared SSRF-safe HTTP client utility 2026-03-23 16:34:22 +00:00
kolaente f96b53fe99 feat: add outgoingrequests config keys for centralized SSRF protection 2026-03-23 16:34:22 +00:00
kolaente 654d2c7042 fix: prevent link share IDOR by validating project_id in Delete and ReadOne 2026-03-23 16:34:07 +00:00
kolaente b8edc8f17f fix: prevent attachment IDOR by validating task_id in ReadOne (GHSA-jfmm-mjcp-8wq2) 2026-03-23 16:34:07 +00:00
kolaente 3111f3d70c test: add IDOR test for task attachment ReadOne (GHSA-jfmm-mjcp-8wq2) 2026-03-23 16:34:07 +00:00
kolaente b2c3c36b6f test: add attachment fixture on inaccessible task for IDOR test 2026-03-23 16:34:07 +00:00
kolaente 833f2aec00 refactor: use accessibleProjectIDsSubquery in addBucketsToTasks 2026-03-23 16:26:37 +00:00
kolaente 67a47787fa fix: filter related tasks by project access to prevent cross-project info disclosure 2026-03-23 16:26:37 +00:00
kolaente e2683bb2bc refactor: add accessibleProjectIDsSubquery helper for project-level authz filtering 2026-03-23 16:26:37 +00:00
kolaente 50c3eebd23 test: add failing test for cross-project task relation info disclosure 2026-03-23 16:26:37 +00:00
kolaente 589d2a5556 test: add cross-project task relation fixture for authz test 2026-03-23 16:26:37 +00:00
MidoriKurage 68a74416a4 fix(openid): Merge VikunjaGroups and ExtraSettingsLinks from userinfo
When `forceuserinfo: true`, `mergeClaims` discards `vikunja_groups`
and `extra_settings_links` claims fetched from the userinfo endpoint,
failing team sync for opaque tokens.

Fixes team sync for OIDC providers using opaque tokens.
2026-03-23 16:11:17 +00:00
kolaente 212968cec4
chore(lint): suppress additional gosec false positives
Add #nosec comments for G703/G704 findings in db, doctor, webhooks,
gravatar, unsplash, and migration helper code.
2026-03-23 16:40:07 +01:00
kolaente 2053426062
chore(lint): suppress known gosec false positives
Add config-level exclusions for G117 (secret-named struct fields),
G101 in test files, G702/G704 in magefile, and goheader in plugins.
Add inline #nosec comments for specific G703/G704 false positives
in export, dump/restore, migration, and avatar code.
2026-03-23 16:23:15 +01:00
kolaente 595002bf96 fix: update ParadeDB search test count for new fixture
Project 40 (archived child project) is pulled into ParadeDB fuzzy
search results via the recursive CTE.
2026-03-23 14:13:53 +00:00
kolaente d0606eadea fix: check child project's own IsArchived flag in CheckIsArchived
CheckIsArchived() previously skipped checking a child project's own
IsArchived flag when ParentProjectID > 0, immediately recursing to
only check the parent. This allowed write operations on individually
archived child projects whose parent was not archived.

Now the function loads the project from the database first, checks its
own IsArchived flag, and only then recurses to check parent projects.
2026-03-23 14:13:53 +00:00
kolaente a7a8ae072a fix(auth): return correct error type for locked users in OIDC callback
Return ErrAccountLocked for locked users instead of ErrAccountDisabled.
Also skip profile updates and avatar sync for disabled/locked users
found during OIDC login — HandleCallback rejects the auth anyway.
2026-03-23 12:06:16 +00:00
kolaente c7740fc4aa fix(user): use unique error code for ErrCodeAccountLocked
Was 1025 which collides with ErrorCodeInvalidTimezone. Changed to 1026.
2026-03-23 12:06:16 +00:00
kolaente 37394fb336 fix(user): use getUser directly for uniqueness checks in UpdateUser
The username and email uniqueness checks don't need status filtering —
they just need to know if the name/email exists regardless of account
status. Use getUser (which skips the status check) instead of the
public wrappers, reducing cyclomatic complexity back under the threshold.
2026-03-23 12:06:16 +00:00
kolaente 8409bdb120 refactor(user): export IsErrUserStatusError for use across packages
Make isErrUserStatusError public and replace all verbose
!IsErrAccountDisabled(err) && !IsErrAccountLocked(err) checks
with the shorter IsErrUserStatusError(err) call.
2026-03-23 12:06:16 +00:00
kolaente cd6148511a fix(auth): reject disabled/locked users in API token middleware
checkAPITokenAndPutItInContext now returns 401 Unauthorized when the
token owner's account is disabled or locked, instead of a 500 error.
Also fixes the API token test to match the actual middleware behavior.
2026-03-23 12:06:16 +00:00
kolaente 525f5ee407 test: verify GetUserByID rejects disabled users and returns user with error 2026-03-23 12:06:16 +00:00
kolaente 8b614a4cb3 test: verify disabled user is rejected via CalDAV auth
Also fix BasicAuth to check for status errors from checkUserCaldavTokens
before falling through to password-based auth.
2026-03-23 12:06:16 +00:00
kolaente e4379eff10 test: verify disabled user's API token is rejected 2026-03-23 12:06:16 +00:00
kolaente 198322c8e1 test: add API token fixture for disabled user 2026-03-23 12:06:16 +00:00
kolaente 22a4b6fbb8 fix(auth): reject disabled/locked users in OIDC callback 2026-03-23 12:06:16 +00:00
kolaente ea4ba18def fix(user): handle status errors across the codebase, remove redundant checks 2026-03-23 12:06:16 +00:00
kolaente 91c0f386c6 fix(user): handle status errors in pkg/user callers, remove redundant checks 2026-03-23 12:06:16 +00:00
kolaente 04704e0fde fix(user): reject disabled/locked users in getUser by default
getUser now returns ErrAccountDisabled or ErrAccountLocked (alongside
the full user object) for users with StatusDisabled or StatusAccountLocked.
Callers that need disabled/locked users discard the error; all others
propagate it automatically.

GHSA-94xm-jj8x-3cr4
2026-03-23 12:06:16 +00:00
kolaente be771289db feat(user): add ErrAccountLocked error type
Separate from ErrAccountDisabled so callers can distinguish between
admin-disabled accounts (no recovery) and locked accounts (recoverable
via password reset).
2026-03-23 12:06:16 +00:00
kolaente 0f98c19ab6 fix: add TTL-based expiry and cleanup for used TOTP passcode entries
Store a unix timestamp instead of a boolean, and treat entries older
than 90 seconds as expired. A background goroutine lazily cleans up
expired keys after each successful validation to prevent unbounded
growth in the keyvalue store.
2026-03-23 10:34:49 +00:00
kolaente acafa6db10 fix: update TOTP reuse test to use user10 matching rebased fixture 2026-03-23 10:34:49 +00:00
kolaente 5f06e1dce5 fix: prevent TOTP passcode reuse within validity window
Store used TOTP passcodes in the keyvalue store after successful
validation. On subsequent validation attempts, check if the passcode
was already used for the same user and reject it with
ErrTOTPPasscodeUsed. This prevents replay attacks where an intercepted
TOTP code could be reused within its 30-second validity window.
2026-03-23 10:34:49 +00:00
kolaente 5591ca94ba test: add failing test for TOTP passcode reuse prevention
Add TestTOTPPasscodeCannotBeReused which verifies that a valid TOTP
passcode cannot be used twice within its validity window. Also add
ErrTOTPPasscodeUsed error type for the new behavior.
2026-03-23 10:34:49 +00:00
kolaente de58f630ee test: add TOTP fixture and load it in user test bootstrap
Add a TOTP fixture for user1 with a known secret to enable
testing TOTP validation logic. Update InitTests to load the
totp fixture alongside users and user_tokens.
2026-03-23 10:34:49 +00:00
Frederick [Bot] 1b246a0ff7 chore(i18n): update translations via Crowdin 2026-03-21 01:09:32 +00:00
kolaente 1f2aef776c test: verify CalDAV token auth bypasses TOTP check
Add a CalDAV token fixture (kind=4) for user10 who has TOTP enabled,
and implement the previously-skipped test proving token-based auth
still works when TOTP is active.
2026-03-20 12:22:27 +00:00
kolaente 1ed813caf0 fix: update TOTP fixtures and tests to avoid conflicts with existing enrollment tests
- user10 gets enabled TOTP (for CalDAV 2FA test)
- user1 gets enrolled-but-not-enabled TOTP (for existing QR/settings tests)
- TOTP enrollment test uses user2 (no TOTP fixture) instead of user1
2026-03-20 12:22:27 +00:00
kolaente 659e73af05 fix: use user10 instead of user1 for TOTP fixture to avoid breaking login tests 2026-03-20 12:22:27 +00:00
kolaente cdf5d30a42 fix: reject CalDAV basic auth when TOTP is enabled 2026-03-20 12:22:27 +00:00
kolaente a66bda2f51 test: register totp fixture in test setup 2026-03-20 12:22:27 +00:00
kolaente bda16e770f test: add failing test for CalDAV 2FA bypass via basic auth 2026-03-20 12:22:27 +00:00
kolaente 27ef92b9bf test: add TOTP fixture data for user1 2026-03-20 12:22:27 +00:00
kolaente b7a1408098 fix: use require.Error instead of assert.Error for error assertions 2026-03-20 11:41:28 +00:00
kolaente 4b91e5efa1 refactor: rename checkProjectBackgroundWriteRights to checkProjectBackgroundWritePermissions 2026-03-20 11:41:28 +00:00
kolaente 49419619bd fix: only enforce task_id check when TaskID is provided
Internal callers (reactions) look up comments by ID without knowing
the task. The IDOR protection is still effective because ReadOne
always has TaskID set from the URL parameter.
2026-03-20 11:41:28 +00:00
kolaente f066eb3ea4 fix: require CanUpdate for project background deletion
RemoveProjectBackground previously used checkProjectBackgroundRights
which only checks CanRead, allowing read-only users to delete project
backgrounds. Added checkProjectBackgroundWriteRights that checks
CanUpdate and use it in RemoveProjectBackground.

Ref: GHSA-564f-wx8x-878h
2026-03-20 11:41:28 +00:00
kolaente f60f3af70b test: add failing test for project background delete with read-only access
Proves that a user with read-only access to a project can delete its
background image. The test expects a 403 Forbidden but the operation
proceeds because RemoveProjectBackground only checks CanRead.

Adds fixture entry giving user 15 read-only access to project 35
(which has a background_file_id).

Ref: GHSA-564f-wx8x-878h
2026-03-20 11:41:28 +00:00
kolaente bc6d843ed4 fix: verify comment belongs to task in URL to prevent IDOR
Add task_id check to getTaskCommentSimple so that a comment can only
be loaded if it actually belongs to the task specified in the URL.
Previously, any valid comment ID could be read through any accessible
task endpoint.

Ref: GHSA-mr3j-p26x-72x4
2026-03-20 11:41:28 +00:00
kolaente 2da89258e5 test: add failing test for task comment IDOR
Proves that a user can read a comment from an inaccessible task by
supplying an accessible task ID in the URL. Comment 18 belongs to
task 34 (owned by user 13), but testuser1 can read it via task 1.

Ref: GHSA-mr3j-p26x-72x4
2026-03-20 11:41:28 +00:00
kolaente be0aaa7060 fix: adapt image preview DoS protection to new FileStorage interface
File.File is now io.ReadCloser (no Seek). Buffer the file bytes
once via io.ReadAll, then use bytes.NewReader for both DecodeConfig
and Decode. Test updated to use io.NopCloser instead of afero.
2026-03-20 11:34:41 +00:00
kolaente af61d0f1a0 fix: reject images exceeding 50M pixels before decode 2026-03-20 11:34:41 +00:00
kolaente f7592e2cfd test: add failing test for image preview with oversized dimensions 2026-03-20 11:34:41 +00:00
kolaente 89923ebe70 fix: update test expectations for new disabled user fixture
- TestListUsers expects 17 users (was 16)
- TestCleanupOldTokens expects 3 old tokens deleted (was 2)
2026-03-20 11:23:21 +00:00
kolaente 049f4a6be4 fix: prevent email confirmation from re-enabling admin-disabled accounts 2026-03-20 11:23:21 +00:00
kolaente 2260d763b5 test: add web test for disabled user password reset rejection 2026-03-20 11:23:21 +00:00
kolaente 241b0e80b6 test: add tests for disabled user password reset prevention 2026-03-20 11:23:21 +00:00
kolaente 708ccab895 fix: reject password reset token requests for disabled users 2026-03-20 11:23:21 +00:00
kolaente d8570c603d fix: prevent password reset from re-enabling admin-disabled accounts 2026-03-20 11:23:21 +00:00
kolaente 4c80932b64 fix: block login for StatusAccountLocked users 2026-03-20 11:23:21 +00:00
kolaente 7792bf6cea refactor: use StatusAccountLocked for TOTP lockouts 2026-03-20 11:23:21 +00:00
kolaente f42a045bdc feat: add StatusAccountLocked user status for TOTP lockouts 2026-03-20 11:23:21 +00:00
kolaente ddd9ef5f22 style: fix alignment in config key declarations 2026-03-20 11:08:00 +00:00
kolaente a498dd6991 fix: configure Echo IPExtractor to prevent rate limit bypass via spoofed headers 2026-03-20 11:08:00 +00:00
kolaente 26324a740a feat: add service.ipextractionmethod and service.trustedproxies config options 2026-03-20 11:08:00 +00:00
kolaente 17eccd848f test: add FileStat assertion to validate storage path in attachment test 2026-03-20 10:59:44 +01:00
kolaente 4cd63f93a4 fix: use file mime type instead of hardcoded application/zip in S3 export 2026-03-20 10:59:44 +01:00
kolaente 0e1f44e57e refactor: replace afero with FileStorage interface
Replace the github.com/spf13/afero dependency with a purpose-built
FileStorage interface (Open, Write, Stat, Remove, MkdirAll) with three
implementations: localStorage (with basePath), s3Storage (with key
prefix), and memStorage (for tests).

Each implementation owns its base path — callers pass only file IDs.
Delete s3fs.go, change File.File from afero.File to io.ReadCloser,
and fix duplication flows to buffer content for seeking.
2026-03-20 10:59:44 +01:00
maggch b0ede53c05 fix: handle S3 backend in user export download
http.ServeContent requires io.ReadSeeker which S3 files don't support.
Add S3 branch using io.Copy with explicit headers, matching the pattern
already used in task attachment downloads.

Refs: #2347

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-20 10:59:44 +01:00
maggch b065c62007 feat: replace afero-s3 with minimal S3 afero.Fs implementation
Replace the third-party afero-s3 library (and its temporary fork) with
a minimal in-tree afero.Fs implementation (~200 lines) that only supports
the three S3 operations Vikunja actually uses: Open (GetObject), Remove
(DeleteObject), and Stat (HeadObject).

The s3File implementation lazily opens a GetObject stream on first Read
and supports Seek by closing and re-opening the stream from the new offset,
matching the behavior of the fixed afero-s3 fork.

All other afero.Fs/File methods return ErrS3NotSupported since they are
never called in the S3 code path (writes already use direct PutObject).

This removes the fclairamb/afero-s3 dependency and the temporary replace
directive in go.mod entirely.

Closes #2347

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-20 10:59:44 +01:00
Henry Cole e7f1e99878
fix(caldav): use /dav/projects/ as home to make iOS/MacOS reminders work (#2417)
Resolves issue #475 by modifying CalDAV discovery so Apple Reminders can
use /dav/projects/ as the home set without exposing that synthetic path
as a real task list, preserving the existing principal-based flow. This
is because Apple Reminders defaults back to the /dav/projects/ URL,
rather than accepting the /dav/principals/username/ URL specified in
Vikunja.

Resolves #475
2026-03-20 09:33:56 +00:00
kolaente 629b6d447c test(webhooks): allow non-routable IPs in E2E tests
E2E tests use httptest.NewServer bound to 127.0.0.1, which is now
blocked by default SSRF protection. Opt in to non-routable IPs.
2026-03-19 15:18:06 +01:00
kolaente 8d9bc3e65e feat(webhooks): add built-in SSRF protection using daenney/ssrf
Block webhook requests to non-globally-routable IP addresses by default.
Uses net.Dialer.Control hook to validate resolved IPs against IANA
Special Purpose Registries after DNS resolution, preventing DNS rebinding.

Configurable via webhooks.allownonroutableips (default: false).
2026-03-19 15:18:06 +01:00
kolaente d5dbf04bd0 test(webhooks): add SSRF protection tests 2026-03-19 15:18:06 +01:00
kolaente 37fdd088d6 feat(config): add webhooks.allownonroutableips setting 2026-03-19 15:18:06 +01:00
kolaente 3bc0093686 fix: invalidate all sessions when enabling TOTP
When a user enables two factor authentication, all existing sessions are
now invalidated, requiring re-authentication. This prevents pre-existing
sessions from bypassing 2FA. The frontend now shows a notice explaining
the logout before the user confirms, and properly logs out after enabling.

Ref: GHSA-pgc7-cmvg-mvp4
2026-03-19 12:27:44 +01:00
Frederick [Bot] 369b456d64 [skip ci] Updated swagger docs 2026-03-19 09:26:05 +00:00
Tink d11f097eee
fix(tasks): support both expand and expand[] query parameter formats (#2415)
The `expand` query parameter only supported the `expand[]=foo` array
format, but the swagger docs described it as a plain string parameter.
This adds support for both formats (`expand=foo` and `expand[]=foo`),
matching the existing pattern used by `sort_by` and `order_by`
parameters.

Closes #2408

---------

Co-authored-by: kolaente <k@knt.li>
2026-03-19 09:18:11 +00:00
Tink ada2ebab9e
fix: preserve CalDAV inverse relations when parent has no RELATED-TO (#2389)
- Fixes `removeStaleRelations` in CalDAV storage provider to only remove
relations of kinds explicitly declared in the incoming VTODO's
`RELATED-TO` properties
- When a VTODO has no `RELATED-TO` at all (e.g., a parent task from
Tasks.org), no relations are removed — they were auto-created as
inverses by child tasks
- When a VTODO declares specific relation kinds (e.g.,
`RELATED-TO;RELTYPE=PARENT`), only relations of that kind are checked
for staleness; other kinds (like auto-created `subtask` inverses) are
preserved

Fixes #2383

---------

Co-authored-by: kolaente <k@knt.li>
2026-03-11 09:40:09 +01:00
kolaente e19bea8e3a fix: register bulk label route correctly for API token permissions
The tasks_labels_bulk route was not recognized as a CRUD route by
isStandardCRUDRoute, causing it to be processed as a non-CRUD route
and registered in the wrong apiTokenRoutes group. API tokens with
tasks_labels permissions could not access the bulk endpoint, resulting
in a 401 error.

Fixes https://github.com/go-vikunja/vikunja/issues/2375
2026-03-10 23:58:44 +01:00
kolaente 554593cdb6 test: add failing test for bulk label API token route registration 2026-03-10 23:58:44 +01:00
kolaente 675dfb3ea4 test: add web tests for bulk label task endpoint 2026-03-10 23:58:44 +01:00
kolaente 79a612aa5d fix: send account deletion notification before deleting user row
When deleting a user via CLI (`vikunja user delete <id> -n`), the user
row was deleted first, then `notifications.Notify` was called. But
`Notify` calls `User.ShouldNotify()` which queries the database to check
the user's status — and since the row was already deleted within the same
transaction, it returned `ErrUserDoesNotExist`.

Move the notification call before the `DELETE` so the user row still
exists when `ShouldNotify` checks it.

Closes go-vikunja/vikunja#2335
2026-03-10 23:44:53 +01:00
Frederick [Bot] 30fccfb058 chore(i18n): update translations via Crowdin 2026-03-10 01:08:39 +00:00
kolaente a043940e14 refactor: use config.ResolvePath for all rootpath-relative paths
Replaces ad-hoc path joining in files, config value loading, and
default config values with the centralized ResolvePath function.
2026-03-09 16:02:05 +01:00
kolaente 2a7165aaba refactor: add centralized ResolvePath for rootpath-relative paths 2026-03-09 16:02:05 +01:00
kolaente d3cbc4fc4f fix: prefer working directory for service.rootpath default
When running as a systemd service, the binary is often in /usr/local/bin
but WorkingDirectory points to a data directory like /var/lib/vikunja.
Previously, rootpath defaulted to the binary's directory, causing
Vikunja to attempt creating files/db in /usr/local/bin.

Now os.Getwd() is preferred, which respects systemd's WorkingDirectory
and is the intuitive default for all deployment scenarios.
2026-03-09 16:02:05 +01:00
Frederick [Bot] d5f52868c5 chore(i18n): update translations via Crowdin 2026-03-09 01:14:37 +00:00
Frederick [Bot] 74771ed132 [skip ci] Updated swagger docs 2026-03-08 18:53:26 +00:00
kolaente 05cc65fe9e test: add e2e tests for user-level webhooks
Add comprehensive e2e tests for user-level webhook CRUD operations
and update existing project webhook tests to use LoadFixtures() for
cleanup instead of manual DELETE queries.
2026-03-08 19:45:53 +01:00
kolaente 47a0775c73 feat: add API routes for user-level webhooks
Add CRUD endpoints for user-level webhooks under /user/settings/webhooks.
Users can create webhooks that fire on user-directed events (task.reminder.fired,
task.overdue, tasks.overdue). Includes proper permission checks via
canDoWebhook which loads webhook ownership from DB.
2026-03-08 19:45:53 +01:00
kolaente dbbc80aea6 feat: extend WebhookListener for user-level webhooks
Add User field to reminder and overdue events so the webhook listener
can look up user-level webhooks. Add conditional user filtering to
reminder and overdue cron jobs - when only email is enabled, filter to
email-enabled users; when webhooks are enabled, fetch all users so
events can be dispatched. Dispatch TasksOverdueEvent and
TaskReminderFiredEvent for webhook consumption.
2026-03-08 19:45:53 +01:00
kolaente d4577c660f feat: add user_id to webhooks and user-directed event infrastructure
Add user_id column to webhooks table (nullable, for user-level webhooks
vs project-level). Extend webhook model, permissions, and listener to
support user-level webhooks that fire for user-directed events like
task reminders and overdue task notifications.

Add TasksOverdueEvent for dispatching overdue notifications via webhooks.
Update webhook permissions to handle both user-level and project-level
ownership. Add webhook test fixture and register webhooks table in test
fixture loader.
2026-03-08 19:45:53 +01:00
kolaente aacf650ec2 test: add tests for conversational email system
Add tests for conversational header generation, HTML rendering
with p-tag wrapping, plain-text conversion, footer handling,
and conditional action links. Update mention test fixtures
with Project field.
2026-03-08 16:03:47 +01:00
kolaente def73e2f8e feat: add translation keys for conversational emails
Add action strings with doer names for comments, mentions,
assignments, and reminders. Add notification settings footer
and task identifier format strings.
2026-03-08 16:03:47 +01:00
kolaente b3572c5932 feat: convert notifications to conversational email style
Convert task comment, mention, assignment, and reminder notifications
to use the conversational email format. Add Project field to notification
structs, include task identifiers in subjects and headers, add doer
avatars, notification settings links in footers, and From headers.
2026-03-08 16:03:47 +01:00
kolaente d4b03026f0 feat: add conversational email template and rendering
Add conversational email style with GitHub-inspired header design.
Includes mail struct extensions (headerLine, conversational flag),
CreateConversationalHeader helper, HTML template with avatar support,
p-tag wrapping for content lines, plain-text stripping, and
conditional action link rendering.
2026-03-08 16:03:47 +01:00
Casper Børgesen b5afe82713
fix(cli): make user deletion confirmation check Windows compatible (#2339)
The current implementation of the user confirmation when deleting a user
using the CLI seems to favour Linux. On Windows the <ENTER> command adds
"\r\n" to the user input while Linux only adds "\n".

I have added an extra check to the confirmation, but GO is a new
language for me, so there is probably a much better way to do this.

---------

Co-authored-by: kolaente <k@knt.li>
2026-03-05 15:19:08 +01:00
kolaente d36ac9ddda test: fix ParadeDB project search count to 27
The recursive CTE pulls in child projects of matched parents,
resulting in 27 total results, not 12.
2026-03-05 13:57:05 +01:00
kolaente df0e3a84a9 test: fix non-ParadeDB project search count assertion
ILIKE '%Test1%' matches Test1, Test10, Test11, Test19 + favorites = 5,
not 2. Also use 'Test2"' pattern to avoid matching Test20/Test21.
2026-03-05 13:57:05 +01:00
kolaente c7c63e8ead test: add result count assertions for ParadeDB search tests
Address review feedback: assert exact result counts when ParadeDB is
active. fuzzy(1, prefix=true) broadens matches via edit distance,
returning 6 projects for "TEST10", 14 tasks for "number #17", and
12 projects for "Test1".
2026-03-05 13:57:05 +01:00
kolaente b69705e64b test: fix lint and adjust project search test for ParadeDB fuzzy matching
- Use require.NotEmpty instead of require.Greater for testifylint
- Skip exclusion assertions in web project search test when ParadeDB is
  active, since fuzzy(1, prefix=true) on "Test1" also matches Test2, Test3
2026-03-05 13:57:05 +01:00
kolaente 6268c48f15 test: adjust ParadeDB search tests for fuzzy prefix match broadening
ParadeDB fuzzy(1, prefix=true) returns more results than ILIKE due to
edit-distance tolerance on tokenized terms. Adjust assertions to check
containment rather than exact result sets when ParadeDB is active.
2026-03-05 13:57:05 +01:00
kolaente 3568aaacee test: add task #48 to expected results in feature tests
The new fixture task #48 (Landingpages update, project 1) needs to
appear in all feature test expected result sets that list project 1
tasks. Also bumps the expected next index in TestTask_Create.
2026-03-05 13:57:05 +01:00
kolaente e6cbd67ab5 test: call real MultiFieldSearch function and branch on db engine
Instead of manually constructing builder.Expr conditions to simulate
the ParadeDB path, call MultiFieldSearchWithTableAlias() directly and
use isParadeDB() to branch assertions. When ParadeDB is available,
assert the ||| operator with ::pdb.fuzzy(1, t) syntax; otherwise
assert the LIKE/ILIKE fallback. Tests skip when no database engine
is initialized (x == nil).
2026-03-05 13:57:05 +01:00
kolaente 0a38ec0838 fix: use ParadeDB v2 fuzzy prefix matching for search (#2346)
Switch from legacy @@@ paradedb.match() to v2 ||| operator with
::pdb.fuzzy(1, t) cast. This enables prefix matching so 'landing'
matches 'landingpages', and adds single-character typo tolerance.
2026-03-05 13:57:05 +01:00
kolaente ee2723d9cf test: rewrite MultiFieldSearch tests with SQL output verification 2026-03-05 13:57:05 +01:00
kolaente 892b38b3b6 test: add web tests for prefix/substring search (#2346) 2026-03-05 13:57:05 +01:00
kolaente 275f714224 test: add fixture task with compound word for prefix search testing 2026-03-05 13:57:05 +01:00
kolaente 1f3509bf27 test: add e2e API test package with webhook pipeline verification
New pkg/e2etests/ package runs with the real Watermill event system to
verify the full async pipeline: web handler -> DB -> event dispatch ->
Watermill -> listener -> side effect.

First test (TestTaskUpdateWebhookE2E) updates a task and asserts that
a webhook HTTP POST arrives at a test server.
2026-03-05 12:49:27 +01:00
kolaente 1b1e8e5b19 feat: add InitEventsForTesting and Unfake for real event dispatch in tests
InitEventsForTesting sets up a real Watermill GoChannel router with a
cancellable context, returning a readiness channel. Skips Prometheus
metrics and poison queue to avoid duplicate registration panics.

Unfake re-enables real event dispatch after test init helpers call Fake().

Also guards Dispatch against nil pubsub with a clear error message.
2026-03-05 12:49:27 +01:00
kolaente f9cb0a2de1 fix: include remote IP address in HTTP request logs 2026-03-04 23:47:40 +01:00
kolaente 7288483879 fix: handle deleted user in saved filter view event listener
Use a separate error variable for the user lookup in
UpdateTaskInSavedFilterViews so that ErrUserDoesNotExist does not
pollute the named return `err`. The `:=` inside the for loop shadowed
the outer `err`, leaving it set to the stale user-not-found error,
which caused the handler to be poisoned.

Closes #2359
2026-03-04 22:05:01 +01:00
Frederick [Bot] ff70fa1369 [skip ci] Updated swagger docs 2026-03-04 19:40:40 +00:00
kolaente b5086febc7 docs: update user search endpoint description for external team bypass 2026-03-04 20:32:11 +01:00
kolaente 54c7c4aef2 refactor: move ListUsers tests from pkg/user to pkg/models
The ListUsers function now references team_members and teams tables
via a subquery for external team discoverability. The pkg/user test
environment only syncs user tables, so these tests need to run in
pkg/models which has the full schema and all fixtures.

Also adds new tests for the external team discoverability bypass
directly in the models package alongside the moved tests.
2026-03-04 20:32:11 +01:00
kolaente 06617891fa test: verify email masking for external team name search 2026-03-04 20:32:11 +01:00
kolaente 28b913f29f feat: bypass discoverability settings for external team members 2026-03-04 20:32:11 +01:00
kolaente 3a730165bc test: add tests for external team user discoverability bypass 2026-03-04 20:32:11 +01:00
kolaente 64e455a613 test: add user 11 to external team 14 for discoverability tests 2026-03-04 20:32:11 +01:00
Weijie Zhao 54d977532e
fix: allow browser caching for file downloads (#2349) 2026-03-04 17:43:03 +01:00
Frederick [Bot] 740ec183d9 [skip ci] Updated swagger docs 2026-03-04 16:29:06 +00:00
kolaente 9c23e19644 fix: preserve cover image when duplicating task
Track old-to-new attachment ID mapping during duplication and
re-set CoverImageAttachmentID on the new task if the original
had a cover image configured.
2026-03-04 17:20:26 +01:00
kolaente 7aad96b199 fix: close source file handle when duplicating attachments
Save the source file handle before calling NewAttachment (which
overwrites attachment.File) and use defer to ensure it gets closed.
This prevents file descriptor leaks on both success and error paths.
2026-03-04 17:20:26 +01:00
kolaente 692357a648 refactor: use TaskRelation.Create for copy relation
Use the TaskRelation Create method instead of raw inserts. This
handles the bidirectional relation automatically and dispatches
the appropriate event.
2026-03-04 17:20:26 +01:00
kolaente e07eeed211 refactor: batch label inserts during task duplication
Collect all LabelTask records in the loop and insert them in a single
database call instead of inserting one at a time.
2026-03-04 17:20:26 +01:00
kolaente 6da0f68562 fix: remove debug log statements from task duplicate
Remove all log.Debugf calls from the Create method as they were
leftover development aids and should not be in production code.
2026-03-04 17:20:26 +01:00
kolaente 4d494ba442 test: add web integration tests for task duplication 2026-03-04 17:20:26 +01:00
kolaente 77fdf1b84b feat: register task duplicate API route 2026-03-04 17:20:26 +01:00
kolaente d8f3a96b06 feat: add task duplicate backend model and tests 2026-03-04 17:20:26 +01:00
kolaente cd7d40583a fix: only dump Vikunja-owned tables
Same fix as WipeEverything - use the registered table list instead
of x.DBMetas() to avoid including PostgreSQL extension tables in
database dumps.
2026-03-04 15:37:54 +01:00
kolaente 14e2c95a83 fix: only drop Vikunja-owned tables in WipeEverything
Previously WipeEverything used x.DBMetas() which returns all tables
in the database, including those owned by PostgreSQL extensions like
PostGIS. This caused restore to fail with 'cannot drop table
spatial_ref_sys because extension postgis requires it'.

Now uses the registered table list instead.

Fixes go-vikunja/vikunja#2219
2026-03-04 15:37:54 +01:00
kolaente 0a8534ded9 feat: add RegisteredTableNames helper to db package 2026-03-04 15:37:54 +01:00
kolaente 3dd2ba4aa4 feat: register Vikunja tables with db package at init 2026-03-04 15:37:54 +01:00
kolaente d26936f869 feat: add table registration to db package
Part of the fix for dump/restore failing when PostgreSQL extensions
add extra tables (e.g. PostGIS spatial_ref_sys).
2026-03-04 15:37:54 +01:00
kolaente 18f16878a8 fix: prevent nil pointer panic in mention notification listeners
When a task bucket is updated without changing buckets (early return in
updateTaskBucket), b.Task remains nil. The TaskUpdatedEvent was then
dispatched with a nil Task, causing a nil pointer dereference in
HandleTaskUpdatedMentions when accessing event.Task.Description.

This adds:
- A nil guard in TaskBucket.Update to skip event dispatch when b.Task is nil
- Nil checks in HandleTaskUpdatedMentions, HandleTaskCreateMentions,
  HandleTaskCommentEditMentions, and UpdateTaskInSavedFilterViews
- Tests verifying the handlers gracefully handle nil task events

Closes #2351
2026-03-04 10:29:16 +01:00
Tink 40bcf2b36f
fix: validate default settings timezone on startup (#2345) 2026-03-03 12:16:37 +00:00
kolaente f516bbe560 test: update event assertions to work with deferred dispatch
Tests that call model methods directly now call events.DispatchPending
before asserting event dispatch.

Refs #2315
2026-03-03 12:46:34 +01:00
kolaente 6ed684d708 fix(events): dispatch pending events in migration and export handlers
Refs #2315
2026-03-03 12:46:34 +01:00
kolaente 924eef58d1 fix(events): dispatch pending events in CalDAV handlers after commit
CalDAV handlers manage their own database sessions. Now that model
methods use DispatchOnCommit, the CalDAV handlers must call
DispatchPending after commit and CleanupPending on rollback.

Refs #2315
2026-03-03 12:46:34 +01:00
kolaente 1f363dbd43 fix(events): defer event dispatch for user creation and task positions
Refs #2315
2026-03-03 12:46:34 +01:00
kolaente 8afbdf2deb fix(events): defer event dispatch for team operations
Convert events.Dispatch to events.DispatchOnCommit in team and team
member CRUD. Also removes premature s.Commit() from TeamMember.Delete
since the handler manages the transaction lifecycle.

Refs #2315
2026-03-03 12:46:34 +01:00
kolaente dd7f7de518 fix(events): defer event dispatch for project operations
Convert events.Dispatch to events.DispatchOnCommit in project CRUD,
project-user sharing, and project-team sharing.

Refs #2315
2026-03-03 12:46:34 +01:00
kolaente fe459c9297 fix(events): defer event dispatch for task sub-entities
Convert events.Dispatch to events.DispatchOnCommit in task assignees,
comments, attachments, relations, and kanban bucket operations.

Refs #2315
2026-03-03 12:46:34 +01:00
kolaente 3eb289262f fix(events): defer task event dispatch until after transaction commit
Convert events.Dispatch to events.DispatchOnCommit in Task.Create,
updateSingleTask, Task.Delete, and triggerTaskUpdatedEventForTaskID.
Events are now dispatched by the handler after s.Commit(), ensuring
webhook listeners see committed data.

Fixes #2315
2026-03-03 12:46:34 +01:00
kolaente 217a481162 feat(handlers): dispatch pending events after transaction commit
All generic CRUD handlers now call events.DispatchPending(s) after
s.Commit() and events.CleanupPending(s) on rollback paths. This is
preparation for switching model methods from events.Dispatch to
events.DispatchOnCommit.

Refs #2315
2026-03-03 12:46:34 +01:00
kolaente 564573bdd5 feat(events): add DispatchOnCommit/DispatchPending for deferred event dispatch
Events dispatched inside model methods run before the transaction commits,
causing listeners (especially webhooks) that open new sessions to read
stale data. These new functions allow accumulating events during a
transaction and dispatching them only after commit.

Refs #2315
2026-03-03 12:46:34 +01:00
Dominik Pschenitschni d94429d33c fix(caldav): parse timestamps in configured timezone 2026-03-03 12:18:48 +01:00
kolaente 51f789bf5c fix(e2e): drain event handlers and stop browser between tests
Async event handlers (via Watermill) from the previous test can hold
SQLite connections, starving the next test's fixture setup PATCH request.

Three changes fix this:

1. Track in-flight event handler goroutines with a WaitGroup.
2. Call WaitForPendingHandlers() in the test endpoint before
   truncating/inserting data.
3. Navigate the browser to about:blank in fixture teardown to stop
   notification polling and other frontend requests between tests.
2026-03-03 10:41:19 +01:00
kolaente 39acdac531 fix(caldav): eliminate nested db session in CalDAV auth
checkUserCaldavTokens called user.GetCaldavTokens which creates its own
db.NewSession(), while the caller (BasicAuth) already holds an open
session. With SQLite this caused a deadlock because the second session
blocks on the write lock held by the first session in the same goroutine.

Add GetCaldavTokensWithSession that accepts an existing session and use
it from checkUserCaldavTokens.
2026-03-03 10:41:19 +01:00
kolaente 530973c475 fix(auth): make SameSite=None conditional on HTTPS for refresh cookie
SameSite=None requires Secure=true per browser spec. When running over
plain HTTP (local dev, e2e tests), browsers reject or downgrade the
cookie, breaking session refresh. Fall back to SameSite=Lax for HTTP
while keeping SameSite=None for HTTPS (needed for the Electron desktop
app cross-origin scenario).
2026-03-03 10:41:19 +01:00
kolaente 98f2893ffe fix(db): use WAL mode for SQLite and temp file for ephemeral databases
Three SQLite connection issues are fixed:

1. The refactoring in 26c0f71 accidentally dropped _busy_timeout from
   the file-based SQLite connection string. Without it, concurrent
   transactions get instant SQLITE_BUSY errors instead of waiting.

2. _txlock=immediate forced ALL transactions (including reads) to
   acquire the write lock at BEGIN, serializing all database access.
   WAL mode makes this unnecessary: readers use snapshots and never
   block writers, so the SHARED-to-RESERVED deadlock cannot occur.

3. In-memory shared cache (file::memory:?cache=shared) uses table-level
   locking where _busy_timeout is ineffective (returns SQLITE_LOCKED,
   not SQLITE_BUSY) and concurrent connections deadlock. Replace with a
   temp file using WAL mode for proper concurrency.
2026-03-03 10:41:19 +01:00
kolaente 26c0f71b6c
fix(db): use immediate txlock for SQLite instead of MaxOpenConns(1)
MaxOpenConns(1) caused Go-level deadlocks: when two goroutines needed
database connections concurrently, the second blocked forever waiting
for the single connection pool slot. This broke CI (sqlite web tests
timed out after 45min, e2e tests hung).

The actual "database is locked" errors were caused by SQLite's default
deferred transaction locking: two connections both acquire SHARED locks,
then deadlock when both try to promote to RESERVED for writing. SQLite
detects this instantly and returns SQLITE_BUSY, bypassing busy_timeout.

_txlock=immediate fixes this by acquiring the write lock at BEGIN time.
The second concurrent transaction waits (up to busy_timeout) instead of
deadlocking. Combined with WAL mode (concurrent readers + single writer),
this handles concurrency correctly without restricting the Go connection
pool.
2026-03-02 14:03:33 +01:00
kolaente 28f98a7a96
fix(auth): use SameSite=None for refresh token cookie to fix desktop app
SameSite=Strict prevents the browser from sending the HttpOnly refresh
token cookie in cross-origin contexts like the Electron desktop app,
where the page runs on localhost but the API is on a remote host. This
caused sessions to expire quickly because refresh requests never
included the cookie.

SameSite=None allows cross-origin sending while HttpOnly still prevents
JavaScript from reading the cookie value (XSS protection).

Resolves #2309
2026-03-02 13:54:10 +01:00
kolaente 79dbb40985
fix(db): prevent SQLite "database is locked" errors under concurrent writes
Configure SQLite connections with WAL journal mode, a 5-second busy
timeout, shared cache, and a max of 1 open connection. SQLite only
supports a single writer at a time, so without these settings concurrent
API requests (e.g. bulk task creation) would immediately fail with
"database is locked" instead of waiting and retrying.
2026-03-02 12:37:55 +01:00
Weijie Zhao 3ca4913fcb
fix: use MinPositionSpacing threshold in calculateNewPositionForTask (#2320)
calculateNewPositionForTask only checked for lowestPosition == 0 before
triggering a full position recalculation. Extremely small position
values (e.g. 3.16e-285) passed this check, causing new tasks to get
meaningless positions via the index * 2^16 fallback, breaking sort
order.

Use the same < MinPositionSpacing threshold that
createPositionsForTasksInView and the position update handler already
use.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-02 09:14:38 +01:00
kolaente 23d84e7811
fix(views): assign default position when creating new project views
When creating a new view without specifying a position, it defaulted to
0, causing it to always sort before all other views. Apply
calculateDefaultPosition to assign a unique position based on the view
ID, consistent with how projects, tasks, and buckets handle this.

Fixes go-vikunja/vikunja#2319
2026-03-02 08:35:35 +01:00
kolaente a7e4a4f4af
fix(migration): support space-separated date format in TickTick importer
Fixes https://github.com/go-vikunja/vikunja/issues/2324
2026-03-02 08:35:35 +01:00
kolaente 412215ee2f fix(auth): correctly delete older password reset tokens in cron 2026-02-27 14:44:26 +01:00
kolaente 5c2195f9fc fix(auth): remove password reset token after use 2026-02-27 14:44:26 +01:00
kolaente 3d5ad73d4f
fix(filter): recover from datemath panic on malformed date filter values
The go-datemath lexer panics with "scanner internal error" when given
certain malformed inputs like "no" (it starts recognizing "now" but
hits EOF). Wrap datemath.Parse in a recover so the panic becomes a
regular error, allowing the fallback date parser to handle it gracefully.

Closes go-vikunja/vikunja#2307
2026-02-26 16:09:13 +01:00
Frederick [Bot] 599d05dd5e [skip ci] Updated swagger docs 2026-02-25 13:05:32 +00:00
kolaente d1e1cb3b4f test(api): add tests for password validation in reset and update flows
- Add httpCodeGetter interface to handle ValidationHTTPError in test helper
- Add test case for password too short in password reset
- Add test case for password too short in password update
- Fix existing test data to use valid 8+ char passwords
2026-02-25 13:44:56 +01:00
kolaente 89c17d3b23 feat(api): enforce password validation on reset and update flows
Add bcrypt_password validation to password reset and update endpoints:
- Add validation tag to PasswordReset.NewPassword struct field
- Add validation tag to UserPassword.NewPassword struct field
- Add c.Validate() calls in both handlers
- Fix off-by-one error in bcrypt_password validator (use <= 72 not < 72)

Password requirements: min 8 chars, max 72 bytes (bcrypt limit)
2026-02-25 13:44:56 +01:00
kolaente 39da47e435 fix: detect and fail on oversized zip entries instead of silent truncation
Replace io.LimitReader with a new readZipEntry helper that reads one extra
byte to detect when content exceeds maxZipEntrySize (500MB). This prevents
silent data corruption where partial file bytes would be stored as if the
upload succeeded.

The import now fails with ErrFileTooLarge instead of accepting truncated
content for attachments and background blobs.
2026-02-25 13:01:00 +01:00
kolaente db4fa9a4b6 fix(restore): extract preValidateTableData to reduce cyclomatic complexity 2026-02-25 13:01:00 +01:00
kolaente f3ac0574c0 fix(auth): use checked type assertions for all JWT claims 2026-02-25 13:01:00 +01:00
kolaente 1b3d8dc59c fix(restore): pre-validate all table data JSON before wiping database 2026-02-25 13:01:00 +01:00
kolaente 9fd5b62fde fix(restore): limit zip entry read size to prevent decompression bombs 2026-02-25 13:01:00 +01:00
kolaente 329c07f24b fix(attachments): use mime.FormatMediaType for Content-Disposition header 2026-02-25 13:01:00 +01:00
kolaente 7e7e778d49 fix(db): validate table names and quote identifiers in raw SQL 2026-02-25 13:01:00 +01:00
kolaente 9d19a04550 fix(migration): use checked type assertion for background file id 2026-02-25 13:01:00 +01:00
kolaente fc5ab844de fix(migration): limit zip entry read size to prevent decompression bombs 2026-02-25 13:01:00 +01:00
kolaente 6815cdbda4 fix(migration): reject zip entries with path traversal in vikunja-file import 2026-02-25 13:01:00 +01:00
kolaente bbe1a2bbd0 refactor(utils): extract ContainsPathTraversal to shared utils package 2026-02-25 13:01:00 +01:00
kolaente c2cf5ba1c5 fix(restore): validate migration data before wiping database
Move archive validation (migration file existence and slice bounds
check) before the database wipe. Previously a malformed archive
would first destroy the database and then panic, leaving the
instance in an irrecoverable state with total data loss.

Now the migration data is fully parsed and validated before any
destructive operations occur.
2026-02-25 13:01:00 +01:00
kolaente 3c0ea7099e fix(restore): validate database file names in zip archive
Check that database entries in the zip have a .json suffix and a
non-empty base name before slicing the extension off. This prevents
a panic from index-out-of-range when the filename is too short.
Also use TrimPrefix instead of ReplaceAll for correctness.
2026-02-25 13:01:00 +01:00
kolaente 7971500467 fix(restore): sanitize config file path to prevent zip slip
Use filepath.Base() on the config file name from the zip archive
before passing it to os.OpenFile, ensuring the config file is
always written to the current directory regardless of what path
the zip entry claims to have.
2026-02-25 13:01:00 +01:00
kolaente 12dca5f0b0 fix(restore): reject zip entries with path traversal sequences
Validate all zip entry names during restore to reject entries
containing directory traversal sequences (e.g. ../../../pwned.txt).
This prevents a Zip Slip attack where a malicious archive could
write files outside the intended extraction directory.
2026-02-25 13:01:00 +01:00
kolaente b6155d525c
feat(cli): reorganize repair commands under unified 'vikunja repair' parent (#2300)
Consolidate four scattered repair/maintenance CLI commands into a unified `vikunja repair` parent command with subcommands.
2026-02-25 11:50:09 +00:00
kolaente a5b1a90c42 refactor: remove typesense support
Typesense was an optional external search backend. This commit fully
removes the integration, leaving the database searcher as the only
search implementation.

Changes:
- Delete pkg/models/typesense.go (core integration)
- Delete pkg/cmd/index.go (CLI command for indexing)
- Simplify task search to always use database searcher
- Remove Typesense event listeners for task sync
- Remove TypesenseSync model registration
- Remove Typesense config keys and defaults
- Remove Typesense doctor health check
- Remove Typesense initialization from startup
- Clean up benchmark test
- Add migration to drop typesense_sync table
- Remove golangci-lint suppression for typesense.go
- Remove typesense-go dependency
2026-02-25 12:15:28 +01:00
kolaente 71657fce30 feat: add repair-projects CLI command 2026-02-25 11:56:25 +01:00
kolaente ad307a3499 feat: add RepairOrphanedProjects function 2026-02-25 11:56:25 +01:00
kolaente 963235c0ce test: add failing tests for RepairOrphanedProjects 2026-02-25 11:56:25 +01:00
kolaente 9e050fe40e test: add orphaned project fixture for repair-projects command 2026-02-25 11:56:25 +01:00
kolaente 107a92f573 fix: commit transaction in session cleanup cron
RegisterSessionCleanupCron opens a transaction via db.NewSession() but
never calls s.Commit(). The deferred s.Close() auto-rolls-back, making
the DELETE a no-op. Add the missing commit.
2026-02-25 11:03:02 +01:00
kolaente 2f680d041c fix: address review comments on session lifecycle
- user_export.go: Remove defer s.Close() from checkExportRequest since
  it returns the session to callers. Callers now own the session
  lifecycle with their own defer s.Close(). Close session on all error
  paths within checkExportRequest.

- user_delete.go: Close the read session immediately after Find() before
  the per-user deletion loop, avoiding a long-lived transaction holding
  locks unnecessarily.

- user/delete.go: Remove double s.Close() in notifyUsersScheduledForDeletion
  by closing immediately after Find() instead of using both defer and
  explicit close.

- caldav_token.go: Return nil token on Commit() error to prevent callers
  from using an unpersisted token.
2026-02-25 11:03:02 +01:00
kolaente b3d8a56364 fix: use caller's session in LDAP syncUserGroups to avoid nested transactions
syncUserGroups created its own db.NewSession() internally while being
called from AuthenticateUserInLDAP which already has an active session
with writes. In SQLite shared-cache mode this causes a lock conflict.

Pass the caller's session through instead, and add s.Commit() before
db.AssertExists calls in LDAP tests.
2026-02-25 11:03:02 +01:00
kolaente 2f718206f9 fix: add TestMain to caldav tests and fix session conflicts
Add a proper main_test.go for the caldav test package that initializes
the logger, config, test database, and event system. Previously, these
were initialized inline in TestSubTask_Create and TestSubTask_Update
relied on running after it (fragile test ordering).

Fix session handling in TestSubTask_Update: close the read session
before calling UpdateResource (which creates its own internal session)
to avoid SQLite lock conflicts from concurrent transactions.
2026-02-25 11:03:02 +01:00
kolaente a7086e5e49 fix: prevent session leaks and visibility issues in model tests
Two categories of fixes:

1. Use defer s.Close() instead of explicit s.Close() to prevent session
   leaks when require.FailNow() triggers runtime.Goexit(), which skips
   explicit close calls but runs deferred functions. Leaked sessions
   hold SQLite write locks that block all subsequent fixture loading.

2. Add s.Commit() before db.AssertExists/db.AssertMissing calls. These
   assertion helpers query via the global engine (not the test session),
   so they cannot see uncommitted data from the session's transaction.

For block-scoped sessions (kanban_task_bucket_test.go), wrap each block
in an anonymous function so defer runs at block boundary rather than
deferring to the enclosing test function.
2026-02-25 11:03:02 +01:00
kolaente 2a10b22c5c fix: use session-aware file creation to avoid nested transactions
files.Create() and files.CreateWithMime() internally create their own
sessions and transactions. When called from within an existing
transaction (now that db.NewSession() auto-begins), this creates nested
transactions that deadlock on SQLite.

Switch to files.CreateWithSession() and files.CreateWithMimeAndSession()
to participate in the caller's existing transaction instead.
2026-02-25 11:03:02 +01:00
kolaente cbfd0e63ed fix: pass pointer to xorm Update to avoid hash panic in transaction mode
In transaction mode, xorm stores the bean argument as a map key in
afterUpdateBeans. Since Task contains slices and maps (unhashable
types), passing a Task value causes "hash of unhashable type" panic.
Passing a pointer (&ot) fixes this since pointers are always hashable.
2026-02-25 11:03:02 +01:00
kolaente 2188c7a79d fix: add missing Commit() to event listeners and cron jobs
With db.NewSession() now starting real transactions, all sessions that
do writes must explicitly commit. These listeners and cron jobs were
previously relying on auto-commit mode where each SQL statement was
committed immediately. Without explicit Commit(), the writes are
silently rolled back on Close(), and the held write locks cause
"database is locked" errors for subsequent requests on SQLite.
2026-02-25 11:03:02 +01:00
kolaente eea59c33c7 fix: isolate deletion notifications into per-user transactions
On Postgres, a failed operation puts the transaction in an error state
where subsequent operations fail. The previous loop with continue would
keep trying to use a broken transaction. Each user now gets its own
transaction so a single notification failure doesn't affect others.
2026-02-25 11:03:02 +01:00
kolaente 312648d7d6 fix: remove transaction control from File.Delete to prevent premature commit/rollback
File.Delete() had s.Commit() and s.Rollback() calls that could
prematurely commit or abort an outer transaction when using a shared
session. The caller is now responsible for transaction management.
2026-02-25 11:03:02 +01:00
kolaente 1167b08e70 fix: handle Begin() error in db.NewSession() instead of ignoring it 2026-02-25 11:03:02 +01:00
kolaente 23176bb8e1 test: add regression test for atomic parent project deletion
Verify that deleting a parent project atomically deletes all child
projects, including archived children and deeply nested hierarchies.
Also add missing defer s.Close() to existing delete test cases.
2026-02-25 11:03:02 +01:00
kolaente 49bba7f830 fix: eliminate nested database sessions to prevent table locks
Refactor functions that created their own sessions when called from
within existing transactions, which caused "database table is locked"
errors in SQLite's shared-cache mode.

Changes:
- Add files.CreateWithSession() to reuse caller's session
- Refactor DeleteBackgroundFileIfExists() to accept session parameter
- Add variadic session parameter to notifications.Notify() and
  Notifiable.ShouldNotify() interface
- Update all Notify callers (~17 sites) to pass their session through
- Use files.CreateWithSession in SaveBackgroundFile and NewAttachment
- Fix test code to commit sessions before assertions
2026-02-25 11:03:02 +01:00
kolaente a6e6f252db refactor: remove redundant Begin() calls after NewSession auto-begins
Since NewSession() now auto-begins a transaction, explicit Begin()
calls are redundant (xorm's Begin() is a no-op when already in a
transaction). Removing them reduces confusion.

Special case: user_delete.go's loop previously called Begin/Commit
per user on a shared session. Restructured to create a new session
per user deletion so each gets its own transaction.
2026-02-25 11:03:02 +01:00
kolaente 764d3569ce fix: close leaked database sessions
Add defer s.Close() to sessions that were never closed:
- auth.GetAuthFromClaims inline session
- models.deleteUsers cron function
- notifications.notify database insert
2026-02-25 11:03:02 +01:00
kolaente c9c250fb1c fix: add missing Commit() to write callers
After NewSession() auto-begins a transaction, callers that perform
writes must explicitly call Commit() for changes to persist. Without
this, writes are silently rolled back when Close() is called.

Affected callers:
- user deletion notification cron
- caldav token generation/deletion
- token cleanup cron
- mark-all-notifications-read endpoint
- saved filter view cron
- project background delete
- typesense reindex
- export cleanup cron
- task last-updated listener
- saved filter view listener
- SSO team cleanup cron
- migration status start/finish
- background set/remove handlers
- orphaned task position cleanup
- file creation
2026-02-25 11:03:02 +01:00
kolaente fd77e041a1 fix: add transaction begin to db.NewSession()
All sessions now start with an active transaction. This makes
multi-statement write operations atomic — if any step fails, all
changes are rolled back instead of leaving the database in an
inconsistent state.

Callers must call s.Commit() for writes to persist. s.Close()
auto-rollbacks uncommitted transactions.
2026-02-25 11:03:02 +01:00
Frederick [Bot] 2cb0b84602 [skip ci] Updated swagger docs 2026-02-25 09:39:04 +00:00
kolaente 2ef693a7cf test: add session lifecycle tests
Integration tests covering session creation on login, refresh token
rotation, session listing, deletion, and session invalidation on
password change.
2026-02-25 10:30:25 +01:00
kolaente 8ee069a2a3 feat: add session-based auth with refresh token rotation
- Login creates a server-side session and sets an HttpOnly refresh
  token cookie alongside the short-lived JWT
- POST /user/token/refresh exchanges the cookie for a new JWT and
  rotates the refresh token atomically
- POST /user/logout destroys the session and clears the cookie
- POST /user/token restricted to link share tokens only
- Session list (GET) and delete (DELETE) routes for /user/sessions
- All user sessions invalidated on password change and reset
- CORS configured to allow credentials for cross-origin cookies
- JWT 401 responses use structured error code 11 for client detection
- Refresh token cookie name constants annotated for gosec G101
2026-02-25 10:30:25 +01:00
kolaente b3d0b2f697 feat: add Session model with CRUD, permissions, and cleanup cron
- Session struct with UUID primary key, hashed refresh token, device
  info, IP address, and last-active tracking
- Token generation via generateHashedToken (SHA-256, 128 random bytes)
- CreateSession, GetSessionByRefreshToken, GetSessionByID
- Atomic RotateRefreshToken with WHERE on old hash to prevent replays
- ReadAll scoped to authenticated user (link shares rejected)
- Delete scoped to owning user (link shares rejected)
- Hourly cleanup cron for expired sessions based on is_long_session
- ErrSessionNotFound error type with HTTP 404 mapping
2026-02-25 10:30:25 +01:00
kolaente a6bdeb67b0 feat: add jwtttlshort config key for session tokens
Adds ServiceJWTTTLShort (default 600s) to control the lifetime of
short-lived JWTs issued during token refresh. The existing jwtttl
and jwtttllong keys remain for session expiry and long sessions.
2026-02-25 10:30:25 +01:00
kolaente 04e60472b7 feat: add sessions table migration
Adds the `sessions` table for storing server-side session records.
Each row tracks a session ID (UUID), user ID, hashed refresh token,
device info, IP address, and timestamps.
2026-02-25 10:30:25 +01:00
kolaente 6de82db7e4 fix: decouple webhook dispatch from email/mailer config
The reminder and overdue crons now always run and dispatch webhook
events. Email notifications are only sent when both
ServiceEnableEmailReminders and MailerEnabled are true. Webhook
dispatch errors are logged but no longer abort the cron run.
2026-02-24 20:24:56 +01:00
kolaente 54aacd3707 feat: dispatch TaskOverdueEvent from overdue cron 2026-02-24 20:24:56 +01:00
kolaente 626e731ae4 feat: dispatch TaskReminderFiredEvent from reminder cron 2026-02-24 20:24:56 +01:00
kolaente 83dc7537c4 feat: register reminder and overdue events for webhooks 2026-02-24 20:24:56 +01:00
kolaente e04c1a3d2e feat: add TaskReminderFiredEvent and TaskOverdueEvent types 2026-02-24 20:24:56 +01:00
kolaente 0c7c07b3b8 fix: preserve teams external_id type when renaming on mysql
Avoid using the generic renameColumn helper for this migration on MySQL because it renames columns as BIGINT. Handle the teams oidc_id -> external_id rename with a MySQL-specific CHANGE statement that keeps VARCHAR(250) and remains idempotent.
2026-02-24 14:29:49 +01:00
kolaente 3d6c527b64 fix: cast bucket_configuration to text in postgres catchup query
The new catchup migration compared the postgres JSON column with string literals using !=, which fails in migration smoke tests. Use a PostgreSQL-specific WHERE clause with ::text and cover it with unit tests.
2026-02-24 14:29:49 +01:00
kolaente 99ac3e65b8 fix: add comprehensive catchup for bucket and filter format migrations
Convert bucket_configuration filters regardless of bucket_configuration_mode and catch remaining view-level filter values still stored in the legacy string format. Includes focused tests for the bucket conversion helper logic.

Fixes #2172

Fixes #2188

Fixes #2202
2026-02-24 14:29:49 +01:00
kolaente 4acad97688 fix: make teams oidc_id rename migration idempotent
Use the shared renameColumn helper for non-SQLite databases and skip the SQLite table rebuild when oidc_id is already absent. This prevents failures after partial upgrades where the column was already renamed.

Refs #2172

Refs #2285
2026-02-24 14:29:49 +01:00
kolaente b1534f1cc8 fix: replace tx.Sync() with explicit ALTER TABLE in webhooks migration
tx.Sync() fails on PostgreSQL because it tries to reconcile primary key constraints/indexes, causing index-drop errors. Use explicit ALTER TABLE ADD COLUMN with existence checks instead.

Fixes #2215
2026-02-24 14:29:49 +01:00
kolaente a13ecbd3cc
fix: prevent browser from caching API responses
Without explicit Cache-Control headers, browsers may heuristically cache
API JSON responses. This causes stale data to be served on normal page
refresh (F5) — for example, projects newly shared with a team not
appearing until the user performs a hard refresh (Ctrl+Shift+R).

Add Cache-Control: no-store to all API responses via middleware and
configure the service worker's NetworkOnly strategy to explicitly bypass
the browser HTTP cache for API requests.

Ref: https://community.vikunja.io/t/team-members-cannot-see-project/1876
2026-02-24 10:37:49 +01:00
kolaente 249b651692
fix: treat archived TickTick tasks as done during import
TickTick uses status "2" (Archived) for completed tasks that were
subsequently archived. The import only checked for status "1"
(Completed), causing archived tasks to be imported as open despite
having a completion timestamp.

Closes go-vikunja/vikunja#2278
2026-02-23 14:52:20 +01:00
kolaente 1ccc8dce3a
fix: load file content before generating attachment preview
LoadFileByID() was called after the preview branch, so GetPreview()
received a nil io.Reader causing a panic in image.Decode.
2026-02-22 09:28:25 +01:00
kolaente d222d4502a fix: escape attachment download filename 2026-02-22 00:00:11 +01:00
kolaente c6370bb739 fix: fall back to application/octet-stream when the file has no mime type stored 2026-02-22 00:00:11 +01:00
kolaente 55c122fb42 feat: add repair-file-mime-types CLI command
Add a CLI command that finds all files in the database with no MIME type
set, detects it from the stored file content, and updates the database.
This is useful for backfilling MIME types on files created before MIME
detection was added on upload.
2026-02-22 00:00:11 +01:00
kolaente 4915f535d0 fix: add Content-Disposition attachment header to task attachment downloads
Set Content-Disposition to "attachment" instead of "inline" so browsers
trigger a file download with the correct filename. Also move shared
response headers (Content-Type, Content-Length, Last-Modified) out of
the S3-specific branch so they apply to both storage backends.
2026-02-22 00:00:11 +01:00
kolaente 519f66a51f fix: detect and store mime type when creating file attachments
Use mimetype.DetectReader in files.Create to automatically detect and
store the MIME type from file content. This fixes task attachment
previews, S3 Content-Type headers, and UI display for newly uploaded
files.
2026-02-22 00:00:11 +01:00
Frederick [Bot] 05ff67b681 chore(i18n): update translations via Crowdin 2026-02-21 01:09:54 +00:00
Frederick [Bot] 3ce8c5bdda [skip ci] Updated swagger docs 2026-02-19 13:35:37 +00:00
kolaente c12bbbe93e feat(comments): support order_by query parameter in comments API
Add an OrderBy field to the TaskComment struct with a query:"order_by"
tag so that the frontend can request ascending or descending comment
order. The value is validated to only accept "asc" or "desc", defaulting
to "asc". Also adds the corresponding swagger @Param annotation.
2026-02-19 14:20:52 +01:00
kolaente 0026c74fb5 fix(tests): properly assert sort order including task47 in web tests
Restore full sort-order assertions that verify task47 appears at the
end of priority-sorted results. The previous fix incorrectly removed
the trailing `]` which meant the tests no longer verified the last
element in the sorted array.
2026-02-19 12:40:29 +01:00
kolaente c3e223887d fix(tests): update web test assertions for new task47 fixture
Remove trailing `]` from JSON substring assertions in priority sort
tests since task47 now appears after the previously-last task in the
sort order.
2026-02-19 12:40:29 +01:00
kolaente 1943d6993c fix: only merge range comparators in sub-table filter grouping
Restrict the AND-joined sub-table filter merging to range comparators
(>, >=, <, <=) only. Equality and negative comparators (=, !=, in,
not in) must remain as separate EXISTS/NOT EXISTS subqueries because
each matching value lives in its own row.

Merging equality filters like `labels = 4 && labels = 5` into a single
EXISTS would produce an unsatisfiable condition (no single row has
label_id=4 AND label_id=5). Merging negative filters like
`labels != 4 && labels != 5` into NOT EXISTS(label_id IN 4 AND
label_id IN 5) would be trivially true.

Also fix the join tracking to use the first filter's join type
(how the group connects to the previous element) instead of the last.
2026-02-19 12:40:29 +01:00
kolaente 302b58dac0 style: fix alignment in test case 2026-02-19 12:40:29 +01:00
kolaente a93f6bf160 test: add OR-joined reminder filter regression test
Verify that OR-joined conditions on the same sub-table still produce
separate EXISTS subqueries and match independently, as expected.
2026-02-19 12:40:29 +01:00
kolaente d1901f46c3 test: update expected task index after adding task #47 fixture
Task #47 in project 1 has index 32, so the next auto-assigned index
is now 33 instead of 18.
2026-02-19 12:40:29 +01:00
kolaente c034e431cb fix: merge AND-joined sub-table filters into single EXISTS subquery
When multiple AND-joined filter conditions target the same sub-table
(e.g., reminders > X && reminders < Y), they are now combined into
a single EXISTS subquery so that all conditions must be satisfied by
the same row. Previously, each condition generated a separate EXISTS
subquery that could match different rows, causing false positives.

Fixes #2245
2026-02-19 12:40:29 +01:00
kolaente cd72231502 test: add failing test for sub-table filter multi-row matching bug #2245
Add task47 variable (with reminders straddling the test window) and new
test cases that verify AND-joined sub-table filters match the same row.
The test "filtered reminder dates should not match task with reminders
outside window" will fail until the fix is applied.
2026-02-19 12:40:29 +01:00
kolaente 6733ac4e22 test: add task #47 with reminders outside window for bug #2245
Add a second reminder to task 2 (in 2019, outside the test window)
and create task #47 with two reminders that straddle the test window
(2018-08-01 and 2019-03-01) but neither falls inside it. This exposes
the multi-row matching bug where separate EXISTS subqueries can match
different rows in the same sub-table.
2026-02-19 12:40:29 +01:00
Mattia Maglie 8779a28d1d
fix: prevent duplicated sql condition in filters (#1546)
Proposing the fix as in #1545

Co-authored-by: mattia.maglie <mattia.maglie@alispa.com>
Co-authored-by: kolaente <k@knt.li>
2026-02-18 17:02:25 +01:00
Quiwy 6dbc108be8
feat(auth): allow LDAP authentication with anonymous bind (#2226)
As discussed on Matrix, Vikunja currently prevents users from using LDAP
authentication if the server allows anonymous binds (common in local
environments like YunoHost). The application would previously trigger a
`log.Fatal` if `AuthLdapBindDN` or `AuthLdapBindPassword` were left
empty in the configuration.

#### **How this fixes the problem:**

* **Validation:** Removed the strict requirement for Bind credentials in
`InitializeLDAPConnection`.
* **Connection Logic:** Updated `ConnectAndBindToLDAPDirectory` to
attempt an `UnauthenticatedBind` from the `go-ldap` library when no
credentials are provided.
* **Safety:** If a Bind DN is provided, the behavior remains unchanged
(authenticated bind).

#### **Testing:**

* Tested manually on a **YunoHost** instance by replacing the binary.
* Confirmed that Vikunja now successfully starts and authenticates users
via the local LDAP (localhost) without requiring a service account.
* Added a basic unit test in `pkg/modules/auth/ldap/ldap_test.go` to
ensure the initialization logic doesn't crash with empty credentials.

*Note: This is my first contribution to a Go project (assisted by an LLM
for syntax). Feedback on code style is more than welcome!*
2026-02-17 22:24:35 +01:00
John Starich b2715bb56d refactor: use Go idioms for running tests 2026-02-17 18:01:05 +01:00
John Starich 591a646f84 refactor: remove environment variable requirements for go test 2026-02-17 18:01:05 +01:00
Micah 31da3c4533
fix(migration): make migration from Microsoft Todo work for those with previously migrated wunderlist accounts (#2126) 2026-02-17 16:54:03 +01:00