2631 Commits

Author SHA1 Message Date
kolaente
dd82549f17 chore(config): disable icon changes by default
We have recieved feedback from enterprise users who found this to be very unprofessional not not worthy of enterprise software. Therefore, we made the difficult decision to disable icon changes by default. We're looking into adding more clues that transport the same message but are less obvious.
2026-07-15 12:19:41 +02:00
TowyTowy
3af3c04f03 fix(filter): accept non-zero-padded dates in task filters
The manual date fallback in parseTimeFromUserInput parses dates like
2022-11-1, but `year, err :=` shadowed the named err return. On a
successful manual parse the function returned a valid time alongside the
stale error from the earlier failed layout attempts, so filters such as
`due_date = 2022-11-1` were rejected as invalid. Assign to the named
return so success clears the error.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-13 07:50:32 +00:00
Tink
4425e0d146 fix(attachments): keep blob mime type so pdf previews open inline (#3157) 2026-07-11 18:53:09 +02:00
Tink
b396bad3bb fix(postgres): resolve all statements against the configured schema (#3156) 2026-07-11 18:02:57 +02:00
TowyTowy
efdc675fa7 fix(caldav): compute VTODO DURATION components correctly (#3155) 2026-07-11 17:28:59 +02:00
kolaente
85231284ab feat(auth): add OpenID provider availability monitoring and retry logic (#3145) 2026-07-10 17:52:01 +02:00
Tink
190ff249ab fix(notifications): queue mails only after the notification row is committed (#3150) 2026-07-10 17:50:55 +02:00
kolaente
0b70fb2658 fix(cli): drain the mail queue before user commands exit
Mail sending is asynchronous: SendMail only enqueues the message and a
background daemon goroutine performs the actual SMTP delivery. CLI
commands exited as soon as their run function returned, killing the
daemon before the handshake completed, so `user reset-password`,
`user delete` and `user create` reported success without ever
sending their mail.

Add mail.StopMailDaemon which closes the queue and blocks until the
daemon has delivered all remaining messages (with a timeout so a broken
SMTP server can't hang the CLI), and call it from a PersistentPostRun
hook on the user command. The web server's non-blocking behavior is
unchanged.
2026-07-09 17:05:21 +00:00
kallegrens
82ee780161 feat(caldav): implement RFC 6578 sync-collection REPORT to sync task deletions
iOS Reminders (and other RFC 6578 clients) never learned about tasks
deleted on the Vikunja side: caldav-go answers unknown REPORT types
with 412, which makes iOS silently stop syncing, and deleted tasks left
no protocol-level trace a client could observe.

Intercept sync-collection REPORTs before caldav-go sees them and answer
per RFC 6578: tasks changed since the sync token as 200 entries and
deleted tasks as 404 entries. Deletion records come from task
soft-deletes, which are kept for 30 days before being purged.

Token edge cases: delta comparisons are inclusive because tokens have
second granularity (re-reported items keep their etag, so clients skip
the download), and tokens older than the soft-delete retention answer
403 + valid-sync-token to force a full resync instead of a delta that
would silently miss purged deletions.
2026-07-07 11:57:04 +00:00
kolaente
c85a56511b fix(tasks): bump project and task updated times when task sub-entities change
Relations, comments, attachments, assignees (via the shared listener)
and label add/remove now advance the project's updated timestamp, and
label changes also advance the task's, so CalDAV ctags and
sync-collection deltas reflect every change that is visible in a VTODO.
2026-07-07 11:57:04 +00:00
kallegrens
c8f136a11b feat(caldav): emit STATUS:NEEDS-ACTION for incomplete todos
Some clients, iOS Reminders among them, classify VTODOs more reliably
when the status is explicit instead of implied by a missing
STATUS:COMPLETED.
2026-07-07 11:57:04 +00:00
Frederick [Bot]
3ac5b29aef [skip ci] Updated swagger docs 2026-07-07 09:51:58 +00:00
Tink
d83e90541c feat(tasks): soft-delete tasks with permanent deletion after 30 days (#3119) 2026-07-07 11:38:33 +02:00
Frederick [Bot]
9d3c505e7f [skip ci] Updated swagger docs 2026-07-06 16:26:52 +00:00
kolaente
8a79c30477 chore(search): consolidate scoring comments and deduplicate test helpers
The ranking-gate comment still claimed title and description weigh
equally, which the 1.5x boost made wrong; scoring semantics now live only
at the boost call site. Test subtests share searchTaskPositions instead
of hand-building position maps four times.
2026-07-06 15:45:55 +00:00
kolaente
0986782321 feat(search): boost title matches 1.5x over description matches
With constant-score relevance, 1.5 ranks a title match above a description
match while two matched words (2.0) still beat a single boosted title word,
preserving "more matched words wins". ParadeDB-only; the substring fallback
has no scoring.
2026-07-06 15:45:55 +00:00
kolaente
f980f0424f docs(search): describe the actual constant-score relevance semantics
The fuzzy query shape makes pdb.score a constant-score sum (1.0 per
exact/prefix word-field match, 0.5 per one-edit match), not BM25, and no
field boosting is configured. Correct the comments claiming otherwise.
2026-07-06 15:45:55 +00:00
kolaente
350704573e test(search): pin ParadeDB matching contract with a behavioral test suite
Covers per-token matching across title and description, word-order
independence, description-only matches, and fuzzy(1)-against-token-prefix
matching, including its surprising looseness ("two" matching "world" via
the one-edit prefix "wo"). Skipped on databases without ParadeDB, whose
whole-string substring fallback intentionally matches less.
2026-07-06 15:45:55 +00:00
kolaente
167556ce2b feat(search): add explicit relevance sort field
sort_by=relevance renders as pdb.score(tasks.id) DESC at its requested
position, so clients can express orderings like done,relevance (undone
first, most relevant within each group). The implicit no-sort ranking
reuses the same mechanism via an injected sort param.

When the database or query shape cannot score, relevance params are
dropped and the remaining sorts plus the id tiebreaker apply, so the
field is safe to send unconditionally; getOrderByDBStatement also only
emits pdb.score when ParadeDB is actually available.

The quick-actions search sends sort_by=done,relevance (typed via
TaskFilterParams) and keeps surfacing undone tasks first.
2026-07-06 15:45:55 +00:00
kolaente
e24ec07293 feat(search): rank ParadeDB search results by relevance
Searching with ParadeDB returns fuzzy/OR matches in id order, so a task
matching all query words can sink below tasks matching only one. When a
search term is present and the client sends no explicit sort, order the
results by pdb.score(tasks.id), keeping the stable id tiebreak.

Numeric #id searches and the Favorites view keep the default order since
pdb.score rejects those query shapes. On the all-projects scope the
favorites arm is dropped when every favorited task already lies inside
the project scope, so global search stays ranked for users with
favorites; out-of-scope favorites fall back to the unranked order.

The frontend omits sort_by while searching in project views unless the
user explicitly picked a sort, so the backend ranking engages.
2026-07-06 15:45:55 +00:00
nithin varma
afdc471c24 fix(task): recurring tasks return to original bucket when no default is set (#2806) 2026-07-03 23:18:00 +02:00
kolaente
cc627ee634 fix(tasks): prevent position race conditions with atomic upserts and locking (#3098) 2026-07-03 19:58:39 +02:00
Tink
0e3171ebbb fix(tasks): show filter-matched subtasks whose parent is filtered out (#2952) 2026-07-03 19:38:01 +02:00
kolaente
7217604010 fix: only audit denied admin access for confirmed non-admins
A failed user read (transient DB error, or a deleted user with a
still-valid JWT) previously also emitted admin.access.denied, writing
a misleading probing entry for what may be a legitimate admin.
2026-07-03 16:56:03 +00:00
kolaente
3a666da860 feat: audit admin reads and denied admin access
GET /admin/users returns every user's email address; compliance
regimes commonly require admin PII access to be logged, so the list
read now emits admin.users.listed. The admin gate additionally emits
admin.access.denied (outcome=failure, with method and path) whenever
an authenticated user without the instance-admin flag probes an
/admin/* route — dispatched directly with the request context since
there is no transaction.

Both entries have no single affected resource, so the entry target is
now omitted when empty instead of serialising a zero value.
2026-07-03 16:56:03 +00:00
kolaente
6f21c57c78 feat: audit admin user and project mutations
Every mutating admin action now dispatches a typed event on commit
(admin.user.created, admin.user.admin.granted/revoked,
admin.user.status.changed, admin.user.password.set,
admin.user.password_reset.sent, admin.user.deleted,
admin.project.owner.changed) which the audit catalog maps onto an
entry with the acting admin as actor and the affected user or project
as target. The model actions grow a doer parameter since the acting
admin was not threaded into the models layer before.

Admin creation deliberately emits both user.created (actor = the new
user, unchanged semantics) and admin.user.created (actor = the admin).
The events are instance-level, not project-scoped, so they are not
exposed as webhook events. No password or reset-token material is ever
part of a payload.
2026-07-03 16:56:03 +00:00
kolaente
39bbf8d006 fix: dispatch pending events in admin handlers
Neither the v1 nor the v2 admin handlers called events.DispatchPending
after commit, so events queued via DispatchOnCommit were silently
dropped: admin-created users never fired user.created (no webhook,
notification or audit entry) and mode=now user deletion dropped the
cascaded project.deleted events. Each dropped queue also leaked an
entry in the pendingEvents map keyed on the dead session.

Dispatch after commit, clean up on rollback, and thread the real
request context through the v2 handlers so request metadata flows onto
the events.
2026-07-03 16:56:03 +00:00
Bradley Erickson
61d74ee2ef fix: assign bot-created projects to the bot's owner
When a bot user creates a project, the bot's owner (the human who
created the bot) now owns the project instead of the bot itself. The bot
retains admin access via an explicit project share. This ensures the
human can always see and manage projects their bots create.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-03 06:53:29 +00:00
Tink
f11c90ca6b fix: create saved filter positions before the fetch query (#3084) 2026-07-02 19:43:15 +02:00
Tink
d0dccf2736 feat(pro): admin password reset for existing users (#3085) 2026-07-02 19:39:00 +02:00
Robert Szulist
7ae80c073b fix: GCS signing issue (#2920) 2026-07-02 17:38:37 +02:00
Bradley Erickson
bdb07799d3 fix(api): return 200 instead of 500 when listing attachments on a task with none
ReadAll used a bare return when len(attachments) == 0, which returned
nil for the interface{} result. The v2 handler's type assertion then
failed on nil, producing an untyped error that Huma surfaced as 500.

Return the empty slice explicitly so the assertion succeeds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-01 12:42:32 +00:00
kolaente
93f88084de fix: resolve golangci-lint v2.12 findings 2026-06-30 18:53:56 +00:00
kolaente
c7413a1d86 fix: adapt to sentry-go v0.47 API changes
Scope.SetExtra was removed in sentry-go v0.47.0 (deprecated earlier,
dropped in getsentry/sentry-go#1274). For error events the recommended
replacement is SetContext, so attach the request URL via SetContext
instead.
2026-06-30 18:28:11 +00:00
kolaente
e1afa039cb fix: use correct type for permission error 2026-06-29 17:18:05 +02:00
kolaente
fa0c9a8584 feat(api): exchange rich-text fields as markdown on v2
Wire the conversion helpers into every rich-text handler: read/list/echo
convert HTML to markdown, create/update convert markdown to HTML before
persisting, and each op documents the format query field. Opt-in via
?format=markdown or the X-Vikunja-Format header.
2026-06-29 08:12:35 +02:00
kolaente
71639a3dc5 feat(api): add v2 markdown conversion helpers
Adds the opt-in format plumbing for v2: requestWantsMarkdown (query or
X-Vikunja-Format header), convertToMarkdown/convertToHTML/convertTasksToMarkdown
field converters, the cross-cutting API description, and stripPatchFormatQuery
(AutoPatch drops the query, so PATCH advertises only the header).
2026-06-29 08:12:35 +02:00
kolaente
8d10e053d4 fix(caldav): store markdown descriptions as HTML, skip spurious updates
Incoming CalDAV descriptions are markdown; convert them back to canonical HTML
(rebuilding mentions) before persisting. Skip the conversion when the markdown
is unchanged from the stored HTML so a passthrough sync doesn't churn the value
or bump the ETag.
2026-06-29 08:12:35 +02:00
kolaente
a728e50796 feat(caldav): serialize task descriptions as markdown
CalDAV clients render DESCRIPTION as plain text, so convert the stored HTML to
markdown when serializing VTODOs. On the near-impossible conversion error, log
it and keep the stored value.
2026-06-29 08:12:35 +02:00
kolaente
9015bad65c feat(richtext): add markdown-domain change detection
Changed reports whether inbound markdown differs from stored HTML by comparing
in the markdown domain, so callers can skip rewriting unchanged fields.
2026-06-29 08:12:35 +02:00
kolaente
3459158b99 feat(richtext): add Markdown to HTML conversion with mention rebuild
Converts GFM Markdown to canonical HTML via goldmark (GFM, no WithUnsafe),
rewriting task lists into TipTap's structure and resolving @username mentions
to <mention-user> tags against real users.
2026-06-29 08:12:35 +02:00
kolaente
3abe8d650a feat(richtext): add HTML to Markdown conversion
Converts rich-text HTML to GFM Markdown (standard + GFM extensions) including
the Vikunja/TipTap-specific nodes (mentions, task lists). Adds the
html-to-markdown/v2 dependency.
2026-06-29 08:12:35 +02:00
karl Einziger
0efae572cd fix(auth): use binddn as group sync dn instead of userbind 2026-06-27 15:12:10 +00:00
kolaente
9e880e98a5 fix(api): export api-token permission groups in snake_case
The api-token permission group key is derived from the route slug. Every
group is snake_case except "time-entries", whose URL slug carries a hyphen.
The frontend snake_cases request payloads, rewriting that group key to
"time_entries", which the backend then rejected — so a token granted the
Time Entries scope could not be saved.

Canonicalise group and path-segment names to snake_case where they are
derived, and normalise the group key on token validation and authorisation
so any token stored under the old hyphenated key keeps resolving. No data
migration is needed: the v2 time-entries resource has never shipped in a
release.
2026-06-27 15:01:54 +00:00
gabe
98b3613247 feat(filters): generate open task saved filter on user creation 2026-06-27 13:35:50 +00:00
kolaente
330b94c3c4 feat(migration): import recurring tasks from todoist 2026-06-26 13:32:08 +00:00
kolaente
0d043e80e4 feat(api/v2): add kanban bucket CRUD endpoints
Port the standalone bucket list/create/update/delete from v1 to the
Huma-backed /api/v2, under /projects/{project}/views/{view}/buckets,
using v2 verb conventions (POST creates, PUT updates). The handlers
reuse the generic handler.Do* functions, so permissions are enforced
by the Bucket model's existing Can* methods.

Mirrors v1: no read-one route (the model has no ReadOne/CanRead), so
AutoPatch synthesises no PATCH. No model changes.
2026-06-26 08:56:15 +00:00
Bradley Erickson
f8eacca7c8 fix(auth): allow api tokens to access global v2 task list endpoint
The tasks.read_all special case in CanDoAPIRoute only covered v1 paths.
Both GET /api/v2/tasks and GET /api/v2/projects/:project/tasks normalize
to the same tasks.read_all map key, but only one RouteDetail survives —
the project-scoped path overwrites the global one. The exact path
comparison then rejects the global endpoint with 401.

Extend the special case to include the v2 paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-24 17:49:02 +00:00
Tink
81791fd346 fix(auth): link OIDC username fallback on preferred_username, not just sub (#2945) 2026-06-19 20:47:05 +02:00
Frederick [Bot]
764e4efa18 [skip ci] Updated swagger docs 2026-06-19 16:52:13 +00:00