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.
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
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).
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.
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
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.
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.
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
- 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>
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.
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).
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
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.
When a new project was created, it contained all details already. This led to duplicated views and overridden attributes in the response.
Resolves https://kolaente.dev/vikunja/vikunja/issues/2242