Commit Graph

2249 Commits

Author SHA1 Message Date
kolaente 6ca0151d02 test(webtests): add end-to-end TOTP lockout test
Drives the login endpoint through 11 failed TOTP attempts against user10
and asserts the account ends up locked in the database, then verifies a
subsequent login with a valid TOTP code is rejected with
ErrCodeAccountLocked. Exercises the GHSA-fgfv-pv97-6cmj regression
against the real handler path.
2026-04-09 16:08:26 +00:00
kolaente 75629158cb test(user): cover TOTP lockout persistence and password-reset unlock
Verifies that HandleFailedTOTPAuth locks the account after 10 rolled-back
caller sessions (the regression from GHSA-fgfv-pv97-6cmj), and that the
persisted password reset token can unlock the account via ResetPassword.
2026-04-09 16:08:26 +00:00
kolaente d435c50df3 fix(security): persist TOTP lockout across login rollback
The failed-TOTP handler shared the login request's xorm session, and the
login handler rolled that session back after a failed login. The status
change to StatusAccountLocked was silently discarded, so the account was
never locked regardless of how many failed TOTP attempts arrived.

HandleFailedTOTPAuth now opens its own session and commits independently
of the caller. The login handler rolls back its session before invoking
the handler so the lockout write can acquire a write lock on SQLite
shared-cache.

Also handles the Redis keyvalue backend returning the attempt counter as
a string instead of int64, which would have prevented the lockout path
from ever running on Redis.

See GHSA-fgfv-pv97-6cmj.
2026-04-09 16:08:26 +00:00
kolaente 6df0d6c8f5 feat(tasks): cap repeat_after at 10 years to harden repeating-task handler
Add MaxTaskRepeatAfterSeconds (10 years in seconds) and reject any
create/update that tries to store a value outside [0, cap] with a new
ErrInvalidTaskRepeatInterval (error code 4029). Defense-in-depth
alongside the arithmetic fix in addRepeatIntervalToTime: keeps stored
values well away from int64 overflow and bounds the range of inputs
a future refactor could trip over.
2026-04-09 16:07:48 +00:00
kolaente 3c3d4b863d test(tasks): add DoS regression test for ancient repeating due dates
Exercises updateDone end-to-end with a 1900-01-01 due date, 1-second
interval, and asserts the call completes in well under a second.
Catches any regression that reintroduces the O(n) loop in
addRepeatIntervalToTime (GHSA-r4fg-73rc-hhh7).
2026-04-09 16:07:48 +00:00
kolaente 9dc3d7eb4f fix(tasks): replace O(n) loop in repeating-task handler with arithmetic
addRepeatIntervalToTime used to advance t by whole intervals via an
unbounded loop. A repeating task with an ancient due_date and a
one-second interval required billions of iterations per task update,
turning completion of such a task into a trivial denial-of-service
(GHSA-r4fg-73rc-hhh7). Compute the number of intervals directly, with
guards for zero/negative durations, saturated time.Sub, and int64
overflow.

Covered by TestAddRepeatIntervalToTime, including the 1900-01-01 PoC
case.
2026-04-09 16:07:48 +00:00
kolaente 879462d717 fix(caldav): enforce URL project match in GetResourcesByList
Multiget REPORT requests would happily return tasks from projects
different from the one in the href, even though GetTasksByUIDs now
filters by access. Drop any returned task whose real project_id does
not match the project ID parsed from the href path segment.

Hardening for GHSA-48ch-p4gq-x46x.
2026-04-09 16:07:32 +00:00
kolaente 200b787c16 fix(caldav): reject GetResource when URL project mismatches task project
Even with the GetTasksByUIDs authz filter in place, a user with access
to multiple projects could read a task from project B by requesting it
under project A's URL. Enforce that the task's real project_id matches
the project ID parsed from the CalDAV URL path and 404 otherwise.

Adjusts the Delete Subtask test to use the correct URL project for
uid-caldav-test-child-task-2 (which lives in project 38, not 36);
the previous URL only worked because of the authz gap being closed.

Hardening for GHSA-48ch-p4gq-x46x.
2026-04-09 16:07:32 +00:00
kolaente f1e12c6f64 fix(caldav): enforce task read authorization on GetTasksByUIDs
Previously GetTasksByUIDs returned any task matching the UID regardless
of the caller's access, letting any authenticated CalDAV user read any
task by guessing or knowing a UID. Filter by accessible project IDs at
the SQL level using the existing accessibleProjectIDsSubquery helper.

Fixes GHSA-48ch-p4gq-x46x.
2026-04-09 16:07:32 +00:00
kolaente 0f3730d045 fix(notifications): escape markdown in user-controlled strings in email lines
Task titles, project titles, team names, doer/assignee names, and API
token titles were interpolated raw into Line(...) calls whose content is
rendered to HTML by goldmark and then sanitized with bluemonday UGCPolicy.
UGCPolicy intentionally allows safe <a href> and <img src> with
http/https URLs, so a title containing Markdown link or image syntax
would survive sanitization as a working phishing link or tracking pixel
in a legitimate Vikunja email.

Introduce notifications.EscapeMarkdown, which prefixes every CommonMark
§2.4 backslash-escapable ASCII punctuation character — including '<' so
autolinks like `<https://evil.com>` are neutralized before reaching
goldmark — with a backslash. Apply it to every user-controlled argument
of every Line(...) call in pkg/models that feeds into an i18n template,
and to the hand-built "* [title](url) (project)" Markdown link in the
overdue-tasks digest notification.

Also escape the migration error string in MigrationFailedNotification,
an additional sink not listed in the advisory (error messages can carry
user-controlled content from the external migration source).

Subject(...), Greeting(...), and CreateConversationalHeader(...) are
left unchanged: Subject is passed directly to the mail library and is
not markdown-rendered, Greeting is rendered via html/template's built-in
HTML escaping without markdown, and the conversational header is
sanitized as raw HTML by bluemonday in mail_render.go.

Fixes GHSA-45q4-x4r9-8fqj.
2026-04-09 15:44:04 +00:00
kolaente aa2b8c43f1 fix(caldav): escape user-controlled strings per RFC 5545 in VCALENDAR output
Task titles, UIDs, descriptions, categories, organizer usernames, alarm
descriptions, relation UIDs, and the calendar name were concatenated raw
into the VCALENDAR text. A task title containing CR/LF could plant new
iCalendar properties (ATTACH, X-INJECTED, VALARM, etc.) that CalDAV
clients would parse as legitimate calendar data.

Introduce escapeICalText, which escapes backslash, CR/LF, semicolon, and
comma per RFC 5545 §3.3.11, and apply it at every sink in ParseTodos,
ParseAlarms, and ParseRelations. Each Category is escaped individually;
the comma that joins categories is the literal list delimiter and stays
unescaped. The now-redundant regexp-based LF handling in the DESCRIPTION
branch is removed.

getCaldavColor is hardened at the same output boundary: non-hex
characters are stripped before interpolation so CR/LF in a crafted color
string cannot inject new iCal property lines, closing a gap where
upstream HexColor validation only bounds length and does not reject
control characters.

Fixes GHSA-2g7h-7rqr-9p4r.
2026-04-09 15:44:04 +00:00
kolaente fc216c38af fix(labels): derive label max permission from accessible tasks only
The previous hasAccessToLabel implementation ran `Get(ll)` against a
label_tasks LEFT JOIN with no ORDER BY, which meant the database was
free to pick any matching row. When a label had multiple attachments,
or when access was granted via the creator branch while the label also
had label_tasks rows pointing at inaccessible tasks, the picked row
could belong to a task the caller could not actually read.

That led to two concrete problems reported on the follow-up review of
GHSA-hj5c-mhh2-g7jq:

  1. maxPermission (exposed as the x-max-permission response header)
     could be derived from a task the caller has no access to, ending
     up as 0 or lower than the caller's real best permission on the
     label.
  2. Task.CanRead on a dangling/inaccessible task could return an
     error and surface as a 500, even though the label itself was
     perfectly readable via the creator branch.

Split the logic instead:

  * Use `Exist` for the boolean access check, using the same carefully
    grouped `And(Eq{labels.id}, Or(accessibleTask, creator))` cond.
  * Compute maxPermission by selecting the label_tasks rows whose
    task lives in a project the caller can access, then iterating
    those tasks with `Task.CanRead` and taking the maximum.
  * Fall back to PermissionRead when the access was granted via the
    creator branch and no accessible task attachment exists.
2026-04-09 15:43:04 +00:00
kolaente e836555032 fix(labels): correct broken access-control query for label reads (GHSA-hj5c-mhh2-g7jq)
hasAccessToLabel built its WHERE clause by chaining xorm session .Where,
.Or, and .And calls. xorm flattened those to `WHERE A OR B OR C AND D`,
which under SQL precedence evaluates as
`A OR B OR (C AND D)` — so the `labels.id = ?` predicate only narrowed
the project-access branch. The standalone
`label_tasks.label_id IS NOT NULL` branch leaked every label with any
label_tasks row to any authenticated user, and the
`labels.created_by_id = ?` branch leaked any label the caller had ever
created regardless of the requested id.

Rewrite the query using explicit builder.And / builder.Or grouping so
the label-id scope wraps the entire disjunction, drop the bogus
label_tasks-is-not-null branch, and keep the creator branch only for
real user auths. Replace Exist(ll) with Get(ll) so the resulting
LabelTask row is populated and the follow-up Task.CanRead check that
computes maxPermission actually runs; fall back to PermissionRead when
the match came via the creator branch and no task row is joined.
2026-04-09 15:43:04 +00:00
kolaente 379d8a5c19 test(security): webtest that a deleted link share rejects its still-valid JWT
End-to-end regression test for GHSA-96q5-xm3p-7m84 / CVE-2026-35594: mints
a JWT for a link share via the real helper, then deletes the share row and
invokes the real ReadAllWeb handler to prove the full request path (not
just the unit-tested GetLinkShareFromClaims) surfaces the revocation.

Also fixes a pre-existing stale literal in the TestLinkSharing test fixture
struct: linkshareRead declared Hash="test1" while the actual fixture row
id=1 uses Hash="test". The old code never looked at the DB so the mismatch
went unnoticed; after the fix it would cause every link-share webtest that
used linkshareRead to fail hash validation.
2026-04-09 15:38:07 +00:00
kolaente e025209e3c fix(security): validate link share JWTs against DB on every request
Previously GetLinkShareFromClaims built a *LinkSharing entirely from JWT
claims with no DB interaction, so deleted shares and permission downgrades
took up to 72h (the JWT TTL) to take effect. The permission and sharedByID
claims were trusted blindly.

GetLinkShareFromClaims now takes an *xorm.Session, looks up the share via
GetLinkShareByID, verifies the hash claim against the DB row, and returns
ErrLinkShareTokenInvalid when the row is missing or the hash mismatches.
The permission and sharedByID claims are discarded; the DB row is
authoritative. GetAuthFromClaims opens a read session for the link-share
branch, mirroring the existing API-token branch.

Token creation and the JWT format are unchanged, so already-issued tokens
keep working except when the underlying share has been deleted or its hash
no longer matches.

Fixes GHSA-96q5-xm3p-7m84 / CVE-2026-35594.
2026-04-09 15:38:07 +00:00
kolaente 6a0f39b252 fix(security): enforce HTTP method and path in scoped API token matcher
CanDoAPIRoute's non-CRUD fallback branch compared a path-derived
permission name to the token's permission strings without checking
the request method. A token with projects.background (registered for
GET /projects/:project/background) could therefore invoke DELETE on
the same path. The same method-confusion affected the whole
/projects/:project/views/:view/buckets[/:bucket] cluster, where a
token with projects.views_buckets (registered for GET) authorized
PUT, POST, and DELETE on any accessible view's buckets.

The matcher also leaked CRUD permissions between parent and nested
sub-resource groups. When the request targeted a nested CRUD resource
(e.g. projects_teams, projects_shares, projects_users, projects_views,
projects_webhooks, projects_views_tasks, tasks_assignees, tasks_labels,
tasks_comments, tasks_relations, tasks_attachments, teams_members),
the matcher fell back from the specific group to the parent's permission
list but then looked the permission name up inside the sub-resource's
RouteDetail map. The effect was that a token holding only projects.read_all
also authorized GET on every nested projects_* list endpoint, and the
same held for create/update/delete and for the tasks.* family.

Rewrite the matcher to iterate the token's own permissions and accept
only when the stored RouteDetail's (Path, Method) matches the request.
This removes all the path-derived group guessing and makes the stored
detail the single source of truth. Preserve the tasks.read_all quirk
(one permission, two list endpoints) as an explicit two-path allowlist
inside the loop.

Extract a GetAPITokenRoutes accessor so the new property-based webtest
can consume the same snapshot served by GET /api/v1/routes.

Add TestAPITokenMethodMatching in pkg/webtests: using the live echo
router and the live apiTokenRoutes map, it iterates every advertised
permission against every registered route and asserts the matcher
accepts iff the stored (Path, Method) matches. Any future collision
introduced by a new non-CRUD route on a shared path will be caught.

After this change, previously-dead permissions like
projects.background_delete, projects.views_buckets_{put,post,delete},
other.avatar, other.ws and caldav.access start working as their UI
labels imply. Tokens that relied on the over-broad background /
views_buckets grants, or on cross-cluster CRUD bleed-through, will
lose the extra access - that is the fix.

Refs: GHSA-v479-vf79-mg83
2026-04-09 15:17:20 +00:00
kolaente 88b534776a fix(kanban): skip upsert when repeating task already in default bucket (#2573)
When a repeating task dropped on the done bucket is already in the
view's default bucket, the upsert would try to UPDATE with an
unchanged bucket_id. MySQL reports 0 affected rows for unchanged
updates, so upsert fell through to INSERT and hit the unique
constraint on (task_id, project_view_id).
2026-04-09 10:45:34 +00:00
kolaente 1e784aa194 fix(tasks): route repeating tasks to default bucket when marked done (#2573) 2026-04-09 10:45:34 +00:00
kolaente 007379cff1 refactor(tasks): add moveTaskToDefaultBuckets helper (#2573) 2026-04-09 10:45:34 +00:00
kolaente 9a8126c111 test(tasks): add failing test for repeating task bucket routing via Task.Update (#2573) 2026-04-09 10:45:34 +00:00
kolaente 3d7bab4497 fix(kanban): route repeating tasks to default bucket when dropped on done (#2573) 2026-04-09 10:45:34 +00:00
kolaente e37b54abca test(kanban): add failing test for repeating task bucket routing on done (#2573) 2026-04-09 10:45:34 +00:00
kolaente 457ffbfe51 test(webhook): assert bad webhook is retried in no-duplicate test
Adds a hit counter to the bad webhook and asserts it is attempted at
least 3 times, proving the watermill retry middleware actually fires
on a failing delivery. We use GreaterOrEqual rather than an exact
count because gochannel resends nacked messages, so a permanently
failing delivery keeps running through retry cycles until the test
times out its wait window.
2026-04-09 09:26:04 +00:00
kolaente ca7f82a5e5 fix(webhook): order matching webhooks by id for deterministic fan-out
Previously WebhookListener.Handle fetched matching webhooks with
Find(&ws) without an explicit ORDER BY, so iteration order depended on
the DB driver. Add ORDER BY id ASC so the fan-out order is stable for
both project- and user-level webhooks, and update the sibling-blocking
regression test to insert webhooks with explicit ids so its ordering
assumption is robust to autoincrement state.
2026-04-09 09:26:04 +00:00
kolaente 85a3b3e469 fix(webhook): return error from delivery listener on nil payload
A nil payload signals data corruption or a version mismatch on the
event bus, not a safe-to-drop condition. Returning an error lets the
watermill retry middleware retry the message and eventually park it in
the poison queue instead of silently acking it.
2026-04-09 09:26:04 +00:00
kolaente bf87796669 test(webhook): handle deleted webhook gracefully between fan-out and delivery 2026-04-09 09:26:04 +00:00
kolaente 2a816f5db5 test(webhook): assert flaky webhook is retried until it succeeds 2026-04-09 09:26:04 +00:00
kolaente b09f50c6e2 test(webhook): assert good webhook delivered once despite sibling retries 2026-04-09 09:26:04 +00:00
kolaente bef58079d7 fix(webhook): dispatch one delivery event per webhook (#2569) 2026-04-09 09:26:04 +00:00
kolaente e5726239d4 feat(webhook): register WebhookDeliveryListener on startup 2026-04-09 09:26:04 +00:00
kolaente eef985011b feat(webhook): add WebhookDeliveryListener for per-webhook delivery 2026-04-09 09:26:04 +00:00
kolaente d89af8ce6d feat(webhook): add WebhookDeliveryEvent for per-webhook fan out 2026-04-09 09:26:04 +00:00
kolaente 38555b1120 test(webhook): add failing test for #2569 sibling webhook blocking
Also clear the example.com fixture webhook (id=1) in the existing
TestTaskUpdateWebhookE2E, since it now errors after sendWebhookPayload
returns non-nil for non-2xx responses.
2026-04-09 09:26:04 +00:00
kolaente 42b0f0ba77 fix(webhook): return error from sendWebhookPayload on non-2xx responses
Previously the HTTP response status was only logged, so retries never
triggered for failing webhooks and downstream fan-out bugs (#2569) were
impossible to exercise via tests. Returning an error lets the watermill
retry middleware do its job.
2026-04-09 09:26:04 +00:00
Rhys McNeill 699c766049 fix: add timeouts to Gravatar, Unsplash, and SSRF-safe HTTP clients 2026-04-09 07:31:08 +00:00
kolaente 17a97cacfa refactor: use per-view IN clause for filter task deletion instead of batching 2026-04-09 07:25:57 +00:00
kolaente bfdcea6bd2 fix: batch delete conditions in filter view cron to avoid SQLite expression depth limit
The filter view cron built an unbounded builder.Or(deleteCond...) tree
that exceeded SQLite's 1000-node expression depth limit when many tasks
needed removal. Delete conditions are now processed in chunks of 500.

Ref: #2550
2026-04-09 07:25:57 +00:00
kolaente 2014343557 fix: catch ErrNeedsFullRecalculation in task creation position conflict resolution
resolvePositionConflictsAfterInsert now falls back to a full position
recalculation when resolveTaskPositionConflicts returns
ErrNeedsFullRecalculation, instead of bubbling the error up as HTTP 500.
This mirrors the existing fallback logic in the CLI repair command.

Ref: #2550
2026-04-09 07:25:57 +00:00
kolaente c5450fb55f fix: update user list test expectations for new fixture user 2026-04-08 09:49:14 +00:00
kolaente 119d7df796 fix: use assert.Empty instead of assert.Equal for empty string check 2026-04-08 09:49:14 +00:00
kolaente a5fb01cc3d fix: reset SSO avatar provider to default when picture claim is removed 2026-04-08 09:49:14 +00:00
kolaente 1065bdd84c test: add tests for SSO avatar provider reset on empty picture URL 2026-04-08 09:49:14 +00:00
kolaente 76790348f7 test: verify background removal preserves project title
Regression test for #2552. Deletes the background of project 35 (owned by
testuser6) and then fetches the project to confirm the title is still
'Test35 with background'.
2026-04-08 09:07:15 +00:00
kolaente d5051c97e4 fix(background): use targeted column update when removing background
Fixes #2552. RemoveProjectBackground was passing a minimal Project struct
(only ID set) through UpdateProject, which always includes 'title' in its
Cols() list. This caused XORM to write the zero-value empty title to the
DB, wiping the real project title. Now uses ClearProjectBackground which
only updates background_file_id and background_blur_hash.
2026-04-08 09:07:15 +00:00
kolaente bf86bee690 feat(models): add ClearProjectBackground for scoped column update 2026-04-08 09:07:15 +00:00
kolaente c166eff95f test: remove obsolete invalid-cache-type test for avatar upload
RememberValue[T] always gob-decodes to the correct type, so the
corrupted-cache recovery path no longer exists.
2026-04-08 08:56:22 +00:00
kolaente 0f54dc43d0 fix: use RememberValue for task attachment preview cache
Migrate task attachment preview caching to RememberValue[[]byte] so it
works correctly with Redis gob-encoded values.
2026-04-08 08:56:22 +00:00
kolaente 59b047f76a fix: register gob types and use RememberValue for avatar and unsplash cache
Register CachedAvatar and Photo with encoding/gob so Redis can properly
deserialize them. Migrate both to use RememberValue[T] which calls
GetWithValue() internally, fixing the broken type assertion when Redis
is the keyvalue backend.

Also removes the recursion-depth fallback in upload.go since
RememberValue eliminates the type mismatch failure mode entirely.
2026-04-08 08:56:22 +00:00
kolaente e2de681b71 feat: add generic RememberValue[T] for type-safe keyvalue caching
RememberValue uses GetWithValue() internally for proper gob-decoding,
which is required when Redis is used as the keyvalue backend.
2026-04-08 08:56:22 +00:00
Frederick [Bot] f528bcc276 chore(i18n): update translations via Crowdin 2026-04-08 01:25:14 +00:00
Frederick [Bot] a0dd7a7270 [skip ci] Updated swagger docs 2026-04-07 15:45:50 +00:00
kolaente bc0bb556ad feat(migration): flatten project hierarchy for single-project imports 2026-04-07 15:20:06 +00:00
kolaente 3437f98dc3 feat(migration): add skip rows option to CSV import
Allow users to skip the first N data rows when importing CSV files.
This is useful when the CSV contains metadata rows before the actual
task data begins. Adds skip_rows to ImportConfig (backend) and a
number input in the parsing options UI (frontend).
2026-04-07 15:20:06 +00:00
Claude f555762def feat(migration): add generic CSV import with column mapping
Add a new CSV migration module that allows users to import tasks from
any CSV file with custom column mapping and parsing options.

Backend changes:
- New CSV migrator module with detection, preview, and import endpoints
- Auto-detection of delimiter, quote character, and date format
- Suggested column mappings based on column name patterns
- Transactional import using InsertFromStructure

Frontend changes:
- New CSV migration UI with two-step flow (upload -> mapping -> import)
- Column mapping selectors for all task attributes
- Live preview showing first 5 tasks with current mapping
- Parsing option controls for delimiter and date format

The CSV migrator creates a parent "Imported from CSV" project with
child projects based on the project column if provided, or a default
"Tasks" project for tasks without a specified project.
2026-04-07 15:20:06 +00:00
Frederick [Bot] 7820bb1ffd [skip ci] Updated swagger docs 2026-04-07 12:22:00 +00:00
kolaente e40877cca1 fix(migration): delete all default buckets when migration provides its own
Previously only the "To-Do" default bucket was deleted, leaving "Doing"
and "Done" as duplicates alongside migration-provided buckets. Now all
default-created buckets are removed when migration data already provides
bucket assignments for all tasks.
2026-04-07 12:05:47 +00:00
kolaente ccf1468884 fix(migration): correct TickTick swagger annotation to PUT 2026-04-07 12:05:47 +00:00
kolaente 1a1fd780ec feat(migration): add WeKan to migration page with logo
Register WeKan in the AvailableMigrators list and add the frontend
migrator entry with the WeKan logo, referenced as "WeKan ®".
2026-04-07 12:05:47 +00:00
kolaente 64aa7a9e75 feat(migration): register WeKan migration routes 2026-04-07 12:05:47 +00:00
kolaente 56ce73738d test(migration): add WeKan migration tests and fixture
Add comprehensive tests for the WeKan conversion function including
edge cases (empty board, orphan cards, color mapping, multiple
checklists, unsupported fields) and a realistic JSON fixture file.
2026-04-07 12:05:47 +00:00
kolaente 4cc7715951 feat(migration): add WeKan board JSON import
Add a file-based migration importer that reads WeKan board JSON exports
and creates Vikunja projects with kanban buckets, tasks, labels,
checklists, and comments.

WeKan lists become kanban buckets. Checklists are converted to HTML
task lists in the description. Card descriptions and comments are
converted from markdown to HTML using goldmark. Label colors are
mapped from WeKan's CSS color names to their actual hex values.
2026-04-07 12:05:47 +00:00
kolaente ac76bce5cd fix: use recursive CTE in accessibleProjectIDsSubquery for inherited project permissions
Resolves #2490. Users with team access on a parent project were not seeing
subtask relations for tasks in child projects because getUserProjectsStatement
does not walk the project hierarchy. The fix wraps the base query in a
recursive CTE that traverses child projects via parent_project_id.
2026-04-05 12:20:35 +00:00
Frederick [Bot] 23d9cbc664 [skip ci] Updated swagger docs 2026-04-05 10:05:20 +00:00
kolaente e9a26b9088 feat: add DELETE /test/all endpoint to truncate all tables 2026-04-05 09:48:09 +00:00
kolaente 6a3dd8b281 feat: add TruncateAllTables function for e2e test isolation 2026-04-05 09:48:09 +00:00
kolaente 6299bea794 fix(mail): guard log calls in GetMailDomain and fix hostname-dependent tests
GetMailDomain called log.Warningf which panics when the logger is not
initialized (e.g. in unit tests). Add log.IsInitialized() guard.

Also fix TestGetThreadID tests that hardcoded "vikunja" as the expected
fallback domain - on CI the os.Hostname() fallback produces a different
value. Tests now dynamically compute the expected domain.
2026-04-03 18:30:39 +00:00
kolaente 85a350749b refactor(mail): use CryptoRandomString for Message-ID generation
Replace manual rand.Read + hex.EncodeToString with the existing
utils.CryptoRandomString helper for generating the random part
of the Message-ID header.
2026-04-03 18:30:39 +00:00
kolaente 07aa3c1b04 fix(mail): fall back to os.Hostname() before hardcoded domain
When the public URL is not configured, GetMailDomain() now tries
os.Hostname() before falling back to the hardcoded "vikunja" string,
and logs a warning in both fallback cases.
2026-04-03 18:30:39 +00:00
kolaente 5249366aa3 refactor(models): use shared GetMailDomain in getThreadID 2026-04-03 18:30:39 +00:00
kolaente bbcd0648ac fix(mail): set RFC 5322 compliant Message-ID using public URL domain
Fixes #2522
2026-04-03 18:30:39 +00:00
kolaente 2f9ff51c60 feat(mail): add GetMailDomain helper for RFC 5322 compliant email IDs 2026-04-03 18:30:39 +00:00
kolaente a628c99006 test: assert position existence instead of conditional skip
Replace if-exists guard with require.True assertion so a
missing position correctly fails the test.
2026-04-03 17:26:55 +00:00
kolaente ce3e56f192 refactor: use nested map for position conflict tracking
Replace struct key map[viewPos]bool with nested
map[int64]map[float64]bool for cleaner lookups.
2026-04-03 17:26:55 +00:00
kolaente 104c8eadae fix: use InDelta for float comparison in tests 2026-04-03 17:26:55 +00:00
kolaente 0c3d01099f fix: detect and resolve position conflicts during task creation
Fixes #2495, fixes #2400

When tasks are created, setTaskInBucketInViews() assigns positions
without checking for conflicts. If two tasks get the same position value,
their order scrambles on reload. This adds a conflict check after
bulk-inserting positions, reusing the existing resolution logic from the
update path.
2026-04-03 17:26:55 +00:00
kolaente c6e79926f0 fix: add position conflict resolution for batch-inserted positions
Add resolvePositionConflictsAfterInsert() which checks newly inserted
task positions for duplicate position values within the same view and
resolves them using existing conflict resolution logic.
2026-04-03 17:26:55 +00:00
kolaente 88c2f0a289 fix(project): remove non-existent columns from UpdateProject column list
The UpdateProject function referenced done_bucket_id and default_bucket_id
in its column update list, but these columns belong to the project_views
table, not the projects table. This caused SQL errors when archiving or
updating a project on MySQL/PostgreSQL.

Also adds a test for archiving a non-archived project.

Fixes #2459
2026-04-03 16:59:05 +00:00
kolaente e7b693c788 fix(tasks): include tasks with deleted parents in subtask-expanded queries
When expand[]=subtasks is used, the LEFT JOIN on task_relations filters
out same-project subtasks. If a parent task was deleted, the JOIN
produces NULL for parent_tasks.id/project_id, and since NULL != value
evaluates to NULL (not TRUE) in SQL, these tasks were incorrectly
excluded.

Add builder.IsNull{"parent_tasks.id"} to the OR condition so tasks
whose parent was deleted are still included in results.
2026-04-02 16:30:23 +00:00
kolaente 4cd79088d1 test: add WebSocket e2e tests
Add comprehensive end-to-end tests for the WebSocket system:

- Protocol tests: auth (valid/invalid token, timeout, double auth),
  subscriptions (valid/invalid event, auth required, unsubscribe),
  message delivery (notification on team add, doer exclusion,
  multi-connection)
- Frontend integration tests: notification badge update, dropdown
  rendering, and logout cleanup via browser-level Playwright tests
- Comment notification test: full flow where user B mentions user A
  in a task comment and user A receives real-time WebSocket notification

Includes ws test dependency, shared test helper utilities, and
cascade-truncation of notifications when truncating users to prevent
test pollution.
2026-04-02 16:30:23 +00:00
kolaente f5385c574e feat(websocket): add notification event with XORM AfterInsert dispatch
Add NotificationCreatedEvent that fires automatically when a
DatabaseNotification is inserted, using XORM's AfterInsertProcessor
interface. The AfterInsert hook dispatches the event after the row
is persisted, without callers needing to manage DispatchOnCommit or
DispatchPending.

The WebSocket listener subscribes to this event, reloads the
notification from the database (ensuring accurate timestamps), and
pushes it to connected clients subscribed to the notification.created
event. Dispatch errors are logged rather than propagated since the
DB notification is already committed at that point.
2026-04-02 16:30:23 +00:00
kolaente 55ea5bd966 refactor(auth): extract shared token validation into auth package
Move JWT parsing (GetUserIDFromToken) and API token validation
(ValidateAPITokenString) into pkg/modules/auth so both HTTP middleware
and WebSocket auth use the same logic. This ensures consistent token
validity checks including expiry and user status (disabled/locked).

The HTTP API token middleware now delegates to the shared function,
removing duplicated lookup/expiry logic.
2026-04-02 16:30:23 +00:00
kolaente 0139e9a2ab feat(websocket): add HTTP upgrade handler and /api/v1/ws route
Add the WebSocket upgrade endpoint at /api/v1/ws with CORS origin
verification using the configured allowed origins. Includes nil hub
guard returning 503 if the WebSocket system hasn't been initialized.

Register the hub initialization in the app startup sequence and wire
the upgrade handler into the Echo router.
2026-04-02 16:30:23 +00:00
kolaente 9255fe07a9 feat(websocket): add message types, connection hub, and connection handler
Add the core WebSocket infrastructure:

- Message type definitions for the wire protocol (subscribe, unsubscribe,
  auth, error, push events)
- In-memory connection hub that tracks per-user connections and routes
  messages to subscribed clients
- Connection wrapper with auth-after-connect flow: connections start
  unauthenticated, client sends JWT as first message, only then can
  subscribe to event topics

Includes auth timeout (30s), shared cancellation context for read/write
loops, hub map cleanup on last connection removal, and proper error
delivery before closing on auth failure.
2026-04-02 16:30:23 +00:00
kolaente 6aa7217dad fix(caldav): skip tests for known CalDAV bugs and fix timing issues
Skip integration tests that document known bugs in Vikunja's CalDAV
implementation or the caldav-go library:
- Permission errors return 500 instead of 403/404
- Invalid VCALENDAR returns 500 instead of 400
- DELETE doesn't look up task by UID (silently fails)
- PROPFIND on nonexistent resource returns 207 not 404
- ETag format inconsistency between PROPFIND/REPORT/GET
- If-None-Match conditional requests not implemented
- Color field not included in CalDAV export
- RRULE (DAILY/WEEKLY/MONTHLY) not round-tripped
- DURATION not exported for VTODOs

Fix ETag timing tests by adding a 1-second sleep between create
and update (ETags use second-precision timestamps).
2026-04-02 11:34:55 +00:00
kolaente ef85a22f99 fix(caldav): resolve lint issues in caldavtests package
- Remove unused helper functions (findResponse, assertMultistatusHasResponses, caldavRequestAsUser)
- Fix gofmt formatting
- Convert WriteString(fmt.Sprintf(...)) to fmt.Fprintf
- Fix unused parameter warnings
- Fix testifylint suggestions (assert.NotEmpty, assert.Positive)
- Add nolint:unparam for assertResponseStatus
2026-04-02 11:34:55 +00:00
kolaente 7e7d168e45 test(caldav): add VTODO field round-trip tests (RFC 5545 §3.6.2)
Adds comprehensive round-trip tests for VTODO properties:
- Core fields: SUMMARY, DESCRIPTION, DUE, DTSTART, PRIORITY, COMPLETED, STATUS, CATEGORIES
- VALARM: absolute, relative-to-start, relative-to-end triggers
- COLOR via X-APPLE-CALENDAR-COLOR
- RRULE: DAILY, WEEKLY, MONTHLY frequencies
- Priority mapping: all 9 CalDAV levels including lossy mappings
- DURATION and DTSTART+DUE interaction
2026-04-02 11:34:55 +00:00
kolaente 0ca7c0dd01 test(caldav): add relation and subtask tests (RFC 5545 §3.8.4.5) 2026-04-02 11:34:55 +00:00
kolaente 7830a9c3ea test(caldav): add client compatibility and bug reproduction tests 2026-04-02 11:34:55 +00:00
kolaente 160fc9c01f test(caldav): add sync semantics tests (ETag, CTag, conditional requests) 2026-04-02 11:34:55 +00:00
kolaente 738bfa33af test(caldav): add authentication and permission tests 2026-04-02 11:34:55 +00:00
kolaente fecd6d6a1d test(caldav): add CRUD operation tests (RFC 4791 §5.3.2) 2026-04-02 11:34:55 +00:00
kolaente c0a9356646 test(caldav): add REPORT query tests (RFC 4791 §7.8, §7.9) 2026-04-02 11:34:55 +00:00
kolaente 56ead32dca test(caldav): add discovery flow tests (RFC 6764, RFC 5397, RFC 4791) 2026-04-02 11:34:55 +00:00
kolaente ebedd312c1 test(caldav): add PROPFIND tests (RFC 4918 §9.1) 2026-04-02 11:34:55 +00:00
kolaente e2478e2fd6 test(caldav): add caldavtests package with infrastructure, helpers, and mage target
- Package skeleton with TestMain, setupTestEnv, and fixture users
- HTTP request helpers (PROPFIND, REPORT, GET, PUT, DELETE, OPTIONS)
- XML/iCal response parsers and assertion utilities
- VTodoBuilder for constructing test VTODO payloads
- Common PROPFIND/REPORT XML body constants
- Smoke test validating the infrastructure works end-to-end
- mage test:caldav command and CI matrix entry
2026-04-02 11:34:55 +00:00
Frederick [Bot] 88c8d7a73d chore(i18n): update translations via Crowdin 2026-04-01 01:44:10 +00:00
Claude fc9c21915d feat(auth): enforce OpenID Connect issuer uniqueness across providers
Detect when two configured OIDC providers resolve to the same issuer URL
at startup and halt with a fatal error, preventing team sync data
corruption caused by ambiguous (external_id, issuer) matching.

Also adds duplicate issuer detection to the doctor service diagnostics
and comprehensive tests with mock OIDC discovery servers.
2026-03-30 22:41:50 +00:00
kolaente 59abe1bd84 test(plugins): add yaegi plugin integration tests
- Smoke test: verify yaegi can load stdlib and vikunja symbols
- Loader test: load example plugin and verify Name/Version
- Routes test: verify plugin HTTP routes serve responses
- Events test: verify plugin event listener registration via Init()
2026-03-30 20:44:46 +00:00
kolaente cc39aa7f08 feat(plugins): add yaegi interpreter-based plugin loader
EvalPath-based loading of Go source directories with typed factory
functions for interface bridging (required by yaegi's wrapping model).

Supports all plugin capabilities: routes, events, and migrations.
Registers itself into the Manager via init() to avoid import cycles.
2026-03-30 20:44:46 +00:00
kolaente 665f1b848c feat(plugins): extract third-party symbols for yaegi
Generated symbol tables for echo and watermill, enabling yaegi plugins
to use HTTP routing and the event/message system.

Exclude pkg/yaegi_symbols/ from golangci-lint (generated code).
2026-03-30 20:44:46 +00:00