Commit Graph
2695 Commits
Author SHA1 Message Date
kolaente c049cac1c7 refactor: tidy the edges of the position guards
Takes no lock to do nothing, says both meanings of the zero return, and
describes what resolving a conflict group actually does to the next one.

Claude-Session: https://claude.ai/code/session_01GR9u1EFvq994jqGoeJqYSX
2026-07-30 02:12:13 +02:00
kolaente afd34a3cc2 fix: keep task numbers when restoring a dump
address pr-swarm finding: ignoring a caller supplied index on the create path
also stripped it from the vikunja-file restore, which is not a request path. A
restore renumbered every task, so PROJ-17 in links and comments came back as
PROJ-4 - while the importer deliberately preserves the identifier those
references are built from.

The other importers never set an index, so the opt-in is a no-op for them.

Claude-Session: https://claude.ai/code/session_01GR9u1EFvq994jqGoeJqYSX
2026-07-30 02:12:13 +02:00
kolaente 44d4110037 fix: give the task index ceiling a real error
address pr-swarm finding: it was a bare errors.New, so a project already holding
an index past the ceiling - reachable on every shipped version, which honours a
client supplied index - answered every later create with an opaque 500. 409 with
the project named says what is wrong.

Healing such a project is left alone on purpose: which index the tasks should
get instead is a migration decision.

Claude-Session: https://claude.ai/code/session_01GR9u1EFvq994jqGoeJqYSX
2026-07-30 02:12:13 +02:00
kolaente 2588fdd127 fix: return the creator with an updated task
updateSingleTask rebuilt the task from a plain row read, where created_by is not
a column, so the response only ever carried a creator when the client had sent
one back itself. Now that the client stopped sending a field it is not supposed
to write, the task detail view showed "created by" with no name until reload.

Looked up from the id the row already holds, so every consumer gets it, not
just the one that used to echo it.

Claude-Session: https://claude.ai/code/session_01GR9u1EFvq994jqGoeJqYSX
2026-07-30 02:12:13 +02:00
kolaente 9d2c9d9a2d fix: resolve position conflicts in a stable order
address pr-swarm finding: grouping the conflicts by view left them in map order,
and the order is observable - resolving a lower group moves its tasks toward the
next one, so which runs first decides whether the view gets a local respace or a
full recalculation. Same data, different result per run.

Also restores the count == 0 early return dropped when that guard was split, and
lets the placement helper own the batch state so a caller cannot hand it tasks
the state was not built from.

Claude-Session: https://claude.ai/code/session_01GR9u1EFvq994jqGoeJqYSX
2026-07-30 01:28:54 +02:00
kolaente afbf55cefc fix: let the server assign every task index
address pr-swarm finding: the previous guard only clamped the value a client
sent. It still honoured anything up to the ceiling, so one request could pin a
project's counter at a billion and every later task got a ten digit identifier,
and calculateNextTaskIndex could still overflow on a project already holding a
poisoned row.

The request path now ignores the field entirely, matching what it is documented
as. Project duplication is the one caller that needs a picked index, and says so
through createTaskOpts. calculateNextTaskIndex refuses to hand out an index past
the ceiling instead of wrapping, which covers the cross project move too.

Claude-Session: https://claude.ai/code/session_01GR9u1EFvq994jqGoeJqYSX
2026-07-30 01:28:54 +02:00
kolaente 520d66b658 perf: look for position conflicts once per view, not once per row
address pr-swarm finding: resolvePositionConflictsAfterInsert ran one select per
inserted position - roughly 800 of them inside one transaction for a 200 task
batch across four views, while holding the view locks. Same check, grouped per
view and chunked like the inserts.

Claude-Session: https://claude.ai/code/session_01GR9u1EFvq994jqGoeJqYSX
2026-07-30 00:42:30 +02:00
kolaente 07cb28c5f1 refactor: place a batch of tasks from task_position.go
address pr-swarm finding: BulkTaskCreate.Create carried 50 lines of position
work while every other position rule lives here. The helper takes the insert as
a callback so "make room, then insert, then spread" cannot be called in the
wrong order - making room can trigger a recalculation, and a recalculation
orders by position, which tasks inserted too early do not have yet.

It also clones the view slice before sorting it by id for the lock walk. The
sort used to hit the slice held in the shared state, so the bulk path processed
views in id order while everything else uses their position order.

Claude-Session: https://claude.ai/code/session_01GR9u1EFvq994jqGoeJqYSX
2026-07-30 00:41:14 +02:00
kolaente fb1d9919b6 fix: ignore a task index outside the range a project can hold
address pr-swarm finding: index is documented read only but huma does not strip
read only fields from a request body, so a caller could post index MaxInt64.
calculateNextTaskIndex then wrapped to MinInt64 and every later create in that
project collided on the unique (project, index) constraint - one request, task
creation in a shared project broken for everyone, permanently.

Internal callers legitimately pass an index (duplication keeps task numbering),
so an implausible one now falls back to the server assigned next index instead
of being rejected outright.

Claude-Session: https://claude.ai/code/session_01GR9u1EFvq994jqGoeJqYSX
2026-07-30 00:28:00 +02:00
kolaente f5071410da refactor: make the per-batch create state usable without a constructor
address pr-swarm finding: the type carried a "always go through the constructor,
the maps have to exist" warning, which CLAUDE.md calls a redesign signal. Each
accessor lazy-inits its own map now, so the zero value works and the warning
goes away. Renamed off "cache" as well - it hands out task indexes and mutates
tasks, it is state, not a lookup table.

Claude-Session: https://claude.ai/code/session_01GR9u1EFvq994jqGoeJqYSX
2026-07-30 00:26:51 +02:00
kolaente b73f406e9c feat: raise the bulk create limit to 200 tasks
Pasting a long list is the workflow this endpoint exists for, and 100 lines is
not a lot of list. The message naming the limit stays for anything above it.

Claude-Session: https://claude.ai/code/session_01GR9u1EFvq994jqGoeJqYSX
2026-07-29 21:51:17 +02:00
kolaente e4852f0d88 perf: look a project up once per batch instead of once per task
createTask redid the same work for every task of a batch: fetching the project,
the doer, the project's views, each kanban view's default bucket, the highest
task index, and updating the project's last-updated timestamp. A 20 task batch
measured 207 statements against 22 for a single create, and 100 sequential
updates of one projects row serialize every other writer touching that project.

createTasks now does the n tasks case and createTask is the n=1 call into it, so
the contract of a single create is unchanged. Project duplication, the other
caller creating tasks in a loop, goes through it as well.

Claude-Session: https://claude.ai/code/session_01GR9u1EFvq994jqGoeJqYSX
2026-07-29 21:50:41 +02:00
kolaente b53720ef89 refactor: heal saved filter positions with the batch helpers
createPositionsForTasksInView placed tasks by the same rule as
makeRoomAtTopOfView plus spreadTasksAtTopOfView, making three implementations of
it in this file. It also returned right after a recalculation, leaving the tasks
it was called for unpositioned until the next heal; going through the helpers
places them straight away.

Claude-Session: https://claude.ai/code/session_01GR9u1EFvq994jqGoeJqYSX
2026-07-29 21:46:47 +02:00
kolaente 80f81381ee fix: let the server pick the position of a bulk created task
Position is documented read only and set through its own endpoint, so honouring
one passed to the bulk create contradicted the rest of the api - and nothing
called for it. The batch is now spread over the gap by slice order alone.

Counting the tasks off by their slice position rather than their task index also
keeps them apart in a view spanning several projects, where indexes repeat.

Claude-Session: https://claude.ai/code/session_01GR9u1EFvq994jqGoeJqYSX
2026-07-29 21:44:55 +02:00
kolaente b8e68092bc docs: say that every relation kind is ordered by position
Review question: the doc talked about subtasks while the code sorts every kind.
Ordering them all the same way is the intent.

Claude-Session: https://claude.ai/code/session_01GR9u1EFvq994jqGoeJqYSX
2026-07-29 21:43:00 +02:00
kolaente b86f6920e9 test: cover the bucket half of a bulk create
setBucket together with skipPositions is a new combination and nothing asserted
tasks still land in a bucket.

Address pr-swarm finding: untested setBucket and skipPositions combination.
2026-07-26 23:10:13 +02:00
kolaente 935f03e4c0 fix: bound the task ids a bulk update accepts
CanUpdate materializes every requested task before any permission check runs,
so an unbounded list let anyone force a giant IN query on ids they cannot see.
Cap it at the schema layer like the bulk create, and keep an empty-list guard
for the v1 route, which binds through Echo and never sees the Huma tags.

Address pr-swarm finding: unbounded bulk update input.
2026-07-26 23:10:02 +02:00
kolaente c836603ae2 fix: honour skipPositions when a task lands in a done bucket
A task created with the done bucket of a view flips to done and went through
moveTaskToDoneBuckets, which wrote a position row. The batch's own slot was
then dropped on conflict and the task fell out of the entered order.

Address pr-swarm finding: skipPositions violated by the done bucket path.
2026-07-26 23:09:57 +02:00
kolaente fbf08ef196 fix: lock the view before placing a batch at the top of it
Two concurrent batches read the same lowest position and computed
byte-identical slots, which is the arbitrary ordering this branch set out to
remove. Take the same lock the single-task path takes, walk projects and views
in a stable id order so two batches cannot deadlock, and run the conflict
repair after inserting.

Address pr-swarm finding: unserialized position read in the batch path.
2026-07-26 23:09:45 +02:00
kolaente 93d52e8b19 feat: add an endpoint to create multiple tasks at once
Claude-Session: https://claude.ai/code/session_01GR9u1EFvq994jqGoeJqYSX
2026-07-26 19:28:17 +02:00
kolaente a235dabeb8 feat: add a model to create multiple tasks at once
Creates the tasks in one transaction and places them above everything the
views already contain, spread out so they keep the order they were passed in.
Doing that one task at a time puts them all in the same spot and leaves the
final order to the position conflict repair.

Claude-Session: https://claude.ai/code/session_01GR9u1EFvq994jqGoeJqYSX
2026-07-26 19:25:41 +02:00
kolaente 2eff63b9f3 refactor: fold createTask flags into an options struct
Makes room for a third opt-out (position creation) without turning the call
sites into a row of unlabelled booleans.

Claude-Session: https://claude.ai/code/session_01GR9u1EFvq994jqGoeJqYSX
2026-07-26 19:17:36 +02:00
kolaente 7918351e12 fix: sort subtasks by their position in the view being read
Nested subtasks were returned in the order their relations were created, so
they ignored the view's own order - reordering a subtask by dragging it, or
sorting the view, left the nesting untouched.

Positions only exist per view, and not every caller reads tasks through one:
the task detail page, CalDAV and exports pass no view, and a subtask in another
project has no row for this view either. Those keep relation id order, which
stays stable, and sort after the ones that do have a position.
2026-07-26 18:25:35 +02:00
kolaente 426d8b92e2 test: note sqlite insertion-order caveat in subtask order test
Address pr-swarm finding.
2026-07-26 17:46:23 +02:00
kolaente 55149aed00 fix: return subtasks in the order their relations were created
Clients render nested subtasks in the order the api hands them back, but the
task relation query had no ORDER BY, so the order was up to the database.
2026-07-26 16:43:33 +02:00
kolaenteandkolaente aa62c00b78 test: guard parent_project_id in project responses
Route-level tests on both api versions, since the pseudo-projects only get
their parent through the read path. The v2 read body embeds models.Project and
Huma's $schema wrapper copies its fields into a generated struct, so a custom
MarshalJSON on the model would not have applied there — assert the raw JSON
instead of the struct.
2026-07-26 09:39:41 +00:00
kolaenteandkolaente 2e0cd81ce0 fix: always serialize parent_project_id for pseudo-projects
781ffac19 (GHSA-44v6-7fxq-vgf4) turned ParentProjectID into a *int64 with
`omitempty` so a write can tell an omitted field from an explicit 0 (detach,
admin only). That also dropped the key from every response where the pointer is
nil: the Favorites and saved-filter pseudo-projects, which are built in Go and
never set it, plus rows whose nullable parent_project_id column was never
backfilled. Clients parse the field as a plain int, so the whole project list
fails to decode (go-vikunja/app#295).

Keep the pointer and its three-way write semantics, but never hand out a nil:
normalize DB rows in AfterLoad and set the field where the pseudo-projects are
built.

Closes go-vikunja/app#295
2026-07-26 09:39:41 +00:00
kolaenteandkolaente 4daccae956 fix(filters): don't let one filter fail the whole filter view maintenance
Both the listener and the cron bailed out completely as soon as a single saved
filter errored, so no filter view in the instance got updated. Skip the failing
filter instead. Genuine db errors still bubble up so the retry middleware can
do its job.
2026-07-26 01:46:14 +00:00
kolaenteandkolaente 771de18c8c fix(filters): skip saved filters owned by disabled users
Evaluating a saved filter needs the list of projects its owner has access to,
which getRawProjectsForUser refuses to return for disabled or locked accounts.
Since the listener walks every manual kanban filter view in the instance, a
single filter of a disabled user made the whole handler fail with "Account is
disabled" - for every task update, for everyone. The recalculation which would
un-crowd the positions is the very thing that failed, so the state never healed.
2026-07-26 01:46:14 +00:00
kolaenteandkolaente 8df183123d perf(tasks): index task_positions on (project_view_id, position)
Every task creation looks for rows with the same position in the view to
resolve conflicts. Without an index on position that scans all positions of
the view, so creating n tasks in one project costs O(n²) row reads - the
second half of the slow import in #3297.
2026-07-26 01:13:08 +00:00
kolaenteandkolaente 0101dc9386 fix(migration): seed task positions for exports without order information
Tasks imported without a position are inserted in front of the lowest one by
halving it. That reaches the minimum spacing every few dozen inserts, which
recalculates every position in the view and makes a large import O(n²) - a
10k task TickTick export with an empty Order column never finished.

Numbering the tasks up front skips the halving entirely and keeps the order
the export was written in.

Closes #3297
2026-07-26 01:13:08 +00:00
Frederick [Bot] c0f026ff47 chore(i18n): update translations via Crowdin 2026-07-25 00:24:11 +00:00
kolaenteandkolaente f02df25510 test: guard schema-link creation for all v2 response types
Registers the full v2 API and fails on Huma's 'unable to create schema link' stderr warning, catching any future response body type Huma cannot wrap. Fixes #3272.
2026-07-24 12:28:45 +00:00
kolaenteandkolaente b8492a0ab9 fix: embed LinkSharing by value in LinkShareToken
Same pointer-embed problem as AdminUser; the share auth response was missing $schema.
2026-07-24 12:28:45 +00:00
kolaenteandkolaente 4cc647b9b8 fix: embed user.User by value in AdminUser
A pointer embed's method set is never empty, so Huma could not create the $schema wrapper for admin user responses.
2026-07-24 12:28:45 +00:00
kolaenteandkolaente 6f65621c14 fix: shadow embedded CRUDable ReadAll method on SavedFilter
Same mechanism as LinkSharing.Update: the promoted method broke Huma's $schema wrapper for savedFilterReadBody.
2026-07-24 12:28:45 +00:00
kolaenteandkolaente c580d516f4 fix: shadow embedded CRUDable Update method on LinkSharing
The unshadowed promoted method made LinkSharing's value method set non-empty, which breaks Huma's $schema wrapper for response bodies embedding it (reflect.StructOf, go#15924).
2026-07-24 12:28:45 +00:00
Frederick [Bot] 675ba6dd97 chore(i18n): update translations via Crowdin 2026-07-24 00:21:02 +00:00
kolaente 6895a7765e fix(security): reject cross-project views in ProjectView Can{Delete,Update} (GHSA-gg93-x632-9ccv) 2026-07-19 18:59:34 +02:00
kolaente 781ffac198 fix(security): require Admin to detach a project from its parent (GHSA-44v6-7fxq-vgf4) 2026-07-19 18:59:34 +02:00
kolaente 1c13624a8e fix(api): reject link shares on reaction and task-read endpoints (consistency; GHSA-vvcv-vpph-h844) 2026-07-19 18:59:34 +02:00
kolaente 3a0ea15d8c fix(security): hash password-reset, email-confirm and deletion tokens at rest (GHSA-r6w9-259g-gwrv) 2026-07-19 18:59:34 +02:00
kolaente 00fd2c6155 fix(security): clear plaintext user tokens for reset/confirm/deletion (GHSA-r6w9-259g-gwrv) 2026-07-19 18:59:34 +02:00
kolaente 4ae2e09301 fix(auth): reject API tokens at the OAuth authorize endpoint (GHSA-v3p6-34mc-hj7v) 2026-07-19 18:59:34 +02:00
kolaente 7854f2729a fix(auth): require verified email before OIDC email-fallback account linking (GHSA-xv7q-fvmc-jx96) 2026-07-19 18:59:34 +02:00
kolaente be36c11e67 fix(api): derive API-token ownership from a verified user principal (GHSA-vvcv-vpph-h844) 2026-07-19 18:59:34 +02:00
kolaente d911caaa11 fix(projects): enforce write permission on target parent when duplicating a project (GHSA-f27p-pw2p-9pr4) 2026-07-19 18:59:34 +02:00
kolaente cfb9c24519 fix(kanban): pin link-share task collection view to the share's project (GHSA-rj9j-8772-4h6c) 2026-07-19 18:59:34 +02:00
kolaente b31d606b88 fix(kanban): prevent cross-tenant bucket relocation via project_view_id mass-assignment (GHSA-569v-q83c-3j3g) 2026-07-19 18:59:34 +02:00
kolaente 36cdc2ce2b fix(kanban): authorize body task_id when moving a task into a bucket (GHSA-5pg6-m483-7vrg) 2026-07-19 18:59:34 +02:00