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