Commit Graph

14032 Commits

Author SHA1 Message Date
kolaente 78ca1904b5
docs(api/v2): mark server-controlled label and user fields read-only 2026-05-31 15:27:44 +02:00
kolaente 451bd5a8d6
feat(api-v2): vendor scalar api docs bundle 2026-05-31 15:23:32 +02:00
kolaente 2602f723c3 docs(api/v2): add field and operation descriptions for labels
v2's OpenAPI spec is generated from struct tags and Operation fields at
runtime; unlike swaggo (v1) it can't read Go doc comments, so v2 shipped
without the field/operation descriptions v1 has. Add doc: tags to the
Label model (kept in sync with the existing comments swaggo reads for
v1) and Summary/Description to each label operation. Makes labels a
complete reference for the pattern.
2026-05-31 12:56:57 +00:00
kolaente 152bbd2ac4 test(middleware): lock in array-param order preservation
The normalizer's docstring and stripBracketSuffix's pair-by-pair walk
promise left-to-right order preservation (load-bearing for sort_by /
order_by), but the only coverage used order-insensitive assert.Contains
after 02e10b287 dropped the dedicated test. Add exact-match assertions
that a mix of plain and bracketed forms re-emits values in send order.
2026-05-31 12:56:57 +00:00
kolaente 3347180f31 fix(api/v2): don't leak internal error detail in 5xx responses
Huma's handler-error path wraps raw errors as NewErrorWithContext(ctx,
500, "unexpected error occurred", err), and since the humaecho5 adapter
writes Huma's response directly it bypasses Vikunja's
CreateHTTPErrorHandler — which returns a generic 500 with no detail for
non-domain errors. The huma.NewError override then copied err.Error()
(raw DB/driver messages, SQL, table/column names) into the problem+json
errors[], a regression vs v1.

Override huma.NewErrorWithContext to drop errs for status >= 500, log
the real cause server-side, and return a generic body. 4xx detail
(validation errors, domain messages) is unaffected.
2026-05-31 12:56:57 +00:00
kolaente 43e910025a fix(models): validate API token permissions against v1+v2 route union
PermissionsAreValid only consulted apiTokenRoutes, so a v2-only resource
(no v1 counterpart) could never be granted as a token scope even though
CanDoAPIRoute already authorises against both tables. Validate against
the union so the v1+v2 authorization and validation paths agree.
2026-05-31 12:56:57 +00:00
kolaente 8532016a2d feat(api/v2): preserve Vikunja numeric error code in problem+json
translateDomainError discarded web.HTTPError.Code, so v2 error bodies
always read code 0 — losing the v1 contract the error docs key off.
Override huma.NewError with a VikunjaErrorModel that adds a code field,
so both the generated OpenAPI schema and runtime responses carry it.
Domain errors with a numeric code now surface it (e.g. 8002 for a
missing label, matching v1); errors without one omit it.
2026-05-31 12:56:57 +00:00
kolaente e257823cef fix(api/v2): return generic 401 instead of leaking internal auth error
authFromCtx surfaced the underlying GetAuthFromContext error message
(e.g. the internal 'no echo.Context' adapter detail) straight to the
client. Log the real error and return a generic 401 instead.
2026-05-31 12:56:57 +00:00
kolaente 14446e3c41 fix(routes): apply rate-limit and metrics middleware to /api/v2
The authenticated v1 group installs setupRateLimit and
setupMetricsMiddleware; the v2 group only had cache-control and token
middleware, so authenticated v2 endpoints bypassed the configured API
rate limiter and route metrics. Mirror the v1 stack.
2026-05-31 12:56:57 +00:00
kolaente d2a3186b3e docs: add api-v2-routes skill and freeze /api/v1
Adds the api-v2-routes agent skill (CRUD + non-CRUD shapes, descriptions
required) and marks /api/v1 frozen in AGENTS.md/CLAUDE.md so new routes go
to the Huma-backed /api/v2.
2026-05-31 12:56:57 +00:00
kolaente 057b2e5439 fix(api/v2): publish OpenAPI Servers and make schemas publicly fetchable
Huma's SchemaLinkTransformer (enabled by default) emits a `$schema`
field on every JSON response and an example URL in the spec. Both were
broken in our setup: the example URL used Huma's "https://example.com"
placeholder because no Servers were declared, and the runtime URL
pointed at /schemas/Label.json instead of /api/v2/schemas/Label.json
because Huma can't see the Echo group prefix.

Two changes:

- Set OpenAPI Servers to a list with the relative GroupPrefix first and,
  if service.publicurl is configured, the absolute deployment URL
  second. Servers[0] feeds Huma's getAPIPrefix / addSchemaField /
  Transform fallback; Servers[1] is informational metadata for SDK
  generators and docs UIs. Keeping the relative URL at index 0 dodges a
  Huma quirk that double-prefixes the runtime $schema URL when the
  index-0 server URL carries a path component.

- Add /api/v2/schemas/:schema to unauthenticatedAPIPaths so editors and
  SDK tooling can fetch schemas without a token, mirroring how the spec
  itself is reachable.
2026-05-31 12:56:57 +00:00
kolaente 00b42234e9 feat(api/v2): serve Scalar docs UI at /api/v2/docs 2026-05-31 12:56:57 +00:00
kolaente 21194e61b0 test(api/v2): Label round-trip, ETag, PATCH, error shapes
Seven integration tests covering the Label pilot:

- Create_Read_Update_Delete — full round-trip through POST/GET/PUT/
  DELETE, asserts body + status at each step.
- List_ReturnsItems — GET /labels, asserts items[] is non-empty and
  contains a known fixture; this is the regression catcher for the
  generic-any silent-empty trap the spike hit.
- ForbiddenErrorShape — user1 reading user13's private label returns
  403 problem+json with the RFC 9457 type/title/status/detail shape.
- ValidationErrorShape — POST with empty title fails Huma's
  minLength:1 check with 422 problem+json + structured per-field
  errors locating `title`.
- ETagReturns304 — first GET captures ETag, second GET with
  If-None-Match returns 304.
- PATCHMergePatch — AutoPatch-synthesised PATCH with partial
  application/merge-patch+json body updates one field and leaves
  the others untouched; a follow-up GET confirms preservation.
- OpenAPISpecDescribesAllFive — the unauthenticated
  /api/v2/openapi.json surfaces GET+POST on /labels and GET+PUT+
  DELETE on /labels/{id}.
2026-05-31 12:56:57 +00:00
kolaente a2156e7231 feat(api/v2): port Label to per-operation Huma handlers
Wires five hand-written huma.Register calls for Label CRUD onto the
existing /api/v2 group: list, read, create, update, delete. Uses
concrete type cast on ReadAll to avoid the generic-any silent-empty
trap. The read operation exposes an ETag via a header-tagged output
struct field and honours conditional.Params so clients can get 304
Not Modified on subsequent reads.

Also closes a prior-phase gap: SetupTokenMiddleware was intended to
run on the /api/v2 group (per task B4 of the plan) but was never
wired. Attach it now and teach the skipper to consult
unauthenticatedAPIPaths so spec + docs remain public.
2026-05-31 12:56:57 +00:00
kolaente b52a451db4 feat(api/v2): enable AutoPatch for automatic JSON Merge Patch 2026-05-31 12:56:57 +00:00
kolaente c6c57d9d15 refactor(models): remove *Arr helper fields now handled by normalizer 2026-05-31 12:56:57 +00:00
kolaente fb9119c98d feat(middleware): normalize PHP-style array query params 2026-05-31 12:56:57 +00:00
kolaente 132f973486 fix(routes): set Cache-Control: no-store on /api/v2 too
The /api/v1 group sets Cache-Control: no-store to prevent browsers
from heuristically caching JSON responses. /api/v2 was missing the
same header, which could lead to stale reads. Extracted the inline
middleware into a shared noStoreCacheControl helper and applied it
to both groups.
2026-05-31 12:56:57 +00:00
kolaente 4125fd47c3 feat(api/v2): declare JWTKeyAuth security scheme 2026-05-31 12:56:57 +00:00
kolaente b56a74d6a7 feat(models): accept v2 PATCH as alias for PUT in API token matcher
Huma's AutoPatch synthesises a PATCH counterpart for every PUT, and both
verbs collapse to the same "update" permission. PATCH is still skipped
during collection (it would clobber PUT under the shared key), but the
matcher now accepts it as an alias for the stored PUT route on the same
path, so token holders aren't forced to use PUT exclusively.
2026-05-31 12:56:57 +00:00
kolaente 8a4f5cbe11 fix(models): make API tokens work on /api/v2 routes
Sub-phase G validation caught that a token scoped to e.g.
`labels.read_one` was rejected on /api/v2/labels because the route
collector only stripped /api/v1/ from paths and did not know about
v2's REST-style verbs (POST create, PUT/PATCH update, inverted
from v1 where PUT creates and POST updates).

Introduce a shadow apiTokenRoutesV2 map keyed under the same
(group, permission) names as the v1 entries. Route collection now
routes v2 paths into this shadow map and CanDoAPIRoute consults
both tables, so the same permission bit authorizes the v1 and v2
endpoints for the same resource without changing the data shape
served at /api/v1/routes (which the frontend token UI depends on).

Also teach getRouteDetail about PATCH so Huma's AutoPatch-synthesized
PATCH routes collapse to the `update` permission instead of being
dropped.
2026-05-31 12:56:57 +00:00
kolaente 15d8ac5f49 feat(auth): add GetAuthFromContext for Huma handlers 2026-05-31 12:56:57 +00:00
kolaente 47b2d1043b chore(deps): add huma/v2 and align transitive deps
Promotes huma/v2 to a direct dep (now imported by pkg/routes/api/v2 and
pkg/modules/humaecho5) and bumps clipperhouse/displaywidth to v0.11.0,
which is required for compatibility with uax29/v2 v2.7.0 that huma pulls
in. Other version bumps are go-mod-tidy consequences of MVS.
2026-05-31 12:56:57 +00:00
kolaente 5fefa88577 feat(routes): scaffold /api/v2 Echo group 2026-05-31 12:56:57 +00:00
kolaente 5fa6d66c41 feat: vendor humaecho adapter for echo/v5 2026-05-31 12:56:57 +00:00
kolaente 137f31bb20 fix(docker): make /tmp world-writable so exports work under any UID
The scratch image shipped /tmp owned by 1000:1000 and writable only by
UID 1000, so containers run under a different user (e.g. Unraid's
99:100, OpenShift random UIDs, or any `user:` override) could not create
the temp file used for data exports, failing with:

  error creating temp file: open /tmp/vikunja-export-*.zip: permission denied

The builder-stage `chmod 1777 /tmp` did not survive into the final image
(see #2316, which had to add --chown to make it writable for UID 1000),
so the world-writable intent was lost. Force the mode at copy time with
BuildKit's --chmod=1777, restoring a normal sticky, world-writable /tmp
that works for every UID.

Closes go-vikunja/vikunja#2755
2026-05-30 14:21:22 +00:00
kolaente e31d73b3df fix(keyvalue): treat undecodable cached values as a cache miss
A GetWithValue deserialization error in RememberFor was returned as fatal.
On a Redis upgrade the metrics counters live under the same keys as before
but were stored as plain int64, so the first decode into the new envelope
would fail and the metric would break permanently. Treat such errors as a
miss and recompute/overwrite so the cache self-heals.
2026-05-30 13:48:01 +00:00
kolaente 9a810f7632 refactor(user): remove the now-empty listeners file
The user package no longer registers any event listeners, so drop the
empty RegisterListeners hook and its caller.
2026-05-30 13:48:01 +00:00
kolaente 71dcb096be test(metrics): verify counts are read from the right table 2026-05-30 13:48:01 +00:00
kolaente 054050b1e2 test(keyvalue): cover RememberFor TTL caching 2026-05-30 13:48:01 +00:00
kolaente 0248bdf5e7 feat(metrics): invalidate the user count cache on registration
Registration is the one hot path where instant freshness is worth an
extra COUNT(*), so bust the cache there rather than waiting for the TTL.
2026-05-30 13:48:01 +00:00
kolaente 9e3e884dac refactor(metrics): drop inline file count tracking
The file count is now read from the database on demand.
2026-05-30 13:48:01 +00:00
kolaente 72a231620d refactor(metrics): drop the project/task/team/attachment count listeners
These counts are now read from the database on demand. The events
themselves stay - they are still used by webhooks and notifications.
2026-05-30 13:48:01 +00:00
kolaente 06000b7a03 refactor(metrics): drop the user count listener
The user count is now counted on demand, so the increment-on-create
listener is no longer needed.
2026-05-30 13:48:01 +00:00
kolaente 051f734f3d refactor(metrics): count entities on demand with a TTL cache
Instead of priming a counter at startup and keeping it in sync via events,
each entity count is now read directly from the database and cached for
30s (countCacheTTL). The cache is the correctness guarantee: counts are at
most one TTL stale and self-healing, so they can never permanently drift.

This fixes vikunja_user_count never updating after registration (#2650):
the count no longer depends on every mutation path dispatching an event.
2026-05-30 13:48:01 +00:00
kolaente ec2f154e10 feat(keyvalue): add RememberFor for TTL-cached values 2026-05-30 13:48:01 +00:00
kolaente 1af6d7763b fix(desktop): show tray icon in packaged builds
The tray icon was loaded from desktop/build/icon.png, but build/ is
electron-builder's default buildResources directory, whose contents are
not packaged into the app. The icon therefore existed when running from
source but was missing in every released build, leaving an empty tray
icon.

Load the icon from the packaged app root instead and add icon.png there,
rendered from the circular logo.svg so it has transparent corners rather
than the square full-bleed source artwork.

Fixes #2668
2026-05-30 13:30:55 +00:00
renovate[bot] e0fa2bbed4 chore(deps): update dependency vue-tsc to v3.3.3 2026-05-30 13:17:09 +00:00
kolaente 9456223556 fix: prevent package postinstall hang when generating jwt secret
The postinstall scripts generated the jwt secret with:

  cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1

This relies on SIGPIPE to terminate the infinite `cat /dev/urandom`
once `head` has read its single line. Inside a dpkg/apt maintainer-script
context the SIGPIPE disposition is not reliably delivered, so
`cat /dev/urandom` spins forever, the postinstall never returns, and the
whole `dpkg -i` / upgrade hangs.

Read a bounded 512 bytes with `head -c` instead so nothing depends on
SIGPIPE to terminate. 512 random bytes yield ~124 alphanumerics on
average, so the trailing `head -c 32` reliably produces a full 32-char
secret while staying dependency-free.

Fixes #2660
2026-05-30 12:39:49 +00:00
dependabot[bot] f7921238e6 chore(deps): bump axios from 1.15.2 to 1.16.0 in /frontend
Bumps [axios](https://github.com/axios/axios) from 1.15.2 to 1.16.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.15.2...v1.16.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.16.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-30 08:48:43 +00:00
Rémi Lapeyre 069685f2a7
fix(caldav): return 404 when trying to access a project that cannot exist with CalDAV (#2796) 2026-05-28 08:14:52 +02:00
Frederick [Bot] 50bece8cdb chore(i18n): update translations via Crowdin 2026-05-28 02:06:56 +00:00
renovate[bot] 7d1372ece3 chore(deps): update dev-dependencies 2026-05-27 21:18:08 +00:00
kolaente 2395239f0b
fix(ci): generate config.yml.sample in release-os-package for vikunja
vikunja's nfpm.yaml packs ./config.yml.sample as /etc/vikunja/config.yml.
The release-binaries action already regenerates it for the zip bundles,
but release-os-package runs on a fresh runner without that file, so
nfpm aborted with "matching ./config.yml.sample: file does not exist"
on every vikunja os-package matrix shard (the veans shards skip this
step entirely).

Add a vikunja-only step to regenerate it before nfpm runs.
2026-05-27 18:16:38 +02:00
kolaente 1e1fcaafbc
fix(ci): drop "./" from PACKAGE_OUTPUT_DIR so strip-path-prefix matches
The s3-action expands the upload glob into paths without a leading
"./", but strip-path-prefix was set to "./dist/os-packages/" (or
"./veans/dist/os-packages/"). The prefix never matched, so packages
landed at /<project>/<version>/<project>/dist/os-packages/<file>
instead of /<project>/<version>/<file>.

Drop the "./" prefix to match the working DIST_PREFIX pattern in
release-binaries.
2026-05-27 17:35:58 +02:00
kolaente 4f8a44de89
fix(ci): switch release composite actions to unstable on non-tag builds
The release-binaries and release-os-package composite actions were
comparing the raw release-version input against the literal "main" to
decide whether to use "unstable" for filenames and the S3 directory.
Callers always pass `steps.ghd.outputs.describe`, which is a value
like `v2.3.0-408-ge053d317` on non-tag builds — so the check never
fired and unstable artifacts landed under `/<project>/<describe>/`
with `<project>-<describe>-...` filenames.

Drive the switch from `github.ref_type == 'tag'` instead, matching the
pattern the desktop and config-yaml jobs in release.yml already use.
The raw describe value still flows into RELEASE_VERSION so the binary
and package metadata keep the precise commit reference.
2026-05-27 17:32:47 +02:00
kolaente e053d3172f
fix(ci): escape ${{ secrets.* }} mention in release-binaries description
GitHub's action manifest parser evaluates `${{ ... }}` expressions inside
`description:` block scalars, and `secrets` isn't a valid context in a
composite action — so the literal example text in the docstring caused
manifest validation to fail before any step ran.
2026-05-27 17:02:56 +02:00
kolaente be7eabb9b3 ci: move build-mage prep job out of test.yml into release.yml
build_mage_bin is only consumed by publish-repos in release.yml, so it
doesn't belong in the test workflow. Move it to release.yml as a
prep job and add it to publish-repos's needs list.
2026-05-27 13:01:44 +00:00
kolaente ed9df9064c refactor(ci): derive composite-action inputs from project name
Reviewer asked us to stop over-configuring the release-binaries and
release-os-package composite actions — they're called only with
vikunja or veans, so per-project paths, artifact names, cache keys, S3
target, and version-or-unstable can all be derived inside the action
from the project name. The xgo-out-name input goes away too.

Vikunja-specific pre-build (downloading frontend_dist, generating
config.yml.sample) now happens inside the action, gated on the project
input. Callers no longer need those preamble steps.

Secrets stay as inputs — composite actions can't read \`\${{ secrets.* }}\`
directly; passing them through is the simplest workaround.

Each callsite shrinks to ~13 lines of mostly-secret pass-through plus
2-4 lines of real parameters.
2026-05-27 13:01:44 +00:00
kolaente 304fe55da7 refactor: drop Release.* from project magefiles and point Dockerfile at build/
The release pipeline lives entirely in build/magefile.go now, so the
per-project Release namespaces in vikunja's magefile.go and
veans/magefile.go are dead weight. Drop them.

Update the Dockerfile in the same commit so the apibuilder stage
invokes `cd build && mage release:xgo vikunja <target>` — the parent
magefile no longer exposes that target.
2026-05-27 13:01:44 +00:00