Commit Graph

14068 Commits

Author SHA1 Message Date
kolaente dab6ac620d feat(api/v2): add team CRUD endpoints
Adds Team CRUD on /api/v2 mirroring the labels reference resource:
list, read, create, update, delete under /teams[/{id}].

- The list op exposes an include_public query param bound onto the
  model so Team.ReadAll can surface public teams (gated by the instance
  public-teams setting).
- Read ops emit an ETag and honor If-None-Match (304).
- Model fields gain doc: tags; server-controlled fields are marked
  readOnly:true.
- Self-registers via init()/AddRouteRegistrar; no routes.go change.
- New webtest TestHumaTeam (named to avoid clashing with the v1 model
  TestTeam) covers list/read/create/update/delete plus negatives
  (non-member 403, nonexistent 403/404) and ETag/304.
2026-06-03 18:56:12 +00:00
kolaente 2cb0fdcded docs(skills): note v2 query params must be direct fields on the handler input
A shared/embedded query-param helper struct silently fails to bind under Huma
when combined with other query params (found implementing Project's expand);
each query param must be a direct field on the operation's input struct.
2026-06-03 18:14:37 +00:00
renovate[bot] 58b2aaa74e chore(deps): update dev-dependencies to v10.9.2 2026-06-03 13:14:33 +00:00
kolaente ceb2b4f161 docs(api/v2): keep registrar godoc attached; clarify registry concurrency + ordering
- Move each resource file's init() below its RegisterXRoutes func so the func doc
  comment stays attached (it was documenting init()).
- Note AddRouteRegistrar is init-only and not concurrency-safe.
- Reword RegisterAll: registrar order is unspecified and irrelevant.
2026-06-03 13:14:13 +00:00
kolaente 5257922f3b docs(skills): document v2 self-registration pattern
Update the api-v2-routes skill: resources self-register via
func init() { AddRouteRegistrar(RegisterFooRoutes) } instead of editing
registerAPIRoutesV2. Note distinct-registrar-name requirement (RegisterAvatarRoutes
collision), config-gating inside the registrar, and AutoPatch being automatic via
RegisterAll. Also flag that mage test:filter injects -short (which skips
pkg/webtests entirely), so run single webtests with go test -run.
2026-06-03 13:14:13 +00:00
kolaente b04d4d269c refactor(api/v2): self-register resource routes via init() registry
Previously every new v2 resource appended an explicit RegisterXRoutes call
(and the EnableAutoPatch line had to stay last) in registerAPIRoutesV2 in
routes.go, causing recurring merge conflicts across in-flight PRs.

Resources now self-register: each resource file calls AddRouteRegistrar from
an init(), and registerAPIRoutesV2 just calls apiv2.RegisterAll, which runs
every registrar and then EnableAutoPatch. New resources touch zero shared
lines.
2026-06-03 13:14:13 +00:00
kolaente 220af19a39 refactor(api/v2): register upload route as RegisterAvatarUploadRoutes
Avoids a duplicate RegisterAvatarRoutes declaration in package apiv2 now that
the avatar GET route (#2818) is on main; both routes are registered distinctly.
2026-06-02 11:55:25 +00:00
kolaente b18e051ab3 fix(api/v2): reject non-decodable images (e.g. SVG) on avatar upload with 400 2026-06-02 11:55:25 +00:00
kolaente d2319e1257 refactor(avatar): share avatar-upload logic between v1 and v2 handlers 2026-06-02 11:55:25 +00:00
kolaente 2f4e3ecb91 fix(api/v2): align avatar upload body limit with global overhead
MaxBodyBytes was set to exactly the configured max file size, but a
multipart request carries extra bytes (boundary, part headers) on top of
the file, so a file at the limit could be rejected by Huma before the
handler runs. Mirror the +2 MB overhead that Echo's global BodyLimit
middleware already allows so a max-sized avatar isn't rejected.
2026-06-02 11:55:25 +00:00
kolaente cfac0773d7 fix(api/v2): accept real image content-types on avatar upload
Browsers set a real image Content-Type (image/png, image/jpeg, ...) on
the multipart avatar part, while programmatic clients often send
application/octet-stream. The part contentType tag is an allow-list for
Huma's MimeTypeValidator, which runs before the handler; broaden it so
both cases are accepted instead of being rejected with a 422.

The byte-level mimetype.DetectReader check in the handler remains the
real security gate and is unchanged.

Extend the webtest with a case that sends a part declared as image/png
and asserts it reaches the handler successfully.
2026-06-02 11:55:25 +00:00
kolaente 782c17c01d feat(api/v2): upload user avatar via multipart
Add PUT /api/v2/user/settings/avatar, the first multipart/form-data file
upload on the Huma-backed v2 API. Reuses v1's byte-level mime validation
(mimetype.DetectReader) and storage (upload.StoreAvatarFile), modeling the
request as a huma.MultipartFormFiles input so it renders as multipart/form-data
in the OpenAPI spec instead of being read off the raw echo context.

Flips the user's avatar provider to "upload" on success. Authenticated (JWT).
2026-06-02 11:55:25 +00:00
kolaente e81ccb3486 refactor(avatar): share avatar resolution between v1 and v2 handlers
Extract the duplicated user-lookup, provider-selection and size-clamping
logic from the v1 GetAvatar and v2 avatarGet handlers into a single
avatar.GetAvatarForUsername helper. Both handlers now call it and keep
only their transport-specific code (v1: echo size parse + c.Blob, v2:
huma input/response). Pure refactor, behavior is unchanged.
2026-06-02 08:17:00 +00:00
kolaente a4a0af91ff feat(api/v2): serve user avatars
Add GET /api/v2/avatar/{username}, the v2 reference for a binary response
modeled in the OpenAPI spec. Reuses the v1 avatar provider logic (provider
lookup, size clamp to config.ServiceMaxAvatarSize, runtime content-type) and
returns raw image bytes via Huma's []byte body + dynamic Content-Type header
idiom, advertised in the spec as application/octet-stream.

The endpoint is authenticated under the global security like every other v2
route (an anonymous request gets a 401); it is not public.
2026-06-02 08:17:00 +00:00
kolaente 774d884f5c test(api/v2): assert admin project id via structured json 2026-06-02 07:38:08 +00:00
kolaente 17bef4f599 test(api/v2): defer license reset in admin webtest 2026-06-02 07:38:08 +00:00
kolaente 730932be13 test(api/v2): defer session close in admin webtest 2026-06-02 07:38:08 +00:00
kolaente 2e8bd6724b fix(api/v2): apply rate limit before the admin gate 2026-06-02 07:38:08 +00:00
kolaente 82ad23c135 feat(api/v2): gate admin routes by feature + instance admin
Add the admin + license gate for /api/v2 and ship the first gated
resource, GET /api/v2/admin/projects (AdminProjectList).

The gate reuses the existing v1 middleware functions unchanged —
RequireFeature(license.FeatureAdminPanel) and RequireInstanceAdmin(),
both of which serve 404 on failure. Rather than splitting the single
v2 Huma API into a separate gated sub-group (which would split the
OpenAPI spec and drop admin operations from /api/v2/openapi.json), the
gate is applied as a path-scoped Echo middleware on the shared /api/v2
group, firing only for /api/v2/admin/* and after the token middleware.
This preserves v1's 404-not-403 semantics and keeps admin routes in the
unified v2 spec and Scalar docs.

AdminProjectList lists every project on the instance (archived
included), behind the gate. Adds doc:/readOnly: tags to the shared
Project model so it documents correctly as a v2 schema.

Tests in pkg/webtests/huma_admin_test.go (TestHumaAdminProjects) cover
all three personas: non-admin -> 404, admin without feature -> 404,
admin with feature -> 200 list, plus unauthenticated -> 401.
2026-06-02 07:38:08 +00:00
bradmartin333 6076102d21 fix(frontend): wrap notifications in Teleport to appear above modals for #2744 2026-06-02 06:30:48 +00:00
renovate[bot] 4fc4125546 chore(deps): update dev-dependencies to v8.60.1 2026-06-02 06:27:20 +00:00
Frederick [Bot] 0f50dc047d [skip ci] Updated swagger docs 2026-06-01 13:22:09 +00:00
kolaente 738bcd0c77 fix(api/v2): scope project view delete to its parent project 2026-06-01 13:04:34 +00:00
kolaente 9858792123 fix(api/v2): guard against nil bucket configuration elements 2026-06-01 13:04:34 +00:00
kolaente 1d7d67541f fix(api/v2): dedupe BucketConfigurationMode enum tag 2026-06-01 13:04:34 +00:00
kolaente 5ddc9d8ff0 feat(api/v2): add project view routes
Add ProjectView CRUD on /api/v2 under the nested path
/projects/{project}/views[/{view}], establishing the two-path-param
binding pattern for sub-resources. Mirrors the labels.go handler shape
and reuses handler.Do* so permission checks stay at the model layer.

Both {project} and {view} are bound on every operation; {project} is
threaded onto ProjectView.ProjectID (ReadOne resolves via
GetProjectViewByIDAndProject, which needs the parent id). List wraps the
[]*models.ProjectView slice in the shared Paginated envelope, read sends
an ETag for If-None-Match/304, and AutoPatch synthesises PATCH.

Also:
- Tag exposed ProjectView / ProjectViewBucketConfiguration / nested
  TaskCollection fields with doc: descriptions; mark server-controlled
  fields (id, project_id, created, updated) readOnly. Safe for v1.
- Give ProjectViewKind and BucketConfigurationModeKind a huma.SchemaProvider
  so the string-serialised enums reflect as string schemas instead of
  Huma's default integer schema (which rejected the string form with 422).

Routes registered in registerAPIRoutesV2 before EnableAutoPatch.
2026-06-01 13:04:34 +00:00
renovate[bot] c7e7f8dca3 chore(deps): update dev-dependencies 2026-06-01 12:30:22 +00:00
Tink 3d6608cac7
feat(api/v2): add task duplicate action (#2815) 2026-06-01 14:13:39 +02:00
Tink bot fd10300597 fix(migration): don't drop TickTick tasks sharing a malformed id
Collapsing unparseable taskIds to 0 meant sortParentsBeforeChildren,
which tracked placement by TaskID, treated every zero-id task after the
first as already placed and silently dropped it. Track placement by task
identity instead so duplicate or zero ids never conflate distinct tasks.
2026-06-01 10:09:58 +00:00
Tink bot ebb89ba4f3 fix(migration): tolerate non-numeric values in TickTick CSV exports
TickTick exports could contain non-numeric values in columns Vikunja
parses as integers (Priority, taskId, parentId). gocsv's strconv.ParseInt
then failed, aborting the entire import and surfacing as an internal
server error reported to Sentry (e.g. parsing "p1": invalid syntax).

Numeric ID columns now fall back to 0 for unparseable values instead of
failing the import. The Priority column, which was previously parsed but
never carried over to the imported task, is now mapped onto the task and
accepts both the plain numeric form (0, 1, 3, 5) and the "pN" form
(p1, p2, p3).

Closes #2822
2026-06-01 10:09:58 +00:00
Frederick [Bot] e1c9ab5939 [skip ci] Updated swagger docs 2026-06-01 10:05:28 +00:00
Tink bot fb6f16adde fix: respect allow_icon_changes config on web and desktop
The `service.allowiconchanges` config option was ignored. On the web ui the
value injected into index.html by the api was immediately overwritten by a
hardcoded `window.ALLOW_ICON_CHANGES = true` in a later inline script, so the
configured value never took effect. The desktop app never received the
injected value at all, since it serves the bundled frontend from its own local
server and only talks to the api for data.

Expose the option via the /info endpoint and read it from the config store,
which is the only channel that reaches both the web ui and the desktop app.
The brittle window injection and its hardcoded default are removed in favor of
this single source of truth.

https://claude.ai/code/session_01HAXTJNsDcfsB4hwDNKTECb
2026-06-01 09:40:37 +00:00
Frederick [Bot] 9bf19e4dc5 chore(i18n): update translations via Crowdin 2026-06-01 00:30:24 +00:00
kolaente 5bbc5aa1ea
fix(ai): correct snake_case in json instructions 2026-05-31 17:16:49 +02:00
kolaente 833e8e817c
docs(skills): hint at readOnly tags for server-controlled v2 fields 2026-05-31 15:30:51 +02:00
kolaente 2488478f69
docs(api/v2): mark error code field read-only 2026-05-31 15:29:46 +02:00
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