Instances that accumulated project managers, favorites, board memberships
or board subscriptions pointing at a project or board that no longer exists
had no way to find or clear them.
The script reports by default and only deletes with `--apply`, which first
asks whether a backup exists and refuses outright without a terminal to ask
on. It removes nothing but reference rows: structural orphans such as a
board without a project carry user content, and a membership whose
denormalized project is stale still grants real access to an existing
board, so both are reported and left alone.
The filtered branch of `getByEndlessListId` applied `LIMIT` without an
`ORDER BY`, so Postgres was free to hand back any matching rows. With a
search over 64 cards the first page returned the oldest ones and a full
cursor walk reached 60 of them across 149 rows — cards both skipped and
repeated. The query now orders the way the cursor reads it.
The cursor itself is validated as ISO 8601, which admits forms Postgres
rejects as a timestamp (`2026`, `2026-W35-3`, a comma as the decimal
separator), each of them a 500 from the adapter. It is normalized before
the query, and stays a string because the equality half of the cursor
reads a `Date` as an empty constraint.
`getManagerProjectsModelArray` mapped every project manager to its project
without checking that the project is in the store, so one row pointing at a
deleted project produced a null the caller then read `id` off — taking down
the whole home view. `getMembershipProjectsModelArray` had the same hole one
level up, destructuring `board.project` before testing `board`.
The projects endpoint fed exactly that state: it derived the id list for the
included records from the manager rows, so a `project_manager` row whose
project no longer exists shipped a manager for a project missing from `items`.
Board memberships had the same gap. Both id lists now come from the records
that actually exist.
The local file manager joined attacker-controlled path segments into the
uploads storage path without ensuring the result stayed under the uploads
root. Any authenticated user could read arbitrary files readable by the
Planka process (e.g. /etc/passwd, /app/.env) via `../` sequences on the
/user-avatars/*, /background-images/* and /favicons/* routes (CWE-22).
Resolve the path and enforce it stays within uploadsBasePath, centrally in
buildPath so every local file operation is covered. Additionally resolve
symlinks in read() and re-check containment, so a symlink inside the root
cannot be used to escape it.
Reported by Alpesh (github.com/Alpastx).
Users can pick an inactivity timeout in their preferences. A warning
appears 30 seconds before, and activity or a logout is synchronised
across open tabs.
Editing a user now opens a modal with an information tab and an API
key tab instead of a chain of popups. The user name in the list opens
it, and the admin two-factor reset moves in as well.
The two-factor stylesheets were carried over from the Pro edition and
referenced CSS custom properties from its theme system, which this
edition does not define. They silently fell back to browser defaults.
Adds TOTP setup with QR code, login challenge, recovery codes and
trusted devices that let a browser skip the second factor for 30
days. Admins can reset another user's second factor by confirming
with their own password.
Bump Node to 24 in Dockerfile and Dockerfile.dev.
Bump sharp to 0.35, nodemailer to 9 and concurrently to 10.
Add overrides for body-parser, qs, multiparty, uuid, diff,
serialize-javascript, linkify-it and svgo to pull in security fixes
that upstream pins block.
Rename the sails patch to match 1.5.18.
The index produced by drag-and-drop refers to the rendered task list,
which excludes completed tasks when hideCompletedTasks is enabled, but
selectNextTaskPosition resolved that index against the unfiltered task
array, computing positions between the wrong (often hidden) neighbors.
Lift the completed-tasks visibility state up to TaskLists so the drag
end handler can pass it through the move action, and apply the same
filter in selectNextTaskPosition that the rendered list uses.
Fixes#1718
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A card created while member/label filters were active was made without
any members or labels, so the all-or-nothing filter immediately excluded
it and the card vanished from the view.
After a card is created, assign the active filter users and labels to it
(reusing addUserToCard/addLabelToCard) so it stays visible. No-op when no
filters are active.
Adds optional project, board, and acting-user scope to each webhook so
events from a specific board or project can be routed to a single
endpoint without an external automation tool in the middle (closes
#1457).
A webhook now fires for an event only when every set scope matches:
empty scope still means "fire for everything", keeping existing
webhooks working unchanged. Scope filtering is centralized in the
sendWebhooks helper, which auto-derives projectId/boardId from the
event payload, so existing call sites are untouched.
Adds a per-list filter to the board action bar, visible only in
List View. Mirrors the existing transient label/member filter
pattern: filterLists is a many-relation on the Board ORM model,
toggled via entry actions and reset on board navigation. Empty
selection keeps the current behavior; non-empty selection shows
only cards whose listId is in the chosen set. Filter UI is hidden
in Kanban/Grid views where columns already convey the same info.
Closes#1524
Adds three new environment variables that give instance operators control
over Apprise notification services, addressing GDPR and compliance concerns
around third-party data transfers.
## New environment variables
- APPRISE_ENABLED (default: true)
Set to "false" to globally prevent users from creating Apprise notification
services and to silently skip sending any Apprise notifications. Existing
services in the database are preserved but not used while disabled.
- APPRISE_ALLOWED_SCHEMAS (default: empty — no restriction)
Comma-separated allowlist of Apprise URL schemas (e.g. "slack,tgram").
When set, only services whose URL schema appears in this list can be
created or will be sent. Takes priority over APPRISE_BLOCKED_SCHEMAS.
Uses Apprise's internal schema names (e.g. "tgram" for Telegram).
- APPRISE_BLOCKED_SCHEMAS (default: empty — built-in list applies)
Comma-separated blocklist of Apprise URL schemas (e.g. "discord,slack").
When set, replaces the built-in blocked list entirely. When empty, the
built-in list (syslog, dbus, kde, qt, glib, gnome, macosx, windows)
continues to apply as a fallback.
## Enforcement
Validation happens at two layers:
1. API layer (create endpoints for user and board notification services)
Returns HTTP 403 when Apprise is disabled, HTTP 422 when the URL schema
is not permitted. This gives users immediate feedback in the UI.
2. Python send layer (send_notifications.py)
The schema config is passed as a JSON argument so the same allow/block
rules apply at send time, guarding against services that were created
before the config was tightened.
## Files changed
- server/config/custom.js — parse new env vars via existing envToArray helper
- server/utils/send_notifications.py — accept schemaConfig as argv[4]; replace
hard-coded BLOCKED_SCHEMAS_SET with dynamic allowed/blocked resolution
- server/api/helpers/utils/send-notifications.js — short-circuit when disabled;
pass schemaConfig JSON to the Python script
- server/api/helpers/notification-services/create-one-in-{user,board}.js — add
appriseDisabled and schemaNotAllowed exits with schema validation logic
- server/api/controllers/notification-services/create-in-{user,board}.js — wire
new exits to forbidden (403) and unprocessableEntity (422) responses
- server/.env.sample — document all three new variables with comments
Adds a dismissible banner in the topbar that informs users about
PLANKA Pro features.
- Dismissible per-user (stored in localStorage)
- Reappears after 30 days
- Rotates between main message and 3 feature highlights
- Links to planka.app/pro (with ref parameter for anonymous source attribution)