RegisterSessionCleanupCron opens a transaction via db.NewSession() but
never calls s.Commit(). The deferred s.Close() auto-rolls-back, making
the DELETE a no-op. Add the missing commit.
- user_export.go: Remove defer s.Close() from checkExportRequest since
it returns the session to callers. Callers now own the session
lifecycle with their own defer s.Close(). Close session on all error
paths within checkExportRequest.
- user_delete.go: Close the read session immediately after Find() before
the per-user deletion loop, avoiding a long-lived transaction holding
locks unnecessarily.
- user/delete.go: Remove double s.Close() in notifyUsersScheduledForDeletion
by closing immediately after Find() instead of using both defer and
explicit close.
- caldav_token.go: Return nil token on Commit() error to prevent callers
from using an unpersisted token.
syncUserGroups created its own db.NewSession() internally while being
called from AuthenticateUserInLDAP which already has an active session
with writes. In SQLite shared-cache mode this causes a lock conflict.
Pass the caller's session through instead, and add s.Commit() before
db.AssertExists calls in LDAP tests.
Add a proper main_test.go for the caldav test package that initializes
the logger, config, test database, and event system. Previously, these
were initialized inline in TestSubTask_Create and TestSubTask_Update
relied on running after it (fragile test ordering).
Fix session handling in TestSubTask_Update: close the read session
before calling UpdateResource (which creates its own internal session)
to avoid SQLite lock conflicts from concurrent transactions.
Two categories of fixes:
1. Use defer s.Close() instead of explicit s.Close() to prevent session
leaks when require.FailNow() triggers runtime.Goexit(), which skips
explicit close calls but runs deferred functions. Leaked sessions
hold SQLite write locks that block all subsequent fixture loading.
2. Add s.Commit() before db.AssertExists/db.AssertMissing calls. These
assertion helpers query via the global engine (not the test session),
so they cannot see uncommitted data from the session's transaction.
For block-scoped sessions (kanban_task_bucket_test.go), wrap each block
in an anonymous function so defer runs at block boundary rather than
deferring to the enclosing test function.
files.Create() and files.CreateWithMime() internally create their own
sessions and transactions. When called from within an existing
transaction (now that db.NewSession() auto-begins), this creates nested
transactions that deadlock on SQLite.
Switch to files.CreateWithSession() and files.CreateWithMimeAndSession()
to participate in the caller's existing transaction instead.
In transaction mode, xorm stores the bean argument as a map key in
afterUpdateBeans. Since Task contains slices and maps (unhashable
types), passing a Task value causes "hash of unhashable type" panic.
Passing a pointer (&ot) fixes this since pointers are always hashable.
With db.NewSession() now starting real transactions, all sessions that
do writes must explicitly commit. These listeners and cron jobs were
previously relying on auto-commit mode where each SQL statement was
committed immediately. Without explicit Commit(), the writes are
silently rolled back on Close(), and the held write locks cause
"database is locked" errors for subsequent requests on SQLite.
On Postgres, a failed operation puts the transaction in an error state
where subsequent operations fail. The previous loop with continue would
keep trying to use a broken transaction. Each user now gets its own
transaction so a single notification failure doesn't affect others.
File.Delete() had s.Commit() and s.Rollback() calls that could
prematurely commit or abort an outer transaction when using a shared
session. The caller is now responsible for transaction management.
Verify that deleting a parent project atomically deletes all child
projects, including archived children and deeply nested hierarchies.
Also add missing defer s.Close() to existing delete test cases.
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
Since NewSession() now auto-begins a transaction, explicit Begin()
calls are redundant (xorm's Begin() is a no-op when already in a
transaction). Removing them reduces confusion.
Special case: user_delete.go's loop previously called Begin/Commit
per user on a shared session. Restructured to create a new session
per user deletion so each gets its own transaction.
Add defer s.Close() to sessions that were never closed:
- auth.GetAuthFromClaims inline session
- models.deleteUsers cron function
- notifications.notify database insert
All sessions now start with an active transaction. This makes
multi-statement write operations atomic — if any step fails, all
changes are rolled back instead of leaving the database in an
inconsistent state.
Callers must call s.Commit() for writes to persist. s.Close()
auto-rollbacks uncommitted transactions.
- Login creates a server-side session and sets an HttpOnly refresh
token cookie alongside the short-lived JWT
- POST /user/token/refresh exchanges the cookie for a new JWT and
rotates the refresh token atomically
- POST /user/logout destroys the session and clears the cookie
- POST /user/token restricted to link share tokens only
- Session list (GET) and delete (DELETE) routes for /user/sessions
- All user sessions invalidated on password change and reset
- CORS configured to allow credentials for cross-origin cookies
- JWT 401 responses use structured error code 11 for client detection
- Refresh token cookie name constants annotated for gosec G101
- Session struct with UUID primary key, hashed refresh token, device
info, IP address, and last-active tracking
- Token generation via generateHashedToken (SHA-256, 128 random bytes)
- CreateSession, GetSessionByRefreshToken, GetSessionByID
- Atomic RotateRefreshToken with WHERE on old hash to prevent replays
- ReadAll scoped to authenticated user (link shares rejected)
- Delete scoped to owning user (link shares rejected)
- Hourly cleanup cron for expired sessions based on is_long_session
- ErrSessionNotFound error type with HTTP 404 mapping
Adds ServiceJWTTTLShort (default 600s) to control the lifetime of
short-lived JWTs issued during token refresh. The existing jwtttl
and jwtttllong keys remain for session expiry and long sessions.
Adds the `sessions` table for storing server-side session records.
Each row tracks a session ID (UUID), user ID, hashed refresh token,
device info, IP address, and timestamps.
The reminder and overdue crons now always run and dispatch webhook
events. Email notifications are only sent when both
ServiceEnableEmailReminders and MailerEnabled are true. Webhook
dispatch errors are logged but no longer abort the cron run.
Avoid using the generic renameColumn helper for this migration on MySQL because it renames columns as BIGINT. Handle the teams oidc_id -> external_id rename with a MySQL-specific CHANGE statement that keeps VARCHAR(250) and remains idempotent.
The new catchup migration compared the postgres JSON column with string literals using !=, which fails in migration smoke tests. Use a PostgreSQL-specific WHERE clause with ::text and cover it with unit tests.
Convert bucket_configuration filters regardless of bucket_configuration_mode and catch remaining view-level filter values still stored in the legacy string format. Includes focused tests for the bucket conversion helper logic.
Fixes#2172Fixes#2188Fixes#2202
Use the shared renameColumn helper for non-SQLite databases and skip the SQLite table rebuild when oidc_id is already absent. This prevents failures after partial upgrades where the column was already renamed.
Refs #2172
Refs #2285
tx.Sync() fails on PostgreSQL because it tries to reconcile primary key constraints/indexes, causing index-drop errors. Use explicit ALTER TABLE ADD COLUMN with existence checks instead.
Fixes#2215
Without explicit Cache-Control headers, browsers may heuristically cache
API JSON responses. This causes stale data to be served on normal page
refresh (F5) — for example, projects newly shared with a team not
appearing until the user performs a hard refresh (Ctrl+Shift+R).
Add Cache-Control: no-store to all API responses via middleware and
configure the service worker's NetworkOnly strategy to explicitly bypass
the browser HTTP cache for API requests.
Ref: https://community.vikunja.io/t/team-members-cannot-see-project/1876
TickTick uses status "2" (Archived) for completed tasks that were
subsequently archived. The import only checked for status "1"
(Completed), causing archived tasks to be imported as open despite
having a completion timestamp.
Closesgo-vikunja/vikunja#2278
Add a CLI command that finds all files in the database with no MIME type
set, detects it from the stored file content, and updates the database.
This is useful for backfilling MIME types on files created before MIME
detection was added on upload.
Set Content-Disposition to "attachment" instead of "inline" so browsers
trigger a file download with the correct filename. Also move shared
response headers (Content-Type, Content-Length, Last-Modified) out of
the S3-specific branch so they apply to both storage backends.
Use mimetype.DetectReader in files.Create to automatically detect and
store the MIME type from file content. This fixes task attachment
previews, S3 Content-Type headers, and UI display for newly uploaded
files.
Add an OrderBy field to the TaskComment struct with a query:"order_by"
tag so that the frontend can request ascending or descending comment
order. The value is validated to only accept "asc" or "desc", defaulting
to "asc". Also adds the corresponding swagger @Param annotation.
Restore full sort-order assertions that verify task47 appears at the
end of priority-sorted results. The previous fix incorrectly removed
the trailing `]` which meant the tests no longer verified the last
element in the sorted array.
Restrict the AND-joined sub-table filter merging to range comparators
(>, >=, <, <=) only. Equality and negative comparators (=, !=, in,
not in) must remain as separate EXISTS/NOT EXISTS subqueries because
each matching value lives in its own row.
Merging equality filters like `labels = 4 && labels = 5` into a single
EXISTS would produce an unsatisfiable condition (no single row has
label_id=4 AND label_id=5). Merging negative filters like
`labels != 4 && labels != 5` into NOT EXISTS(label_id IN 4 AND
label_id IN 5) would be trivially true.
Also fix the join tracking to use the first filter's join type
(how the group connects to the previous element) instead of the last.
When multiple AND-joined filter conditions target the same sub-table
(e.g., reminders > X && reminders < Y), they are now combined into
a single EXISTS subquery so that all conditions must be satisfied by
the same row. Previously, each condition generated a separate EXISTS
subquery that could match different rows, causing false positives.
Fixes#2245
Add task47 variable (with reminders straddling the test window) and new
test cases that verify AND-joined sub-table filters match the same row.
The test "filtered reminder dates should not match task with reminders
outside window" will fail until the fix is applied.
Add a second reminder to task 2 (in 2019, outside the test window)
and create task #47 with two reminders that straddle the test window
(2018-08-01 and 2019-03-01) but neither falls inside it. This exposes
the multi-row matching bug where separate EXISTS subqueries can match
different rows in the same sub-table.
As discussed on Matrix, Vikunja currently prevents users from using LDAP
authentication if the server allows anonymous binds (common in local
environments like YunoHost). The application would previously trigger a
`log.Fatal` if `AuthLdapBindDN` or `AuthLdapBindPassword` were left
empty in the configuration.
#### **How this fixes the problem:**
* **Validation:** Removed the strict requirement for Bind credentials in
`InitializeLDAPConnection`.
* **Connection Logic:** Updated `ConnectAndBindToLDAPDirectory` to
attempt an `UnauthenticatedBind` from the `go-ldap` library when no
credentials are provided.
* **Safety:** If a Bind DN is provided, the behavior remains unchanged
(authenticated bind).
#### **Testing:**
* Tested manually on a **YunoHost** instance by replacing the binary.
* Confirmed that Vikunja now successfully starts and authenticates users
via the local LDAP (localhost) without requiring a service account.
* Added a basic unit test in `pkg/modules/auth/ldap/ldap_test.go` to
ensure the initialization logic doesn't crash with empty credentials.
*Note: This is my first contribution to a Go project (assisted by an LLM
for syntax). Feedback on code style is more than welcome!*
FlushCache was using keyvalue.Del with the base key
(avatar_upload_{userID}) but the actual cache entries are stored with
size suffixes (avatar_upload_{userID}_{size}). The Del call targeted a
key that never existed, so cached avatars were never invalidated.
Switch to keyvalue.DelPrefix to delete all size variants at once,
matching the pattern the gravatar provider already uses correctly.
The test populates the cache with multiple size-suffixed keys
and verifies that FlushCache removes all of them. Currently fails
because FlushCache uses Del with the base key which doesn't match
the actual size-suffixed cache keys.
Remove email, name, emailRemindersEnabled, and isLocalUser from user JWT
claims, and isLocalUser from link share JWT claims. These fields are never
used from the token - the backend always fetches the full user from the
database by ID, and the frontend fetches user data from the /user API
endpoint immediately after login.
Also simplify GetUserFromClaims to only extract id and username, and
remove the now-unnecessary email override in the frontend's
refreshUserInfo.
Previously, `makeLogHandler()` hardcoded "standard" as the logfile name
passed to `getLogWriter()`, causing all log categories (`database`,
`http`, `events`, `mail`) to write to `standard.log` instead of their
own files.
Add a logfile parameter to `makeLogHandler()` so each caller specifies
its category name, producing `database.log`, `http.log`, `echo.log`,
`events.log`, and `mail.log` as expected.
Fixes https://github.com/go-vikunja/vikunja/issues/2177
Adds `files.s3.disablesigning` config option that sends
`UNSIGNED-PAYLOAD` instead of computing SHA256 hashes for S3 request
signing which fixes `XAmzContentSHA256Mismatch` errors with
S3-compatible providers like Ceph RadosGW and Clever Cloud Cellar
Resolves https://github.com/go-vikunja/vikunja/issues/2181
Use a temp file instead of io.ReadAll to avoid buffering the entire
Unsplash image in RAM, which could cause OOM with large images or
high maxsize configuration.
Move the seek-to-start into the local file branch only and simplify
contentLengthFromReadSeeker to seek to end then back to 0 instead of
saving/restoring the original position. This reduces the S3 upload
path from 5 seek operations to 2.
Use a temporary file instead of io.ReadAll when restoring attachments
from a dump. This prevents loading entire files into memory, which could
cause OOM errors for large attachments during restore.
- Replace custom testfile structs with bytes.NewReader
- Remove readerOnly wrapper and non-seekable reader tests (no longer
possible at the type level)
- Update S3 unit tests to remove temp file assertions
Update all code paths that pass file content to the storage layer to
provide io.ReadSeeker instead of io.Reader:
- Avatar upload: use bytes.NewReader instead of bytes.Buffer
- Background upload handler: use bytes.NewReader instead of bytes.Buffer
- Unsplash background: buffer response body into bytes.NewReader
- Dump restore: buffer zip entry into bytes.NewReader
- Migration structure: pass bytes.NewReader directly instead of wrapping
in io.NopCloser
- Task attachment: change NewAttachment parameter from io.ReadCloser to
io.ReadSeeker
The S3 upload path used temp files (vikunja-s3-upload-*) to buffer
non-seekable readers. In Docker containers with restrictive permissions,
these temp files could not be created, causing "permission denied"
errors for avatar and background image uploads.
By changing the file storage API (Create, CreateWithMime,
CreateWithMimeAndSession, Save) to require io.ReadSeeker instead of
io.Reader, the temp file fallback is no longer needed and is removed.
This enforces at the type level that all callers provide seekable
readers, preventing this class of bug from recurring.
Closesgo-vikunja/vikunja#2185
Add DatabasePathConfig struct and ResolveDatabasePath function that
takes all dependencies as parameters, making it easier to test path
resolution logic in isolation. Should also fix the reported cases.
Resolves#2189
This PR adds support for detecting and handling Linux user namespaces (commonly used in rootless Docker containers) and improves error diagnostics when file storage validation fails.
Docs PR: https://github.com/go-vikunja/website/pull/289
---------
Co-authored-by: Claude <noreply@anthropic.com>
When using local file storage, the doctor command now reports:
- Whether the files directory exists
- Directory permissions (octal mode)
- Directory owner and group with uid/gid (Unix)
- Ownership mismatch warning if Vikunja runs as a different user
- Total number of stored files and their combined size
---------
Co-authored-by: Claude <noreply@anthropic.com>
In Echo v5, the 404 error for unmatched routes implements the
HTTPStatusCoder interface but is not a *HTTPError. This caused
the static middleware to fail to catch 404s and serve index.html
for SPA routes, leading to reloading SPA routes returning 404.
Caused by regression introduced in 9a61453e8.
Fixes#2149Fixes#2152
Fixes a bug where the webhook HTTP client was mutating `http.DefaultClient` (the global singleton), causing ALL HTTP requests in the application to use the webhook proxy. This broke OIDC authentication and other external HTTP calls when webhook proxy was configured.
Fixes#2144
Fixes webhook payloads for deletion events that were previously
containing incomplete or empty entity data. This occurred because
entities were being deleted from the database before the webhook event
was dispatched.
## Changes
This PR implements four targeted fixes to ensure complete entity data in
deletion event webhooks:
### 1. TaskAssignee Deletion (`pkg/models/listeners.go`)
- Extended `reloadEventData()` to fetch full assignee user data by ID
- Webhook payload now includes complete user object (username, email,
timestamps, etc.)
### 2. TaskComment Deletion (`pkg/models/task_comments.go`)
- Modified `Delete()` to call `ReadOne()` before deletion
- Ensures comment text, author, and timestamps are included in webhook
payload
- Follows the same pattern used by `Task.Delete()` and
`TaskAttachment.Delete()`
### 3. TaskAttachment Deletion (`pkg/models/task_attachment.go`)
- Extended `ReadOne()` to fetch the `CreatedBy` user
- Webhook payload now includes file creator information
### 4. TaskRelation Deletion (`pkg/models/task_relation.go`)
- Modified `Delete()` to fetch complete relation including `CreatedBy`
user before deletion
- Webhook payload now includes relation timestamps and creator
information
Fixes#2125
This adds the ability to set a base URL for a Gravatar-compatible avatar
service (Gravatar itself, or Libravatar, for instance). The default will
be www.gravatar.com, so nothing will change from current behaviour unless
the user explicitly configures another URL.
Resolves#2082
- Don't create the mail queue when the mailer is disabled to prevent
`SendMail()` from blocking indefinitely
- Add guard clause in `SendMail()` to return early when mailer is
disabled or queue is nil
- Add test to verify notifications don't block when mailer is disabled
This implements the changes from #1080 with the review feedback
addressed (using `package notifications` instead of `package
notifications_test`).
Closes#1080
This changes the error handling to a centralized HTTP error handler in `pkg/routes/error_handler.go` that converts all error types to proper HTTP responses. This simplifies the overall error handling because http handler now only need to return the error instead of calling HandleHTTPError as previously.
It also removes the duplication between handling errors with and without Sentry.
🐰 Hop along, dear errors, no more wrapping today!
We've centralized handlers in a shiny new way,
From scattered to unified, the code flows so clean,
ValidationHTTPError marshals JSON supreme!
Direct propagation hops forward with glee,
A refactor so grand—what a sight to see! 🎉
This PR adds UI support for marking saved filters as favorites. The backend already supports the `is_favorite` field for saved filters, but the frontend didn't expose this functionality. Users can now favorite/unfavorite saved filters just like regular projects.
Adds startup validation that checks if the configured file storage is writable. To do this, Vikunja now tries to create a temporary file and clean it up afterwards.
- 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 change modifies the migration `20251001113831` to flexibly parse bucket configuration filters.
This fixes this migration issue:
```
json: cannot unmarshal object into Go struct field bucketConfigurationCatchup.filter of type string
```
This occurred when a single `bucket_configuration` JSON array contained
mixed formats - some buckets with old string filters and some with
already-converted object filters.
Relates to:
https://community.vikunja.io/t/reordering-not-possible-position-value-the-same-for-different-tasks/4078
Duplicate positions can occur due to race conditions or historical bugs, causing tasks to appear in the wrong order or jump around when the page is refreshed.
This change adds a `repair-task-positions` CLI command to detect and resolve task position conflicts, with dry-run preview option.
Also implemented automatic conflict detection and resolution to ensure
unique task positions.
🐰 Positions once conflicted, clustered tight,
But now we nudge them back into the light!
MinSpacing guards precision from decay,
While conflicts heal and duplicates give way. ✨
Fixes#724 - Tasks in saved filter views get `position: 0` when they first appear in the filter, causing drag-and-drop sorting to not persist correctly.
**Changes:**
- Remove harmful `Position: 0` inserts from cron job and
`SavedFilter.Update` - `RecalculateTaskPositions` already creates
positions with proper values, so the intermediate inserts created a race
window
- Add on-demand position creation when fetching tasks for saved filter
views - safety net for newly matching tasks before the cron runs
- Add 5 new tests covering the fix and regression scenarios
🐰 Positions once zero, now bloom with care,
Sorted with grace, no more despair,
When filters call and tasks appear,
Numbers spring up, crystal clear!
Issue 724 hops away—
Sorting's fixed to stay, hooray! 🎉
- Renames the `/tasks/all` endpoint to `/tasks` for consistency with
other collection endpoints like `/projects` and `/labels`
- Returns `[]` instead of `null` for empty pagination results across all
list endpoints
- Updates the frontend service to use the new endpoint path
- Updates API token tests to use the new endpoint path
Fixes#1984
Email notifications now display user mentions with inline avatar images for improved visual recognition and easier identification. Mentions gracefully fall back to display names if avatars are unavailable.
Add a new `--preserve-config` flag to the restore command that allows
users to restore database and files from a dump while keeping their
existing configuration file untouched.
Resolves https://github.com/go-vikunja/vikunja/issues/1745
- [x] Understand the issue from GitHub issue #1745
- [x] Analyze the codebase to locate the bug in
`duplicateProjectBackground` function
- [x] Fix the bug: return nil explicitly at the end of
duplicateProjectBackground
- [x] Add test for duplicating a project with an uploaded background (as
subtest)
- [x] Run tests and verify the fix
- [x] Run code review and address any feedback
- [x] Run CodeQL security scan
## Summary of Changes
### Problem
When duplicating a project with an uploaded (non-Unsplash) background
image, users encounter an internal server error (HTTP 500). The backend
logs show: `file was not downloaded from unsplash [FileID: X]`
### Root Cause
The `duplicateProjectBackground` function in
`pkg/models/project_duplicate.go` uses named returns. When
`GetUnsplashPhotoByFileID` returns `ErrFileIsNotUnsplashFile` for an
uploaded background, the error was intentionally ignored (to proceed
with copying the file) but not cleared from the named return variable.
This caused the error to be returned at the end of the function via the
bare `return` statement, triggering a 500 response.
### Solution
Changed the bare `return` at the end of `duplicateProjectBackground` to
`return nil` explicitly.
### Changes
1. **`pkg/models/project_duplicate.go`**: Changed bare `return` to
`return nil` at the end of `duplicateProjectBackground`
2. **`pkg/models/project_duplicate_test.go`**: Added subtest "duplicate
project with uploaded background" to `TestProjectDuplicate`
### Testing
- All existing tests pass
- Added subtest to `TestProjectDuplicate` for uploaded background
scenario (project 35 with non-Unsplash background)
### Security Summary
- No security vulnerabilities found by CodeQL
- Code review passed
<!-- START COPILOT CODING AGENT SUFFIX -->
<details>
<summary>Original prompt</summary>
> # Duplicate project with uploaded background - Implementation Plan
>
> ## Overview
> Users encounter an internal server error when duplicating a project
that uses an uploaded background image (non-Unsplash). The b
> ackend attempt to copy the background leaves a non-Unsplash error
(`ErrFileIsNotUnsplashFile`) in a named return value, causing
> the duplication API call to fail even though the error should be
ignored. We need to adjust the duplication flow to allow upload
> ed backgrounds and add regression tests.
>
> ## Current State Analysis
> - Project duplication calls `duplicateProjectBackground` to copy the
background file. The helper tries to copy a downloaded Unsp
> lash image and returns `ErrFileIsNotUnsplashFile` for uploaded files.
> - In the duplication code, the error variable is not cleared after
intentionally ignoring this specific error, so the function s
> till returns the error and triggers a 500 response.
> - There are no automated regression tests covering project duplication
with uploaded backgrounds.
>
> ### Key Discoveries
> - The duplication logic treats Unsplash and uploaded backgrounds
differently and only clears the Unsplash download error, leavin
> g the non-Unsplash error set.
> - The API currently works for Unsplash backgrounds but fails for
uploaded backgrounds due to the lingering error value.
>
> ## Desired End State
> - Duplicating a project succeeds for both Unsplash and uploaded
backgrounds.
> - Uploaded background files (and their metadata) are copied correctly
to the new project when possible, or gracefully skipped wi
> thout failing duplication.
> - Regression tests cover duplication with both background types to
prevent future regressions.
>
> ## What We're NOT Doing
> - No changes to the background upload endpoints or UI selection
workflow.
> - No changes to Unsplash download behavior or quota handling.
> - No new migration or database schema changes.
>
> ## Implementation Approach
> 1. Fix backend duplication error handling so uploaded backgrounds do
not cause a fatal error.
> 2. Add backend tests to cover duplication with uploaded backgrounds
and Unsplash backgrounds (success paths) and verify duplicat
> ion works without returning 500 errors.
> 3. Ensure tests document the expected behavior and guard against
regressions.
>
> ## Phase 1: Fix duplication error handling
> ### Overview
> Make project duplication tolerate uploaded backgrounds by clearing or
not propagating `ErrFileIsNotUnsplashFile` once it has bee
> n intentionally ignored.
>
> ### Changes Required
> - **File:** `pkg/models/projects.go` (or relevant duplication helper)
> - Adjust `duplicateProjectBackground` (or the calling logic) to reset
the named return error after handling `ErrFileIsNotUnspl
> ashFile`, ensuring the function returns `nil` when no real error
occurs.
> - Keep existing behavior for other errors and for Unsplash downloads.
>
> ### Success Criteria
> - Uploaded background duplication no longer returns an internal server
error.
> - Unsplash background duplication remains functional and still
surfaces real errors.
>
> ## Phase 2: Add regression tests
> ### Overview
> Add automated tests verifying project duplication works for both
uploaded and Unsplash backgrounds.
>
> ### Changes Required
> - **File:** `pkg/models/projects_test.go` (or closest existing test
file for project duplication)
> - Add a test that sets up a project with an uploaded background file,
duplicates the project, and asserts duplication succeeds
> and the duplicated project has an appropriate background reference.
> - Add/adjust test coverage for Unsplash background duplication to
confirm unchanged behavior.
> - Use existing fixtures or temporary files as needed for uploaded
background setup.
>
> ### Success Criteria
> - Tests fail on current main branch but pass after the fix.
> - Tests validate that duplication completes without 500 errors for
both background types.
>
> ## Testing Strategy
> - Automated Go tests via `mage test:filter` targeting the new
duplication tests.
> - Optionally run the broader suite (`mage test:feature`) if time
permits to ensure no regressions.
>
> ## Manual Verification
> 1. Create a project and upload a background via the UI; duplicate it;
observe duplication succeeds and background is present or
> gracefully handled.
> 2. Create a project with an Unsplash background; duplicate it; verify
duplication succeeds.
> 3. Check API responses for duplication calls to ensure no internal
server errors.
</details>
<!-- START COPILOT CODING AGENT TIPS -->
---
💬 We'd love your input! Share your thoughts on Copilot coding agent in
our [2 minute survey](https://gh.io/copilot-coding-agent-survey).
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: kolaente <13721712+kolaente@users.noreply.github.com>
This change fixes a few issues with the TickTick import:
1. BOM (Byte Order Mark) Handling: Added stripBOM() function to properly handle UTF-8 BOM at the beginning of CSV files
2. Multi-line Status Section: Updated header detection to handle the multi-line status description in real TickTick exports
3. CSV Parser Configuration: Made the CSV parser more lenient with variable field counts and quote handling
4. Test Infrastructure: Added missing logger initialization for tests
5. Field Mapping: Fixed the core issue where CSV fields weren't being mapped to struct fields correctly
The main problem was in the newLineSkipDecoder function where:
- Header detection calculated line skip count on BOM-stripped content
- CSV decoder was also stripping BOM and applying the same skip count
- This caused inconsistent positioning and empty field mapping
Rewrote the decoder to use a scanner-based approach with consistent BOM handling.
Resolves https://github.com/go-vikunja/vikunja/issues/1870
This fixes a panic that occurred when handling webhooks. The code was
incorrectly using webhook.CreatedByID (user ID) to fetch a project,
when it should use webhook.ProjectID. This could cause GetProjectSimpleByID
to return nil if no project exists with that ID.
Additionally, added a nil check before calling project.ReadOne() to prevent
a nil pointer dereference panic when accessing p.ID.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: kolaente <13721712+kolaente@users.noreply.github.com>
When a user assigns a task to themselves, notifications to other users now
correctly say "User A assigned Task #123 to themselves" instead of
"User A assigned Task #123 to User A"
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: kolaente <13721712+kolaente@users.noreply.github.com>
The filter_include_nulls property from the filter in a view would override the property set through the query string. Because we don't have a way in the UI to set this for filters in views, this makes the setting pretty opaque and unpredictable. Since we want to remove the nulls option anyways, we can just ignore it here.
Resolves https://github.com/go-vikunja/vikunja/issues/1781
When user names contain @ symbols, the email library fails to parse
the sender address format "Name @ Symbol via Vikunja <email@domain.com>".
This fix uses Go's net/mail.Address to properly format the sender
address according to RFC 5322, which automatically quotes names
containing special characters.
Fixes the error: "getting sender address: no FROM address set"
The filter regex pattern was not matching values inside parentheses correctly.
The lookahead pattern only allowed `&&`, `||`, or end-of-string after filter
values, but when filters are wrapped in parentheses like `( project = Filtertest )`,
the closing `)` appears after the value.
Fixed by adding `\)` to the lookahead pattern so it correctly handles closing
parentheses. This allows the project filter (and other filters) to work
properly when nested in parentheses.
- Added tests for project filters in parentheses (both frontend and backend)
- Backend tests confirm the backend already handled this correctly
- Frontend regex pattern now matches the backend behavior
Fixes#1645
This fixes a bug where the project is fetched before adding more details
through ReadOne since ReadOne does not fetch the project. In the normal
project reading flow through the api, this is done in the permission
check.
Resolves https://github.com/go-vikunja/vikunja/issues/1498
This change refactors the bulk task update logic so that it updates all fields a single task update would update as well.
Could be improved in the future so that it is more efficient, instead of calling the update function repeatedly. Right now, this reduces the complexity by a lot and it should be fast enough for most cases using this.
Resolves#1452
Problem:
When using Casdoor as an OpenID provider, there's an inconsistency between the user information in the JWT token and the UserInfo endpoint. The token contains the user's unique ID in the `name` field, while the UserInfo endpoint correctly returns the user's display name.
Solution:
This PR adds a new `ForceUserInfo` option to the OpenID provider configuration. When enabled, it forces the use of the UserInfo endpoint to retrieve user information instead of relying on claims from the ID token.
Impact:
- Default behavior remains unchanged (backward compatible)
- New option allows administrators to force using UserInfo endpoint data
- Particularly useful for providers like Casdoor that don't fully comply with OIDC standards
Related:
I've opened an issue in the Casdoor repository (https://github.com/casdoor/casdoor/issues/3806) to discuss the root cause. However, changing Casdoor's token structure might cause significant compatibility issues for existing integrations, so it's unclear if this can be fixed at the provider level. This PR provides a workaround in Vikunja that doesn't affect existing functionality.
This fixes a bug where old filters where not converted to the new json format correctly, leading to issues when trying to decode the raw filter string as json.
It is unclear whether that old migration had a bug or was not executed at all.
This change adds a new migration to fix all filters in views still stuck in the old filter string format.
Resolves https://github.com/go-vikunja/vikunja/issues/420
This allows CalDav clients to behave properly. In particular, DavX5 will error out on syncing the collections list rather than removing deleted projects from its local cache.
Resolves: https://community.vikunja.io/t/deleting-a-project-breaks-caldav/3315/3
Co-authored-by: Janne Heß <janne@hess.ooo>
Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/3065
Reviewed-by: konrad <k@knt.li>
Co-authored-by: das_j <das_j@noreply.kolaente.dev>
Co-committed-by: das_j <das_j@noreply.kolaente.dev>
I believe that it is possible to endup in the following situation :
- A user logs in using an authorized OIDC provider
- A vikunja user is created with the issuer & subject from the OIDC provider
- The same user logs in using another OIDC provider
- A 2nd vikunja user is created with a different issuer (possibly all other fields beside `created`, `updated` and `id` are equals)
I think it is important to be able to distinguish them in the `user list` command.
Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/3063
Co-authored-by: jyte <marc88@free.fr>
Co-committed-by: jyte <marc88@free.fr>
This changes the way environment variables are read into Vikunja's config.
With this change, Vikunja loads the config from all env variables into the config store explicitly, after all config files have been processed. The breaking change here is that values from env variables now may override values from a config file when both are specified.
This allows specifying openid providers using only environment values. Previously this still required a config file to work, because viper wouldn't know about these values otherwise.
Resolves https://community.vikunja.io/t/configure-openid-via-environment/628/16
This improves the UX because it does not allow external users to change their name in Vikunja, since that change would be overridden once they log in again.
Resolves https://github.com/go-vikunja/vikunja/issues/357
This fixes a bug where the "include nulls" query parameter would get overridden when the current view had a filter set, even if that filter didn't specify the parameter.
This fixes a bug where tasks which were filtered out by their label would still be shown. That was caused by the way the filter query was translated to sql under the hood.
Resolves https://github.com/go-vikunja/vikunja/issues/394
This fixes a bug where the ownership of a project was not transferred when the user was deleted, leading to errors when viewing the project, as the owner user could not be found.
Resolves https://kolaente.dev/vikunja/vikunja/issues/2827
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.
Because the "all projects" handler is the same as the one to fetch a single project, the handler would fail because no project was specified. However, it should return an empty project instead so that it later fetches all projects.
Resolves https://community.vikunja.io/t/http-400-when-trying-to-connect-via-caldav/3054
This fixes a bug where a saved filter would contain many "dead" entries for tasks which are not part of that filter. These entries were "dead" because the filter would not match for them and thus they were not shown.
The problem was caused by a routine during the creation of the filter where all projects from all matching tasks would be used as input for fetching the tasks to add to task_positions.
https://community.vikunja.io/t/not-able-to-move-task-between-buckets-within-a-kanban-view-for-saved-filter/2882/3
If a task is overdue at the same time the notification is sent, it would contain a message like "overdue since" without a time. This now shows "overdue now" instead.
Currently vikunja has a `/health` endpoint that was added in https://kolaente.dev/vikunja/vikunja/pulls/998. Docker/compose cannot utilize this feature since vikunja's docker image doesn't have curl/wget as it is pruned which is great for the image size. This PR adds a `healthcheck` command that send an http request to `/health` and exits with 0 or non-zero depending on the result.
It also adds a `HEALTHCHECK` to the docker image which calls this automatically.
Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/2856
Reviewed-by: konrad <k@knt.li>
Co-authored-by: ScribblerCoder <omar2001.oh@gmail.com>
Co-committed-by: ScribblerCoder <omar2001.oh@gmail.com>
Closes [#348](https://github.com/go-vikunja/vikunja/issues/348)
When moving a project, the old task bucket entries (project, saved
filters) will be removed, but not the corresponding position. This has
the effect that the saved_filter event hook UpdateTaskInSavedFilterViews
wrongly assumes that the task doesn't need to be re-added to the saved
filter since the position part still exists.
Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/2840
Co-authored-by: Maximilian Bosch <maximilian@mbosch.me>
Co-committed-by: Maximilian Bosch <maximilian@mbosch.me>
The config values for openid providers now use a map with the provider as key instead of an array. For example before:
auth:
openid:
providers:
- name: foo
clientid: ...
now becomes:
auth:
openid:
providers:
foo:
clientid: ...
This allows us to read values for openid providers from files using the same syntax as everywhere and makes the configuration more predictable. It also allows configuring providers through env variables, though it is still required to set at least one value via the config file because Vikunja won't discover the provider otherwise.
This removes reading config values from _file and instead only reads from file sub keys. This should make it easier to not accidentally specify the same value twice.
The syntax via env does not change, but via a config file this:
database:
password_file: foo
becomes
database:
password:
file: foo
This change allows to read any config value from a file, when the path to that file is specified in the config with the target config value suffixed with _file. This works with environment variables as well.
For example, setting database.password_file=/path/to/password will load the value from /path/to/password and set it as the config value of database.password.
Resolves https://kolaente.dev/vikunja/vikunja/issues/704
Resolves https://kolaente.dev/vikunja/vikunja/pulls/1621
Fetching the task comments during indexing would always check the permissions - in the specific case of indexing comments into Typesense, this will always return true, because we're checking with the owner of the project. Because this is a rather expensive operation, it is even more unnecessary.
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 adds a feature where you can enable users to only find members of teams they're part of. This makes the user search when sharing projects less confusing, because users only see other users they already know.
It is still possible to add users to teams with their email address, if they have that enabled in the user settings.
When running the testmail command, Vikunja would not stop if it wasn't able to connect to the mail server. This was a regression from 950de7c954.
This change fixes that problem.
Resolves https://kolaente.dev/vikunja/vikunja/issues/2767
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
This allows to configure the used bcrypt rounds and set it to 4 in tests, greatly speeding up the tests. It's not really required to set this to another value but it might be in the future as computers get faster.
This fixes two closely-related bugs:
1. When loading tasks from a bucket of a saved filter, the saved filter query would override the user-supplied filter, which would cause to only tasks matching the saved filter query to be returned.
2. When a filter query for a bucket was specified, the function would only check if one of the top level filters was a filter for tasks in a specific bucket. That means a filter like "bucket_id = 42 && labels = foo" would return the expected result, while a filter like "labels = foo && (bucket_id = 42 && priority = 1)" would fail with an error 500 because the task_buckets table was not joined to the sql query. The fix from the first bug caused such filter queries.
Mysql cannot handle year values < 1. That means filtering for a date value like 0000-01-01 won't work with mysql. Additionally, dates like 0001-01-01 could under some circumstances not work either when the date in combination with the time zone would resolve to something like 0000-12-31 - for example when the server is located (and configured) in UTC, but the user running the query is in New York. This could be observed by setting the time zone manually using the filter_timezone query parameter.
Resolves https://vikunja.sentry.io/share/issue/42bce92c15354c109eb1e6488b6a542b/
Resolves https://vikunja.sentry.io/share/issue/ef81451b0c7b43f1bff2d3a86ba393bb/
This commit introduces the automatic retrieval of TLS certificates from Let's Encrypt. If the feature is enabled, Vikunja will automagically request a certificate from Let's Encrypt and configure it to server content via TLS.
This PR introduces a partial fix for the CalDAV task listing bug (#753) when handling PROPFIND requests with `Depth: 1`, improving task visibility in the iOS Reminders app.
Notes:
* This might make Thunderbird somewhat usable when interacting with tasks using the `/dav/projects/{id} url`.
* This does not fully resolve the issue where the Reminders app will only display the last project after some time when adding the URL.
This is my first time working with Golang and CalDAV, so I’d really appreciate any feedback or suggestions on the code structure, style, or any improvements I could make.
Co-authored-by: JD <43763092+jdw1023@users.noreply.github.com>
Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/2717
Reviewed-by: konrad <k@knt.li>
Co-authored-by: jd <jd@noreply.kolaente.dev>
Co-committed-by: jd <jd@noreply.kolaente.dev>
This fixes an issue where it would be impossible to update a task in Typesense when the position for a view of it was previously saved as int64. This happened because the field is created per view on demand and its type is automatically inferred from the data saved. Now, when the first value for a particular position field is a float which could as well be an int (for example, 42.0), that field gets created as an int64 instead of float. Subsequent tries to save a float into that field will then fail.
Additionally, errors about this are silently discarded when using bulk insert. That's why the problem was not really debuggable at first.
Whenever a task is part of a date filter, it might fall in or out of a filter bucket without anything changing, other than the current time. For example, a filter condition like due_date > now may include different tasks depending on the current time.
For these kinds of tasks to properly show up in the kanban view of a filter, there has to be an entry in the task_buckets table. These entries only got updated when either a task was updated or the filter itself was updated. To account for th changing of time, we also need to check periodically if tasks are now part or not anymore part of that filter.
This change adds a cron task to do precisely that.
We'll have to see if this works resource-wise, but the cron is not the only one doing a bunch of sql queries so it might be fine after all.
Resolves https://community.vikunja.io/t/tasks-in-saved-filter-appear-in-list-view-but-are-not-visible-in-kanban-view/2800
This change allows to specify the task index when creating a task, which will then be checked to avoid duplicates and used. This allows us to calculate the indexes for all tasks beforehand when creating them at once using quick add magic.
The method is not bulletproof, but already fixes a problem where multiple tasks would have the same index when created that way.
Resolves https://community.vikunja.io/t/add-multiple-tasks-at-once/333/16
Bcrypt allows a maximum of 72 bytes. This is part of the algorithm and not something we could change in Vikunja. The solution here was to restrict the password during registration to a max length of 72 bytes. In the future, this should be changed to hash passwords with sha512 or similar before hashing them with bcrypt. Because they should also be salted in that case and the added complexity during the migration phase, this was not implemented yet.
The change in this commit only improves the error handling to return an input error instead of a server error when the user enters a password > 72 bytes.
Resolves https://vikunja.sentry.io/share/issue/e8e0b64612d84504942feee002ac498a/
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.
Before this fix, a project subscription object was added twice to the list of subscriptions for a task when the user did not subscribe to the task directly. This caused the user to receive a comment notification twice for a given task.
This was probably a regression from efde364224.
Resolves https://community.vikunja.io/t/e-mail-notification-twice/2740/18
This fixes a bug where tasks which had their done status changed were not moved in the correct bucket. This affected both frontend and api. The move of the task between buckets is now correctly done in the api and frontend - with a bit of duplicated logic between the two. This could be optimized further in the future.
Resolves https://kolaente.dev/vikunja/vikunja/issues/2610
When creating a related task during the import, migrating would fail because the migration would try to add the task to a bucket before the task was created. This fix changes the order in which that happens to prevent the error.
This fixes a bug where values would not be trimmed before parsing them. That resulted in a value like " 2" being invalid, even though it's a perfectly fine number.
Because the frontend sends the filters for projects and other values with comma-separated spaces like "1, 2, 3", this essentially broke filtering by these values.
Resolves https://kolaente.dev/vikunja/vikunja/issues/2547
Before this fix, creating a project with Typesense enabled would fail with an error because the tasks it fetches as part of that process do not have the task position property in their index. We now fall back to using the db for searching in that case.
In the long run, we should use typesense joins for the task position to make this more efficient.