Replace FormField with Password component for new password input:
- Provides real-time validation feedback (8-72 char requirement)
- Remove redundant password confirmation field
- Disable save button when form is invalid (validation errors or empty fields)
- Add httpCodeGetter interface to handle ValidationHTTPError in test helper
- Add test case for password too short in password reset
- Add test case for password too short in password update
- Fix existing test data to use valid 8+ char passwords
Add bcrypt_password validation to password reset and update endpoints:
- Add validation tag to PasswordReset.NewPassword struct field
- Add validation tag to UserPassword.NewPassword struct field
- Add c.Validate() calls in both handlers
- Fix off-by-one error in bcrypt_password validator (use <= 72 not < 72)
Password requirements: min 8 chars, max 72 bytes (bcrypt limit)
Replace io.LimitReader with a new readZipEntry helper that reads one extra
byte to detect when content exceeds maxZipEntrySize (500MB). This prevents
silent data corruption where partial file bytes would be stored as if the
upload succeeded.
The import now fails with ErrFileTooLarge instead of accepting truncated
content for attachments and background blobs.
Move archive validation (migration file existence and slice bounds
check) before the database wipe. Previously a malformed archive
would first destroy the database and then panic, leaving the
instance in an irrecoverable state with total data loss.
Now the migration data is fully parsed and validated before any
destructive operations occur.
Check that database entries in the zip have a .json suffix and a
non-empty base name before slicing the extension off. This prevents
a panic from index-out-of-range when the filename is too short.
Also use TrimPrefix instead of ReplaceAll for correctness.
Use filepath.Base() on the config file name from the zip archive
before passing it to os.OpenFile, ensuring the config file is
always written to the current directory regardless of what path
the zip entry claims to have.
Validate all zip entry names during restore to reject entries
containing directory traversal sequences (e.g. ../../../pwned.txt).
This prevents a Zip Slip attack where a malicious archive could
write files outside the intended extraction directory.
Typesense was an optional external search backend. This commit fully
removes the integration, leaving the database searcher as the only
search implementation.
Changes:
- Delete pkg/models/typesense.go (core integration)
- Delete pkg/cmd/index.go (CLI command for indexing)
- Simplify task search to always use database searcher
- Remove Typesense event listeners for task sync
- Remove TypesenseSync model registration
- Remove Typesense config keys and defaults
- Remove Typesense doctor health check
- Remove Typesense initialization from startup
- Clean up benchmark test
- Add migration to drop typesense_sync table
- Remove golangci-lint suppression for typesense.go
- Remove typesense-go dependency
Replace innerHTML with DOM API calls in inputPrompt.ts. The oldValue
parameter (sourced from a link's href attribute in the TipTap editor)
was interpolated directly into an HTML string, allowing stored XSS if
an attacker crafted a malicious href. Using document.createElement and
setting .value as a property ensures the value is never parsed as HTML.
TipTap's setContent() parses strings as HTML via DOMParser, allowing
crafted ?filter= URL parameters to inject SVG phishing buttons, anchor
tags, and formatted content into the trusted UI.
Use ProseMirror JSON document format instead of raw strings so the
filter value is always set as a text node, bypassing HTML parsing
entirely.
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.
- Verifies transparent retry and JWT rotation on 401 with code 11
- Verifies no retry for 401 with non-JWT error code
- Verifies current session appears on sessions settings page
- Increases rate limit for e2e test API to prevent 429 errors
- Session model, type interface, and API service
- Sessions settings page showing active sessions with device info,
IP address, last active time, and current session indicator
- Auth store updated to use cookie-based refresh tokens for user
sessions and JWT-based renewal for link shares
- refreshToken() uses Web Locks API to coordinate across browser
tabs — only one tab performs the refresh, others adopt the result
- 401 response interceptor with automatic retry: detects expired JWT
(error code 11), refreshes the token, and replays the request
- Interceptor gated to user JWTs only (link shares skip refresh)
- checkAuth() attempts cookie refresh when JWT is expired, allowing
seamless session resumption after short TTL expiry
- Proactive token refresh on page focus/visibility via composable
- renewToken() tolerates refresh failures when JWT is still valid
- 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.