Commit Graph
229 Commits
Author SHA1 Message Date
kolaente 18f3572ed4 test(migration): build the orphan test data from the real label models
The test declared its own minimal label struct. With
VIKUNJA_TESTS_USE_CONFIG the test engine is the database every other test
package shares, so syncing that partial struct created `labels` without
`created_by_id` and left a row in it. Every package running afterwards
then failed to add the column as NOT NULL.
2026-09-28 15:21:55 +02:00
kolaente 2ed1e5299f fix(migration): clean up label_tasks rows orphaned by deleted labels
Removes the rows left behind by the label deletion bug fixed in the
previous commit.
2026-09-28 15:21:55 +02:00
kolaenteandTink fa9fc4e06f feat(migration): keep queued import uploads in the file storage
The upload was spooled to the receiving instance's temp dir and only its
name travelled with the event. That holds while events stay in-process,
but it ties the job to the instance that accepted the upload.

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

The upload is removed once the import is over. An hourly cron deletes
leftovers from events lost on restart or instances that died mid-import.
2026-09-15 17:57:00 +02:00
kolaenteandTink 84804e71f5 feat(migration): record whether a migration succeeded
A finished migration_status row looked the same whether the import worked
or failed, so the client could only guess. Rows now carry error_kind, a
closed set the client translates (reported, interrupted, credentials,
queue, upload, detail), and error_message, set only for "detail": the
error caused by the user's own data, the same text the failure mail sends.
A failure we report to Sentry stores just the kind, so server internals
never reach the status.
2026-09-15 16:59:37 +02:00
kolaenteandTink 68ac9eaa99 fix(migration): judge an abandoned claim by a heartbeat, not by start time
The claim timeout was measured from started_at, so a long import either
kept a dead claim for a day (the old 24h default) or had its slot taken
over while still running. A running migration now refreshes heartbeat_at
at a tenth of the timeout, clamped to 1s..30s, and a claim only counts as
stale once its last beat is older than the timeout. Rows that never beat
fall back to started_at. The default timeout drops to 5m accordingly.

Also bind the stale cutoff in the configured timezone: xorm writes times in
the engine's zone but binds query args as-is, which broke the comparison on
SQLite.
2026-09-15 16:59:37 +02:00
kolaente a10b98f8e4 fix(migration): leave subscriptions unique index to its dedupe migration
Upgrades from before v2.5.0 ran 20260720120000 before 20260829190000, so the
index recovery tried to create the unique subscriptions index from the current
model while duplicate rows still existed and aborted the upgrade.

Fixes #3882
2026-09-14 17:13:30 +02:00
kolaente 27ccdba7a0 feat(invites): add admin-managed user invite links 2026-09-12 18:21:35 +02:00
kolaente 0fd60fc080 feat(tasks): add task index counters and historical aliases
Each project gets a monotonic high-water counter row, backfilled from the
highest existing task index, and an append-only alias table recording
retired (project, index) addresses. Project creation seeds the counter,
project deletion removes both.
2026-09-11 20:47:11 +02:00
kolaente 8a63b5e466 feat(migration): create and backfill project_ancestors
Backfills from projects in memory, replacing one batch of 500 projects
at a time: a re-run after an interrupted run converges without the
whole table ever being empty under a replica which already serves from
it. A parent cycle or a missing parent ends the chain there.
2026-09-10 22:44:26 +02:00
kolaente 9296449000 test(migration): cover api_tokens sha256 migration, sqlite rebuild and re-runs 2026-09-06 20:29:27 +02:00
kolaente 8ead2bc014 feat(migration): add token_sha256 to api_tokens and make legacy hash columns nullable
Nullable on purpose: existing rows have no plaintext to hash and get
backfilled lazily on first use, and new tokens no longer write the
PBKDF2 salt/hash/last-eight columns. SQLite cannot drop NOT NULL, so
the table is rebuilt there inside one transaction; the rebuild is
skipped when token_sha256 already exists so a re-run cannot null it.
The unique index is created through the dialect so its name matches
what xorm's own Sync would produce on every database.
2026-09-06 20:29:27 +02:00
TinkandGitHub d22c895b50 perf(projects): partial index on parent_project_id for child projects (#3777) 2026-09-06 08:28:57 +00:00
kolaente 44d3540340 fix(migration): quote index identifiers so the swap works on postgres
Postgres folds unquoted identifiers to lowercase. `DROP INDEX IF EXISTS
IDX_tasks_done_due_date` therefore looked for `idx_tasks_done_due_date` and
silently matched nothing whenever the index carried the mixed-case name xorm
gives it, and the companion CREATE stored the new index under a lowercase name
that a later sync would not recognise, creating a second copy.

Both statements now go through the dialect quoter, with CREATE built by
`CreateIndexSQL` so the name matches what xorm derives from the struct tag.

v2.6.0's migration had the same bug, so on postgres the old index is actually
named `idx_tasks_done_due_date` there. Drop both spellings.

Also fixes the test helper, which called XName on index names DBMetas had
already returned in full and so compared against a double-prefixed
`IDX_tasks_idx_tasks_project_done_due_date`.
2026-09-04 10:32:56 +02:00
kolaente 98235c670c perf(tasks): index (project_id, done, due_date) instead of (done, due_date)
The overview's task query filters `project_id IN (...) AND done = false` and
sorts by due_date. `done` is not selective, so with an index on
(done, due_date) the planner walks it in due_date order and heap-probes every
open task in the database just to test project_id.

Leading with project_id lets the scan seek to the user's projects and top-N
sort a small set instead.

The old index is dropped rather than kept: its only other consumer is the daily
overdue-reminder cron, whose selective predicate is the due_date range that
IDX_tasks_due_date already covers.

ProjectID moves above Done in the Task struct because xorm orders composite
index columns by struct field order, which also shifts project_id earlier in
the serialized JSON — hence the churn in the task collection test's expected
response bodies.
2026-09-04 10:32:56 +02:00
kolaente fc78cd3ea5 perf(projects): store the parent of top-level projects as null
The recursive step of the project access CTE joins parent_project_id
against a project id, which implies NOT NULL. Storing root projects as
NULL instead of 0 lets a partial index cover only real children, so the
index scan walks a fraction of the rows.

MySQL has no partial indexes and keeps the existing full index.
2026-09-04 09:28:29 +02:00
kolaente 784f388252 fix(migration): serialize imports per user 2026-08-31 18:37:03 +02:00
kolaente cf4c59fee1 feat(subscriptions): enforce one subscription row per user and entity
The opt-out design assumes at most one row per (entity_type, entity_id, user_id),
but the table never had a constraint saying so - the original migration declared
only plain indexes. Delete inserts as well as deletes now, so concurrent requests
could write duplicates.

Existing duplicates are cleared first, keeping the lowest id. That loses nothing:
muted ships in this same release and defaults to false, so every duplicate today
is a set of identical subscriptions left by the old check-then-insert race.

The unique(entity_user) tag gives fresh installs the same constraint through the
model sync; the index name matches what xorm derives from it so a later sync does
not create a second copy.
2026-08-29 23:42:41 +02:00
kolaente 424befb4ec feat(subscriptions): add muted column to subscriptions
Prepares an explicit opt-out row so a user can unsubscribe from an entity they
are only subscribed to through a parent project.
2026-08-29 23:42:41 +02:00
kolaente a8d4a2b4b0 perf(tasks): add composite index on (done, due_date)
The home overview filters done = false over a due_date range and sorts by
due_date, but tasks only had single-column indexes. A composite lets the
planner walk the index in due_date order and stop at the LIMIT instead of
sorting every open task.

Leaves out a third `id DESC` column: DESC-direction indexes are awkward
across MySQL/PostgreSQL/SQLite and ties on an exact due_date are rare
enough for incremental sort to handle.
2026-08-29 17:50:20 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>kolaente
972eab94a2 fix(deps): update module github.com/yuin/goldmark to v2 (#3627)
This PR contains the following updates:

| Package | Change |
[Age](https://docs.renovatebot.com/merge-confidence/) |
[Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [github.com/yuin/goldmark](https://redirect.github.com/yuin/goldmark)
| `v1.8.2` → `v2.0.0` |
![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fyuin%2fgoldmark/v2.0.0?slim=true)
|
![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fyuin%2fgoldmark/v1.8.2/v2.0.0?slim=true)
|

---

> [!WARNING]
> Some dependencies could not be looked up. Check the [Dependency
Dashboard](../issues/438) for more information.

---

### Release Notes

<details>
<summary>yuin/goldmark (github.com/yuin/goldmark)</summary>

###
[`v2.0.0`](https://redirect.github.com/yuin/goldmark/releases/tag/v2.0.0)

[Compare
Source](https://redirect.github.com/yuin/goldmark/compare/v1.8.5...v2.0.0)

- initial official release of v2

###
[`v1.8.5`](https://redirect.github.com/yuin/goldmark/releases/tag/v1.8.5)

[Compare
Source](https://redirect.github.com/yuin/goldmark/compare/v1.8.4...v1.8.5)

- fix:
[#&#8203;568](https://redirect.github.com/yuin/goldmark/issues/568)

###
[`v1.8.4`](https://redirect.github.com/yuin/goldmark/releases/tag/v1.8.4)

[Compare
Source](https://redirect.github.com/yuin/goldmark/compare/v1.8.3...v1.8.4)

fix: disable svg in data:image urls

###
[`v1.8.3`](https://redirect.github.com/yuin/goldmark/releases/tag/v1.8.3)

[Compare
Source](https://redirect.github.com/yuin/goldmark/compare/v1.8.2...v1.8.3)

**Full Changelog**:
<https://github.com/yuin/goldmark/compare/v1.8.2...v1.8.3>

- fix:
[#&#8203;556](https://redirect.github.com/yuin/goldmark/issues/556)

</details>

---

### Configuration

📅 **Schedule**: (UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/go-vikunja/vikunja).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC40Ni4wIiwidXBkYXRlZEluVmVyIjoiNDQuNDYuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiZGVwZW5kZW5jaWVzIl19-->

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: kolaente <k@knt.li>
2026-08-27 20:34:29 +00:00
kolaente b069439c02 fix(projects): backfill is_archived for descendants of archived projects 2026-08-20 11:58:06 +02:00
kolaente 7677e70419 feat(user): add pending_email column
Stores a new address awaiting confirmation. A direct email update via
UpdateUser (admin, LDAP/OIDC sync) supersedes and clears it.
2026-08-18 22:03:38 +02:00
kolaente 88e8bcf250 feat: drop columns for real in SQLite migrations
SQLite supports ALTER TABLE DROP COLUMN since 3.35 and the bundled
driver ships 3.50.x, so dropTableColum no longer needs to skip SQLite
with a warning that leaves dead columns behind.
2026-08-05 19:35:11 +02:00
kolaente 7eeb88da23 fix(migration): repair kanban views without a bucket configuration mode
Views broken by the bug above keep their state until something writes
them again, so repair them on startup: set the manual mode, seed the
default buckets when the view has none and place the project's tasks in
the default bucket.

Views whose project or saved filter is gone are skipped rather than
repaired with a dangling creator, soft-deleted tasks stay out of the
backfill, and the mode flip is the last write per view so an
interrupted run picks the view up again.
2026-08-04 01:09:10 +02:00
kolaente c385c0e72a feat(tasks): generalize task creation to batches
createTask becomes a wrapper around createTasks, which creates a whole
batch in one pass: project, creator, views and default buckets are
looked up once, indexes are assigned from a single max-index query
(preset indexes kept when free, collisions get the next free one), and
explicitly provided buckets are resolved once per distinct bucket —
verified to belong to the target project and checked against their
limit with the batch's own members counted. The row insert stays per
task because multi-row inserts don't reliably return autoincrement ids
on all supported databases.

Validation errors always carry the payload index; the single-create
wrapper unwraps them so existing callers keep their raw error types.

A new TasksBatchCreatedEvent fires once per batch (single create is a
batch of one) for listeners which can process all new tasks in one
pass; per-task TaskCreatedEvent semantics stay unchanged for webhooks,
mentions and audit.

Split for reviewability: builds together with the follow-up commit
adding the BulkTaskCreation model and error types.
2026-08-02 16:21:50 +02:00
kolaenteandkolaente de7a039068 fix(notifications): delete stored notifications of unknown types
349e6a590 renamed ListCreatedNotification's name from list.created to
project.created without rewriting the notifications.name column, so instances
upgraded from 0.19 or earlier still hold list.created rows carrying a full
project payload. Nothing can hydrate them, so nothing can scope them either,
and the read paths pass unrecognised names through unfiltered — leaking the
project's title, description, identifier, colour and owner to users since
removed from it.

Retaining rows no type can render only preserves that leak, so they go.
2026-07-29 07:58:17 +00:00
kolaenteandkolaente 0638200ac0 fix(notifications): check project access when reading notifications
Notification rows outlived access. A subscription survives a project being
unshared, so every notification already written for a revoked user stayed
readable — comment bodies, task titles, project names, deletion notices. The
read paths filtered on notifiable_id alone, with no permission check anywhere.
#3325 stopped the sender writing new ones; this is the other half.

The project a notification is about is persisted on the row when it is written
and the read paths filter on it in SQL, so LIMIT, OFFSET and total are all
computed on the filtered set. Notification types declare their project through
a capability interface in pkg/notifications, the same way they already declare
SubjectID, ThreadID and ToTitle — which is what lets the package below
pkg/models stay ignorant of what a project is.

project_id 0 means account-scoped and always visible, a positive value is
checked against the projects the caller can read, and -1 marks a project-scoped
row whose project could not be determined, so it is visible to nobody. Filtering
reuses the existing accessibleProjectIDsSubquery, so the page query and the
count cannot drift apart. A migration backfills existing rows from their stored
payloads, resolving through soft-deleted tasks so task.deleted rows still land
on their project.

Covers every read path: the v1 and v2 list endpoints, mark-as-read (which
echoes the payload back), the Atom feed, and the websocket push — the last of
which is load-bearing, since a row is still written for a revoked subscriber.
Deliberately no instance-admin bypass: notifications are always the caller's
own, and being an admin says nothing about whether they should still read a
comment out of a project they were removed from.
2026-07-29 07:58:17 +00:00
TinkandGitHub c4e40567b4 fix(migration): detect sqlite indexes created with lowercase SQL (#3354) 2026-07-29 09:21:25 +02:00
kolaenteandkolaente e684ac06c5 ci(lint): forbid plain Sync in migrations via forbidigo
tx.Sync/tx.Sync2 in pkg/migration now fails lint; brand-new-table
migrations and the fresh-install initSchema carry an explicit nolint
2026-07-26 13:42:33 +00:00
kolaenteandkolaente 957b114724 fix(migration): recreate indexes dropped by partial-struct sync migrations
Recreates model-declared indexes that are missing, matching by column
set so converted databases with differently-named equivalent indexes
are left alone. Fails with an actionable message if duplicate values
block recreating a unique index.

Fixes #3244
2026-07-26 13:42:33 +00:00
kolaenteandkolaente 73bca9344a fix(migration): stop existing-table struct syncs from dropping indexes
xorm's Sync drops every index and unique constraint the synced struct
doesn't declare (#3244). Adds a partialSync helper (no drops) and
converts all 65 existing-table struct syncs to it — the four v2.4.0
migrations that wiped users/tasks indexes on every upgrade, plus every
historical one back to 2019, each a latent index wipe and a pgloader
2BP01 abort point for installs upgrading from versions predating it.
2026-07-26 13:42:33 +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
kolaente 00fd2c6155 fix(security): clear plaintext user tokens for reset/confirm/deletion (GHSA-r6w9-259g-gwrv) 2026-07-19 18:59:34 +02:00
Evan BakerandGitHub d0dad0437f fix(user): stop double-encoding FrontendSettings on UpdateUser (#2793) 2026-07-19 12:13:35 +02:00
TinkandGitHub b396bad3bb fix(postgres): resolve all statements against the configured schema (#3156) 2026-07-11 18:02:57 +02:00
TinkandGitHub d83e90541c feat(tasks): soft-delete tasks with permanent deletion after 30 days (#3119) 2026-07-07 11:38:33 +02:00
TinkandGitHub 7208694960 fix(auth): build OIDC end-session URL with RP-Initiated Logout params (#2943) 2026-06-19 18:27:33 +02:00
kolaenteandkolaente 647f1f4def fix(migration): fail loudly if a deduplicated position pair has no row
A pair returned by the GroupBy was just reported as duplicated, so a row
must exist. Continuing on !has would let the delete loop drop every row
for that pair without re-inserting one, silently losing positions. Abort
the migration instead.
2026-06-17 21:16:41 +00:00
kolaenteandkolaente a61e594952 fix(tasks): prevent duplicate task_positions rows and stale identifiers
A task could end up with more than one task_positions row for the same
(task_id, project_view_id): rapid/concurrent creation raced the
check-then-insert paths, and the create path could insert a position that
a triggered RecalculateTaskPositions had already persisted for the new
task. The table had no unique constraint, so the duplicates were stored
silently (#2844).

In the table view this made the LEFT JOIN on task_positions emit the task
twice; getTasksForProjects enriched only the map entry, so the duplicate
slice row kept an empty identifier and rendered as "#N" instead of
"PREFIX-N" (#2725).

- Add a unique index on task_positions(task_id, project_view_id) via a
  dedup migration (mirrors the task_buckets fix in 20250624092830) plus the
  unique(task_view) struct tag so fresh installs get it too.
- Harden the create path: only queue a position insert when one does not
  already exist for the task+view, and dedupe within the batch.
- Dedupe the task slice returned by getTasksForProjects by id, returning
  the enriched entry, so duplicate position rows can never surface a task
  twice or with a missing identifier.

Fixes #2844
Fixes #2725
2026-06-17 21:16:41 +00:00
kolaenteandkolaente 6387d8138a feat(time-tracking): add the time_entries table migration 2026-06-08 13:54:09 +00:00
kolaenteandGitHub 21ce33f8fd feat(projects): always store identifiers as uppercase (#2775) 2026-05-19 10:35:43 +02:00
kolaenteandkolaente c239834070 feat(migration): add bot_owner_id column to users 2026-05-01 14:44:10 +00:00
kolaenteandkolaente 736773ea77 feat(db): add is_admin column to users 2026-04-20 18:55:06 +00:00
kolaenteandkolaente 35f183979c feat: add license comments for agents and humans 2026-04-15 10:32:37 +00:00
kolaenteandkolaente a82bea567a feat(db): add license_status table migration
Add database migration for the license_status table that stores instance
ID, cached license validation response, and validation timestamps.
2026-04-15 10:32:37 +00:00
kolaenteandkolaente ed2632ddb2 feat(license): add license key validation package
Implement the license validation system with:
- Server communication with retry logic and exponential backoff
- In-memory state management for feature flags and user limits
- Cached validation with 72h expiry stored in database
- Background goroutine with adaptive check intervals (24h/1h)
- Graceful degradation to community mode on failure
- Instance ID generation and persistence
2026-04-15 10:32:37 +00:00
kolaenteandkolaente 9206f98d64 feat(tasks): enforce unique (project_id, index) via migration 2026-04-11 20:44:28 +00:00
kolaenteandkolaente 71282dcffd feat: add OAuth 2.0 authorization code model and migration
Add the OAuthCode model for storing short-lived authorization codes
with PKCE challenges. Codes are hashed (SHA-256) before storage and
are single-use with a 10-minute expiry. Add the database migration
and OAuth-specific error types.
2026-03-27 23:05:04 +00:00
kolaenteandkolaente 867c52745f fix: use MySQL-compatible CREATE INDEX in migration 20260224215050
MySQL does not support CREATE INDEX IF NOT EXISTS syntax. Switch on
database type to use IF NOT EXISTS for Postgres/SQLite and plain
CREATE INDEX with duplicate key error suppression for MySQL.

Fixes #2431
2026-03-23 16:45:50 +00: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