Commit Graph

109 Commits

Author SHA1 Message Date
kolaente 5236e0c306 fix(notifications): use full user so notifications show display name
Notifications and emails showed the acting user's auto-generated
username instead of their display Name.

The doer attached to notification events was built straight from the
JWT via user.GetFromAuth, which only carries id + username (Name is
never set in GetUserFromClaims). Notifications render n.Doer.GetName(),
which falls back to the username when Name is empty, so every "assigned
you", "mentioned you", task-deleted, project-created and team-member
notification rendered the username.

Resolve the full user from the database at the event-producing dispatch
sites. doerFromAuth now re-fetches the user (with Name) and is reused by
all the notification doers; account-status errors are swallowed so flows
acting on behalf of disabled accounts (e.g. user deletion deleting that
user's tasks) keep working while still carrying the display name.

Fixes #2720
2026-06-18 20:57:05 +00:00
kolaente 7b7c850dd8 refactor(tasks): drop in-memory task dedup, rely on unique index
The duplicate task rows getTasksForProjects deduplicated came from the
LEFT JOIN multiplying when duplicate task_positions rows existed. The new
unique index on (task_id, project_view_id) removes the root cause at the
SQL layer (the migration also runs before serving), so the join can no
longer multiply. Revert getTasksForProjects and getRawTasksForProjects to
their pre-dedup shape.
2026-06-17 21:16:41 +00:00
kolaente 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
kolaente f0eff52949 fix(events): build event doers without re-fetching the user
GetUserOrLinkShareUser re-fetches the account and fails its status
check, which broke deleting a disabled user's projects (the deletion
runs with the disabled account as doer). Convert the authenticated
principal directly instead — it also matches what the events serialized
before the doer became concrete, and drops a query per event.
2026-06-12 08:56:08 +00:00
kolaente b3bcab1f72 refactor(events): use a concrete doer on project and team events
ProjectUpdated/Deleted, ProjectSharedWith* and TeamCreated/Deleted
carried an interface-typed Doer that could not be unmarshaled, forcing
the audit registrations to decode anonymous mirror structs. Hydrate the
doer via GetUserOrLinkShareUser at the dispatch sites like the task
events already do, register the events directly and drop the untyped
audit registration path.

Webhook payloads for these events now serialize link share doers as
their pseudo-user (negative id) instead of the raw link share object,
consistent with task events.
2026-06-12 08:56:08 +00:00
kolaente 5cdc785b49 fix(api/v2): return ErrProjectDoesNotExist for unknown project identifiers 2026-06-10 10:12:09 +00:00
kolaente 82ad23c135 feat(api/v2): gate admin routes by feature + instance admin
Add the admin + license gate for /api/v2 and ship the first gated
resource, GET /api/v2/admin/projects (AdminProjectList).

The gate reuses the existing v1 middleware functions unchanged —
RequireFeature(license.FeatureAdminPanel) and RequireInstanceAdmin(),
both of which serve 404 on failure. Rather than splitting the single
v2 Huma API into a separate gated sub-group (which would split the
OpenAPI spec and drop admin operations from /api/v2/openapi.json), the
gate is applied as a path-scoped Echo middleware on the shared /api/v2
group, firing only for /api/v2/admin/* and after the token middleware.
This preserves v1's 404-not-403 semantics and keeps admin routes in the
unified v2 spec and Scalar docs.

AdminProjectList lists every project on the instance (archived
included), behind the gate. Adds doc:/readOnly: tags to the shared
Project model so it documents correctly as a v2 schema.

Tests in pkg/webtests/huma_admin_test.go (TestHumaAdminProjects) cover
all three personas: non-admin -> 404, admin without feature -> 404,
admin with feature -> 200 list, plus unauthenticated -> 401.
2026-06-02 07:38:08 +00:00
kolaente 21ce33f8fd
feat(projects): always store identifiers as uppercase (#2775) 2026-05-19 10:35:43 +02:00
kolaente d24b96b99c feat(user): extract last-admin guard and close invariant gaps 2026-04-20 18:55:06 +00:00
kolaente 5b2cbcb1b5 fix(project): replace CAST(... AS int) with CASE WHEN for MySQL 8 compat
MySQL 8 rejects CAST(... AS int) (only SIGNED/UNSIGNED/CHAR/... are
accepted as target types), causing /api/v1/projects, /api/v1/tasks,
and /api/v1/labels to return HTTP 500 for every authenticated user on
MySQL 8. SQLite, Postgres, and MariaDB lax mode silently accepted the
expression, which is why the regression (introduced in e3045dfd0,
shipped in v2.3.0) passed CI — the mysql CI matrix leg uses
mariadb:12, not real MySQL 8.

Replace the two CAST(all_projects.is_archived AS int) expressions in
the recursive project CTE with MAX(CASE WHEN ... THEN 1 ELSE 0 END),
which is dialect-agnostic and needs no cast on any supported backend.

Fixes #2589
2026-04-11 17:20:53 +00:00
kolaente 75e1f72c6e fix(security): move reparent Admin gate into UpdateProject
GHSA-2vq4-854f-5c72 / CVE-2026-35595: the recursive permission CTE
cascades Admin from any owned ancestor, so a user with Write on a
shared project could reparent it under an attacker-owned root and
resolve as Admin on the moved project via the new parent.

Require Admin on both the moved project and the new parent whenever
parent_project_id is set to a non-zero value that differs from the
stored value. The gate lives in UpdateProject rather than CanUpdate
because CanUpdate is reused by permission-check-only callers
(buckets, webhooks, task ops) that pass stub &Project{ID:...} values
with ParentProjectID=0 and never commit a reparent — gating there
would spuriously trip the check for every such call.

Only non-zero ParentProjectID is gated: the generic update handler
binds a fresh struct, so an omitted parent_project_id is
indistinguishable from an explicit 0. Detach-to-root via the generic
endpoint is therefore out of scope for this fix and is tracked as a
follow-up (needs a pointer field to disambiguate).
2026-04-09 16:47:35 +00:00
kolaente bf86bee690 feat(models): add ClearProjectBackground for scoped column update 2026-04-08 09:07:15 +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
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 e3045dfd00 fix: propagate is_archived from parent to child projects in ReadAll CTE
Replace the Go-side propagateArchivedState function with in-CTE
propagation. The recursive SELECT uses (ap.is_archived OR p.is_archived)
to inherit archived state from parent projects. The outer query uses
GROUP BY with MAX(CAST(is_archived AS int)) to handle projects
accessible via both direct permissions and parent traversal. When
getArchived=false, a HAVING clause filters out archived projects.

The is_archived filter is removed from getUserProjectsStatement so
archived parents enter the CTE and propagation works correctly.
2026-03-25 09:06:33 +00:00
kolaente e2683bb2bc refactor: add accessibleProjectIDsSubquery helper for project-level authz filtering 2026-03-23 16:26:37 +00:00
kolaente d0606eadea fix: check child project's own IsArchived flag in CheckIsArchived
CheckIsArchived() previously skipped checking a child project's own
IsArchived flag when ParentProjectID > 0, immediately recursing to
only check the parent. This allowed write operations on individually
archived child projects whose parent was not archived.

Now the function loads the project from the database first, checks its
own IsArchived flag, and only then recurses to check parent projects.
2026-03-23 14:13:53 +00:00
kolaente dd7f7de518 fix(events): defer event dispatch for project operations
Convert events.Dispatch to events.DispatchOnCommit in project CRUD,
project-user sharing, and project-team sharing.

Refs #2315
2026-03-03 12:46:34 +01:00
kolaente 49bba7f830 fix: eliminate nested database sessions to prevent table locks
Refactor functions that created their own sessions when called from
within existing transactions, which caused "database table is locked"
errors in SQLite's shared-cache mode.

Changes:
- Add files.CreateWithSession() to reuse caller's session
- Refactor DeleteBackgroundFileIfExists() to accept session parameter
- Add variadic session parameter to notifications.Notify() and
  Notifiable.ShouldNotify() interface
- Update all Notify callers (~17 sites) to pass their session through
- Use files.CreateWithSession in SaveBackgroundFile and NewAttachment
- Fix test code to commit sessions before assertions
2026-02-25 11:03:02 +01:00
kolaente c9c250fb1c fix: add missing Commit() to write callers
After NewSession() auto-begins a transaction, callers that perform
writes must explicitly call Commit() for changes to persist. Without
this, writes are silently rolled back when Close() is called.

Affected callers:
- user deletion notification cron
- caldav token generation/deletion
- token cleanup cron
- mark-all-notifications-read endpoint
- saved filter view cron
- project background delete
- typesense reindex
- export cleanup cron
- task last-updated listener
- saved filter view listener
- SSO team cleanup cron
- migration status start/finish
- background set/remove handlers
- orphaned task position cleanup
- file creation
2026-02-25 11:03:02 +01:00
Samuel Rodda 3a47c062da
fix(filters): preserve IsFavorite for saved filters in ReadOne (#2031)
- Saved filters' `IsFavorite` field was not being properly returned when
fetched as pseudo-projects via `/projects/{id}`
- This caused favorited filters to appear in both the Favorites and
Filters sections initially, but then disappear from Favorites after
clicking on them (navigating to the filter)

Fixes #1989

Co-authored-by: iamsamuelrodda <iamsamuelrodda@users.noreply.github.com>
2026-01-06 15:17:17 +00:00
kolaente a81a3ee0e5
feat!: rename right to permission (#1277) 2025-08-13 11:05:05 +02:00
kolaente 07d83e67d7 feat(projects): add support for ParadeDB when searching for project 2025-07-18 23:18:04 +02:00
kolaente a8025a9e36
fix: guard invalid user lookups (#1034) 2025-06-26 21:58:47 +00:00
Dominik Pschenitschni 296577a875
fix: correct license header references (#882)
See originals:
- https://www.gnu.org/licenses/agpl-3.0.txt
- https://www.gnu.org/licenses/gpl-3.0.txt
2025-06-10 12:18:38 +02:00
kolaente 9f30a099ee
fix(projects): (un-)archive child projects when archiving parent (#775) 2025-05-15 14:31:56 +00:00
renovate[bot] 4a75f7d4da
chore(deps): update golangci/golangci-lint-action action to v7 (#462)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: kolaente <k@knt.li>
2025-04-02 09:28:56 +02:00
kolaente 75bd569a93
fix(projects): return list of projects when accessing as link share 2025-01-24 11:46:45 +01:00
kolaente 7f6cb1e06e
feat: expand buckets 2025-01-24 11:03:40 +01:00
kolaente f6bfe2f13c
fix(quick actions): show saved filters in search results
This fixes a bug where saved filters would not show up in quick actions search results. It was caused by explicitely only searching for projects.
During the debugging of this, I've discovered the search parameter is ignored when searching for filters.

Resolves https://community.vikunja.io/t/virtual-projects-a-k-a-saved-filters-do-not-show-up-in-search-bar-results/3180/4
2025-01-21 11:07:28 +01:00
kolaente 7ac2c42e4d
fix(caldav): fetch saved filter
This fixes a bug which caused fetching saved filter and favorite projects to crash, because the respective project ID is not a valid project id without special handling.
2024-12-11 17:05:51 +01:00
graves501 ed5474a28a fix(typo): Simpl -> Simple -> GetProjectsMapSimpleByTaskIDs (#2906)
Fix typo -> GetProjectSimplByTaskID -> GetProjectSimpleByTaskID

Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/2906
Co-authored-by: graves501 <graves501@protonmail.com>
Co-committed-by: graves501 <graves501@protonmail.com>
2024-12-09 13:55:16 +00:00
kolaente 624907ad6a fix: make search in saved filter work 2024-11-19 16:27:22 +00:00
kolaente d081cf80fc
chore(caldav): refactor fetching projects 2024-10-30 08:37:27 +01:00
kolaente 412a8ef654
chore(projects): only pass users to checks 2024-10-29 09:11:06 +01:00
kolaente dace07efda
fix(projects): return 0 if no parent project exists 2024-10-29 08:25:20 +01:00
kolaente e30374a28c
fix(projects): only add conditions to query when they are non-empty 2024-10-28 16:09:31 +01:00
kolaente b013b294e2
fix(projects): remove unnecessary join
The removal of the unnecessary join condition speeds up the query 10x. Before, it would take ~700ms on Vikunja Cloud. With this removal, the otherwise same query now takes ~70ms (which still leaves plenty of room for improvements, but it's already a great step forwards).
2024-10-28 15:58:07 +01:00
kolaente 92b90013ab
feat(projects): optionally return max right when querying all projects
This change introduces an expand query parameter which, when provided, allows to return all projects with the max right the current user has on that project. This allows to show and hide appropriate buttons in the frontend.

Resolves https://github.com/go-vikunja/vikunja/issues/334
2024-10-13 19:22:09 +02:00
kolaente 8d730543c3
fix(project): reset id before creating
Resolves https://vikunja.sentry.io/share/issue/6f1e37d4b8b248188e20650234a45cde/
2024-09-11 23:46:13 +02:00
kolaente 68636f27da
fix(files): use absolute path everywhere 2024-09-06 12:59:48 +02:00
kolaente 4ff8815fe1
fix(subscriptions): cleanup and simplify fetching subscribers for tasks and projects logic
Vikunja now uses one recursive CTE and a few optimizations to fetch all subscribers for a task or project. This makes the relevant code easier to maintain and more performant.
2024-09-04 19:55:17 +02:00
kolaente 2063da9eec
chore(web): move web handler package to Vikunja 2024-08-29 16:15:28 +02:00
kolaente ef0bf3156e
fix(project): do not use project id of nil project in error 2024-07-18 16:39:30 +02:00
kolaente 5644130f01
fix(filters): do not create a default filter for list view when creating a saved filter
Because a default filter in a filter is highly unexpected, it can lead to these filters cancelling each other out, resulting in confusing behaviour. See https://kolaente.dev/vikunja/vikunja/issues/2547#issuecomment-65180
2024-07-11 12:29:09 +02:00
kolaente d12deee977
feat(views): allow reordering views
Resolves https://community.vikunja.io/t/reordering-views/2394
2024-06-18 16:39:52 +02:00
kolaente d8ca1a2de1
fix(favorites): make favorites work with configurable views 2024-04-14 17:12:16 +02:00
kolaente e9de7d8a24
fix(project): delete all related entities when deleting a project 2024-04-13 21:43:44 +02:00
kolaente ce1d7778c7
fix(export): make export work with project views and new task positions 2024-04-13 21:07:06 +02:00
kolaente 500b761fe6
fix(projects): do not return parent project id when authenticating as link share
Related to https://community.vikunja.io/t/vikunja-freezes/2246
Related to https://github.com/go-vikunja/vikunja/issues/233
2024-04-12 18:02:39 +02:00