New CI job runs after os-package and desktop jobs complete. Downloads
all package artifacts, runs Mage repo targets to generate repository
metadata (APT, RPM, APK, Pacman), GPG-signs the metadata, and uploads
to R2 under /repos/.
Publishes to stable suite for tagged releases, unstable for main
branch builds. Artifact uploads from os-package and desktop jobs are
no longer gated on tags to support this.
Add four new Release namespace targets:
- release:repo-apt — generates APT repo metadata using reprepro
- release:repo-rpm — generates RPM repo metadata via createrepo_c
- release:repo-apk — generates Alpine APK index via apk index + abuild-sign
- release:repo-pacman — generates Pacman database via repo-add
All targets read REPO_SUITE env var (stable/unstable, default stable)
to support publishing to different repository suites.
When a user has configured default reminders in their frontend settings,
those are cloned onto every task created via quick-add magic that has a
parsed due date. Tasks without a due date are silently skipped.
The buildDefaultRemindersForQuickAdd helper is exported as a pure
function so it can be unit-tested without stubbing the Pinia store.
Adds the settings field next to the quick add magic mode select, hidden
when that mode is set to Disabled. Uses Reminders.vue directly with
allow-absolute=false and default-relative-to pinned to due date.
Reminders.vue only read three task fields (dueDate/startDate/endDate)
and wrote one back (reminders). The ITask coupling was accidental.
Flip the prop to ITaskReminder[] and pass defaultRelativeTo / allowAbsolute
as plain props. TaskDetailView now owns the due/start/end priority
computation and binds v-model="task.reminders" directly. This also lets
the settings page reuse Reminders.vue for configuring default reminders.
Adds an allowAbsolute prop to ReminderDetail that hides the 'Date and
time' option when set to false, and a lockRelativeTo prop on
ReminderPeriod that hides the relativeTo select and forces the chosen
value. ReminderDetail threads them together so that when absolute
reminders are disallowed, the Custom form can only produce reminders
that anchor to defaultRelativeTo.
Multiple TaskFactory.create(1, {id: N, ...}, false) calls for the same
project were all defaulting to index=1 (from {increment} with count=1),
which collides on the newly added UNIQUE(project_id, index) constraint.
Mirror the numeric id override to index so each row stays unique and
matches the id == index convention used by raw seedTasks helpers.
Fixes the e2e playwright seed failures in subtask-duplicates, list/table
filter/search, kanban filter/search, and overview specs.
The four boolean OIDC provider fields (emailfallback, usernamefallback,
forceuserinfo, requireavailability) were parsed with a strict .(bool)
type assertion. That works for YAML/JSON config where leaves are native
bools, but fails for every other input path: env vars always arrive as
strings, and GetConfigValueFromFile (used by the *.file Docker secret
convention) also always returns strings. The assertion would silently
zero the field for emailfallback and usernamefallback, and log an error
and zero the field for forceuserinfo and requireavailability, which is
what #2599 reports.
Extract a small parseBoolField helper that accepts both native bools and
strings (via strconv.ParseBool) and logs a parse error from each call
site. This also fixes the previously-silent drop of stringified
emailfallback / usernamefallback values — those now log an error if the
input is garbage, matching the behaviour of the other two fields.
Fixes#2599
Regression test for #2599. Exercises getProviderFromMap with native
bools and with stringified booleans ("true"/"false"/"1"/"0") for all
four boolean provider fields — emailfallback, usernamefallback,
forceuserinfo, requireavailability. From env vars and from the
GetConfigValueFromFile path every leaf arrives as a string, so the
current .(bool) assertion silently zeros these fields.
Re-check props.enabled inside the dialogRef watcher. The watcher fires
once Vue mounts the <dialog>, but the caller may have flipped enabled
back to false between the openDialog() call and the mount flush. In that
case the prop state is disabled and we must not open the dialog.
Addresses augmentcode review on #2604.
If the modal is re-enabled within the 150ms close transition the
<dialog> element is still mounted and [open], so the dialogRef watcher
does not re-fire. Clear the leftover data-closing flag directly in
openDialog() so the dialog doesn't remain stuck at opacity 0.
Addresses augmentcode review on #2604.
Replace the nextTick-based showModal() call with a watch on the template
ref so the dialog is opened exactly when the <dialog> element mounts.
The previous implementation could silently skip showModal() if the mount
was deferred past the first nextTick, leaving the dialog in the DOM with
opacity: 0 and no click target. Observed in the Vikunja Desktop v2.3.0
Electron build where the search (quick actions) button was unresponsive.
Closes#2590
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
Regression test for #2589. Locks the contract that getAllProjectsForUser
exposes inherited is_archived for child projects of archived parents and
filters them out when getArchived=false, exercising both the MAX(...)
column expression and the HAVING MAX(...) = 0 filter.
Uses actions/ai-inference with GPT-5 to classify newly opened issues
and pull requests against the area/*, integration/*, db/*, and
concern/* label namespaces.
The system prompt is rendered at runtime from the live repo label list
plus descriptions, so GitHub label state is the single source of truth
for the taxonomy. Suggested labels are re-validated against the live
list before being applied, capped at 6 per item.
The nixpkgs update.py script prompts for confirmation before running,
which fails with EOFError in CI since there is no TTY. Pass skip-prompt
to maintainers/scripts/update.nix to bypass the prompt.
When the backend reports that 2FA is required (412/1017), the OIDC
callback view now shows a TOTP input and restarts the OIDC dance
with the typed passcode stashed in localStorage so it can be
submitted alongside a fresh authorization code.
Refs GHSA-8jvc-mcx6-r4cg
The OIDC callback handler previously issued a JWT without ever
checking TOTP state. For installations with EmailFallback (or
UsernameFallback) enabled, this allowed an attacker who could
authenticate at the IdP with a matching email to log in as a local
user with TOTP enrolled, bypassing the second factor entirely.
HandleCallback now runs enforceTOTPIfRequired after resolving the
user and before any team sync writes, returning 412/1017 when the
passcode is missing or invalid. Clients resubmit the OIDC flow with
the totp_passcode field populated.
Fixes GHSA-8jvc-mcx6-r4cg
Extracts a TOTP gate that the OIDC callback will use to enforce 2FA
for users with TOTP enabled. Mirrors the local-login TOTP flow in
pkg/routes/api/v1/login.go. Not yet wired into HandleCallback.
Refs GHSA-8jvc-mcx6-r4cg
Covers the four states the OIDC TOTP gate must handle: user without
TOTP, TOTP enabled with missing passcode, invalid passcode, and
valid passcode. The helper function under test does not exist yet,
so the package currently fails to compile.
Refs GHSA-8jvc-mcx6-r4cg
Prepares the OIDC callback struct to carry a TOTP passcode so the
handler can enforce 2FA for users with TOTP enabled. No behaviour
change yet.
Refs GHSA-8jvc-mcx6-r4cg
The TestProject_ReadAll/search case on the ParadeDB path was still
expecting 6 results, but adding fixture project 43 (child of project
10) means the recursive CTE now pulls it in as a descendant whenever
the fuzzy search matches project 10. The non-ParadeDB branch was
already updated to account for this (+1, asserting project 43 is in
the result); the ParadeDB branch was missed.
CI was failing with "should have 6 item(s), but has 7" on the
test-api (paradedb, feature) job. Bump the expected length to 7 and
add the matching Contains assertion for project 43.
No fixture or production-code changes.
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).
Covers GHSA-2vq4-854f-5c72 / CVE-2026-35595: attackers with direct or
inherited Write on a project must not be able to reparent it under their
own tree nor detach it to root. Also pins the legitimate rename-with-Write
and owner-detach flows so the upcoming fix does not regress them.
Adds project 43 as a child of project 10 so tests can exercise the
"inherited Write via parent" path exploited by GHSA-2vq4-854f-5c72.
User 1 has Write on project 10 via users_projects id=4 and therefore
inherits Write on this child via the permission CTE.
The test previously fetched the attachment from https://vikunja.io/testimage.jpg,
which caused flaky failures in CI when the external host was unreachable
(context deadline exceeded). Serve the local testimage.jpg via httptest and
temporarily allow non-routable IPs for the SSRF-safe client so the test is
hermetic and deterministic.
Builds an in-memory export zip with a 2 MB payload and a data.json
that claims size: 0, then asserts neither the honest 2 MB row nor
the forged 0-size row ends up in the files table. Covers
GHSA-qh78-rvg3-cv54.
The hard-coded 500 MB per-entry cap meant operators who set a tighter
files.maxsize could not actually enforce it on imports. Derive the cap
from files.maxsize with a floor so data.json / filters.json / VERSION
entries can still be read when the configured limit is tiny.
Clamp the uint64->int64 conversion and the LimitReader cap so absurd
configuration values do not overflow into MinInt64 and cause
io.LimitReader to treat every entry as EOF.
Import metadata is attacker-controlled and can forge a small size to
bypass the attachment size limit (GHSA-qh78-rvg3-cv54). Compute the
size from the decoded content instead of trusting a.File.Size.
Task/project duplication and the Todoist migration were passing stored
or API-reported sizes into NewAttachment. Derive the size from the
actual buffered content so every caller matches the hardened boundary
behaviour (GHSA-qh78-rvg3-cv54 defence-in-depth).