Commit Graph

114 Commits

Author SHA1 Message Date
kolaente fa0c9a8584 feat(api): exchange rich-text fields as markdown on v2
Wire the conversion helpers into every rich-text handler: read/list/echo
convert HTML to markdown, create/update convert markdown to HTML before
persisting, and each op documents the format query field. Opt-in via
?format=markdown or the X-Vikunja-Format header.
2026-06-29 08:12:35 +02:00
kolaente 71639a3dc5 feat(api): add v2 markdown conversion helpers
Adds the opt-in format plumbing for v2: requestWantsMarkdown (query or
X-Vikunja-Format header), convertToMarkdown/convertToHTML/convertTasksToMarkdown
field converters, the cross-cutting API description, and stripPatchFormatQuery
(AutoPatch drops the query, so PATCH advertises only the header).
2026-06-29 08:12:35 +02:00
kolaente 0d043e80e4 feat(api/v2): add kanban bucket CRUD endpoints
Port the standalone bucket list/create/update/delete from v1 to the
Huma-backed /api/v2, under /projects/{project}/views/{view}/buckets,
using v2 verb conventions (POST creates, PUT updates). The handlers
reuse the generic handler.Do* functions, so permissions are enforced
by the Bucket model's existing Can* methods.

Mirrors v1: no read-one route (the model has no ReadOne/CanRead), so
AutoPatch synthesises no PATCH. No model changes.
2026-06-26 08:56:15 +00:00
Tink 7208694960
fix(auth): build OIDC end-session URL with RP-Initiated Logout params (#2943) 2026-06-19 18:27:33 +02:00
Tink f3c6312a9e
feat(projects): make duplicating shares opt-in (#2932) 2026-06-19 10:15:58 +02:00
kolaente 40f2900e9d feat(api/v2): expose notifications atom feed in the OpenAPI spec
Adds GET /api/v2/notifications.atom as a Huma operation producing
application/atom+xml, so the feed shows in the v2 OpenAPI spec with an
opaque XML body schema. It mirrors /feeds/notifications.atom on the wire.

Feed readers can't carry a bearer header, so the op declares an HTTP
Basic security scheme (BasicAuth) and authenticates inside the handler:
it parses the Authorization: Basic header and validates the API token
via the shared feeds.AuthenticateFeedToken, returning a 401 with a Basic
challenge on failure, then streams feeds.BuildNotificationsAtomFeed. The
path is in unauthenticatedAPIPaths so the JWT middleware lets it through.
2026-06-17 20:35:28 +00:00
kolaente 1a4f03bbc8 feat(api/v2): expose healthcheck as a documented endpoint
Adds GET /api/v2/health as a Huma operation so it appears in the v2
OpenAPI spec with a clean JSON schema ({"status": "OK"}). It runs the
same health.Check() probe as the v1 healthcheck and is public — it opts
out of the global bearer auth and is listed in unauthenticatedAPIPaths.
2026-06-17 20:35:28 +00:00
kolaente 7c11c2dc29 feat(api/v2): port refresh-token endpoint to /api/v2
POST /api/v2/user/token/refresh reads the HttpOnly refresh cookie, rotates
the session, mints a new JWT, and sets the new cookie — reusing the shared
auth.RefreshSession core (no v1 change) and the #2912 cookie helpers /
authTokenBody response shape. The cookie is set via the unwrapped echo ctx,
not the OpenAPI spec.

translateDomainError now maps *echo.HTTPError (which RefreshSession returns
for missing/invalid/expired/replayed tokens) so those land as the right
status instead of a 500. Completes the v1→v2 REST migration.
2026-06-17 20:34:38 +00:00
kolaente 422d504a07 feat(api/v2): add OpenID Connect callback on /api/v2
Port the OIDC callback to Huma, reusing openid.AuthenticateCallback. The
route is only registered when OpenID is enabled; unknown providers still 404
per request. v1's bespoke {message, details} error body is replaced by
standard RFC 9457, folding the provider detail into the structured error.
2026-06-17 19:43:41 +00:00
kolaente d4ab438073 feat(api/v2): add login and logout on /api/v2
Port the cookie-setting login and logout endpoints to Huma. Both reuse the
shared auth cores; the HttpOnly refresh cookie and Cache-Control: no-store
header are set via the unwrapped echo context (the cookie stays out of the
OpenAPI schema, matching v1). The token response inlines the JWT to avoid a
schema-name collision with user.Token.

login is public (LDAP-only deployments log in here too); logout inherits the
global JWT auth and no-ops for tokens that carry no session.
2026-06-17 19:43:41 +00:00
kolaente ee8dbf82ba fix(api/v2): close export reader when commit fails before streaming
If s.Commit() fails after loading the export file, the StreamResponse
callback that would close the reader never runs, leaking the open
object-storage/file handle. Close it explicitly on that error path.
2026-06-17 18:39:38 +00:00
kolaente 8c72e83a4d feat(api/v2): add user data export endpoints
Port POST /user/export/request, POST /user/export/download (zip stream) and
GET /user/export (status) to v2. Extract the export-file loader and status
builder into pkg/models (GetUserDataExportFile, GetUserDataExportStatus) with
a shared ErrUserDataExportDoesNotExist, and refactor v1 onto them. The v2
download streams via the shared WriteFileDownload writer; local users confirm
with their password, external-provider users are passed through.
2026-06-17 18:39:38 +00:00
kolaente ac5e94252b feat(api/v2): add totp qr code endpoint
Port GET /user/settings/totp/qrcode to v2 as an image/jpeg blob, modeled in
the OpenAPI spec. Extract the qr-to-jpeg encoding into user.GetTOTPQrCodeAsJpegForUser
so v1 and v2 share it; refactor v1 onto it. The handler reuses the existing
local-account guard, rejecting non-local users with 412.
2026-06-17 18:39:38 +00:00
kolaente 4737114b12 feat(api/v2): add e2e testing-support endpoints on /api/v2
Port the testing fixture endpoints to /api/v2: PUT /test/{table} resets a
table to a posted fixture set and DELETE /test/all truncates everything.
Both authenticate with the configured testing token via a custom
Authorization header (not JWT/API-token) and only mount when that token is
set. Reuses the shared reset/truncate logic extracted from v1.
2026-06-17 12:13:50 +00:00
kolaente 5ccbd0d74e feat(api/v2): add project background download and unsplash proxies
Port the remaining read-only background blob endpoints to /api/v2:

- GET /projects/{project}/background streams the stored background (project
  CanRead, in-handler), modeled as an image/jpeg binary response. Honors
  If-Modified-Since (304) and serves through the shared WriteProjectBackground.
- GET /backgrounds/unsplash/images/{image} and .../thumb proxy the upstream
  Unsplash image through the SSRF-safe client, gated on the unsplash provider
  like the sibling unsplash routes, modeled as image/jpeg binary responses.

All three reuse the v1 business logic extracted in the previous commit.
2026-06-17 11:31:50 +00:00
kolaente 77416d32e4 feat(api/v2): add the generic CSV importer on /api/v2
Port the CSV importer's status/detect/preview/migrate endpoints to the Huma
API. detect/preview/migrate take a multipart upload; preview and migrate also
carry the import config as a JSON form value (modeled as a typed multipart
form field), unmarshaled in one shared place and reused via csv.RunMigration.
2026-06-12 08:51:19 +00:00
kolaente a21822fcec feat(api/v2): add file migrators (vikunja-file, ticktick, wekan) on /api/v2
Port the file-based migrators' status + migrate endpoints to the Huma API.
A single registerFileMigrator helper wires all three (mirroring the OAuth
migrator registrar); the migrate endpoint takes a multipart upload under the
"import" field and reuses handler.RunFileMigration. POST migrate returns 200
since it runs an import rather than creating a REST resource.
2026-06-12 08:51:19 +00:00
kolaente 3af5eb8208 feat(api/v2): add project background upload on /api/v2
Port PUT /projects/{project}/backgrounds/upload to the Huma-backed v2 API. The
multipart handler reuses handler.ValidateAndSaveBackgroundUpload (shared with
v1), checks project write access explicitly, and is gated on the upload provider
config flag. Adds webtests covering the happy path, auth/permission failures,
non-image rejection, the disabled-provider case and the multipart spec shape.
2026-06-12 08:47:08 +00:00
kolaente 5e00fcbbb8 chore(lint): suppress contextcheck on OIDC provider init call sites
Adding a context parameter to the shared package put its call chains in
contextcheck's scope; the flagged background context in the provider
setup is deliberate since provider lifetime exceeds any request.
2026-06-12 08:56:08 +00:00
kolaente acdc2a07f2 feat(audit): emit the login event for the OAuth code exchange
The new v2 OAuth token endpoint mints a fresh session without going
through NewUserAuthTokenResponse, so those logins were missing from the
audit trail. The refresh grant stays unaudited like the v1 refresh.
2026-06-12 08:56:08 +00:00
kolaente f33cde82e2 feat(audit): attribute failed logins to the originating request
Thread the request context through CheckUserCredentials so the
LoginFailedEvent carries IP, user agent and request id — without it,
failed logins were the one auth event useless for brute-force tracing.
All four callers have the request at hand.
2026-06-12 08:56:08 +00:00
kolaente b86710903b fix: dispatch pending events after user creation commits
The register handler, local/LDAP login and the OIDC callback all queue
the user.created event via DispatchOnCommit but never called
DispatchPending, so the event was silently dropped and its queue entry
leaked. Flush after commit and discard on rollback.
2026-06-12 08:56:08 +00:00
kolaente 9da51f5096 refactor(events): pass context to DispatchPending directly
Every DispatchPending caller either has the request context in scope or
is genuinely request-less, so passing it as a parameter replaces the
stored-context mechanism on the pending queue and satisfies
contextcheck. Also fixes lint findings in the audit package.
2026-06-12 08:56:08 +00:00
kolaente 2bbe77c141 fix(api/v2): gate /register at registration time, not per request
Per review: when registration is disabled, skip registering the
/register route entirely instead of registering it and returning 404 on
every request. A request to a disabled instance still 404s (unknown
route). ServiceEnableRegistration is static config, so the gate belongs
in the registrar.
2026-06-12 07:58:17 +00:00
kolaente d8ad9d64f5 test(api/v2): cover ported auth/token endpoints
Add webtests mirroring the v1 coverage for the v2 auth surface:
register (incl. registration-disabled 404), password reset request +
reset, email confirm, link-share auth (password matrix), the OAuth token
flow in both JSON and form-urlencoded encodings, oauth/authorize, the
token-test/check endpoints (200, not 418), /routes and link-share token
renewal (incl. user-token rejection).

Also make the link-share auth body optional so a passwordless share
authenticates with no request body, matching v1.
2026-06-12 07:58:17 +00:00
kolaente 56a516045b feat(api/v2): add token-check, token-routes and link-share renew endpoints
Port the token introspection helpers and link-share token renewal to
/api/v2:

- GET/POST /token/test both return a plain 200 "ok"; v1's POST 418
  teapot easter egg becomes an ordinary success.
- GET /routes lists the scoped-token routes for both API versions
  (models.GetAPITokenRoutes already merges v1 + v2).
- POST /user/token renews a link-share JWT; user tokens are rejected
  (they must use the refresh-token flow), mirroring v1.

The renew response inlines the token field rather than returning
auth.Token directly, since Huma names schemas by bare type and a
top-level auth.Token body would collide with user.Token.
2026-06-12 07:58:17 +00:00
kolaente dc4c3a6a17 feat(api/v2): add OAuth 2.0 token and authorize endpoints
Port oauth/token and oauth/authorize to /api/v2, delegating to the
shared oauth2server.ExchangeToken / Authorize cores.

The token endpoint accepts spec-compliant application/x-www-form-urlencoded
bodies (RFC 6749) in addition to JSON; a form-urlencoded format is
registered on the v2 API that binds into the same json-tagged request
struct. The response carries Cache-Control: no-store. The token endpoint
is public; authorize inherits the global JWT auth.
2026-06-12 07:58:17 +00:00
kolaente 37a174b99e feat(api/v2): add public auth routes (register, password, confirm, link-share)
Port the unauthenticated local-account flows and link-share auth to
/api/v2, delegating to the shared business logic:

- POST /register (404 when registration is disabled)
- POST /user/password/token, POST /user/password/reset
- POST /user/confirm
- POST /shares/{share}/auth

Local-account routes register only when local auth is enabled and the
link-share route only when link sharing is enabled, mirroring v1. Each
operation opts out of global auth and its path is added to
unauthenticatedAPIPaths.
2026-06-12 07:58:17 +00:00
kolaente 5807f2e7b4 refactor(user): share user-search logic between v1 and v2
Extract the duplicated user-search business logic into two helpers both API
versions call, and refactor v1's handlers onto them:
- user.SearchUsers wraps ListUsers + email obfuscation (global search)
- models.SearchUsersForProject wraps the project read check + ListUsersFromProject

Each handler keeps its own forbidden mapping (v1 echo.ErrForbidden vs v2
huma) so v1 stays byte-identical on the wire.
2026-06-11 20:07:43 +00:00
kolaente 5dcc501d54 feat(api/v2): add user search endpoints
Port to /api/v2:
- GET /users (global user search by username/name/email; emails are blanked)
- GET /projects/{project}/users/search (users with access to a project, for
  share autocomplete; requires project read access)

Both are custom routes: the project search loads the project and enforces
CanRead explicitly.
2026-06-11 20:07:43 +00:00
kolaente 3312716afd feat(api/v2): add available webhook events endpoint
Add GET /api/v2/webhooks/events, listing the events a webhook target can
subscribe to. Gated on webhooks.enabled via a registrar early-return, mirroring
v1.
2026-06-11 20:07:43 +00:00
kolaente 56b1ba47ec feat(api/v2): add public instance info endpoint
Add GET /api/v2/info (public — no auth). Extract the /info response type and
its assembly out of the v1 handler into pkg/routes/api/shared.BuildInfo() so
both API versions return byte-identical info; refactor v1's handler onto it.
Add the v2 path to unauthenticatedAPIPaths.
2026-06-11 20:07:43 +00:00
kolaente 6f3dab53cb feat(api/v2): add project background endpoints
Port to /api/v2:
- DELETE /projects/{project}/background (remove background, returns the updated project)
- GET /backgrounds/unsplash/search (q, page; gated on the unsplash provider)
- PUT /projects/{project}/backgrounds/unsplash (set, gated on the unsplash provider)

Custom routes load the project and enforce CanUpdate explicitly. Backgrounds
are gated on the static backgrounds config via a registrar early-return.
Tag background.Image fields with doc: for the v2 schema, and add a scoped
contextcheck exclusion since the unsplash provider's shared interface bottoms
out in context.Background().
2026-06-11 20:07:43 +00:00
kolaente 53d1fa0735 refactor(admin): share user-mutation logic between v1 and v2
The admin set-admin-flag, set-status and delete-user operations were
implemented twice — once in the v1 echo handlers, once in the v2 Huma handlers.
Extract the load/guard/mutate logic into models.SetUserAdminFlag,
models.SetUserStatusAsAdmin and models.DeleteUserAsAdmin so both APIs call the
same code; each handler keeps only its own request binding, validation and
response shape. v1 stays byte-identical on the wire.
2026-06-11 19:32:42 +00:00
kolaente 5b3ee89edd refactor(api/v2): dedup the admin user-mutation handlers
The patch-admin, patch-status and delete-user handlers each repeated the same
session open/load/commit/rollback scaffold. Extract it into adminMutateUser,
which owns the transaction and takes a closure for each handler's distinct
guard-and-write step.
2026-06-11 19:32:42 +00:00
kolaente 5579daa452 feat(api/v2): add admin actions on /api/v2
Port the admin action endpoints to the Huma-backed /api/v2:

- GET    /admin/overview            instance counts + license snapshot
- POST   /admin/users               create a user (201)
- PATCH  /admin/users/{id}/admin    promote/demote (*bool, nil = unchanged)
- PATCH  /admin/users/{id}/status   set status (*Status, nil = unchanged)
- DELETE /admin/users/{id}          delete (mode=now|scheduled, 204)
- PATCH  /admin/projects/{id}/owner reassign project owner

All sit behind the existing gateV2AdminRoutes path middleware (admin + license
gate, 404 on failure), so no per-handler permission checks are added. The
hand-registered PATCH routes carry genuine partial semantics, which AutoPatch
does not synthesise. The admin user response reuses the existing
pkg/routes/api/shared package.
2026-06-11 19:32:42 +00:00
kolaente 9c3c1047ac feat(api/v2): port OAuth migrators (Todoist, Trello, Microsoft To-Do)
Add /api/v2 auth/status/migrate endpoints for the three OAuth-based
migrators. One generic helper registers all three ops per migrator
behind its static config gate, so there's no copy-pasted block per
migrator.

The migrate kick-off orchestration (already-running guard + event
dispatch) is extracted into migrationHandler.StartMigration so v1 and
v2 share it; v1's wire output is unchanged. The guard now surfaces as a
typed migration.ErrMigrationAlreadyRunning (412) so v2 can translate it
through the standard error bridge.
2026-06-11 18:35:55 +00:00
kolaente 809ac118f9 refactor(api/v2): dedup task collection query params via exported embed 2026-06-11 18:31:03 +00:00
kolaente 3bd75acabf feat(api/v2): add task collection (task lists) on /api/v2
Ports v1's task-list surface to /api/v2 as four endpoints. v1 served a
single polymorphic endpoint; v2 makes it monomorphic:

  GET /tasks                                     flat []*Task, all projects
  GET /projects/{project}/tasks                  flat []*Task
  GET /projects/{project}/views/{view}/tasks     flat []*Task (even kanban)
  GET /projects/{project}/views/{view}/buckets/tasks   []*Bucket with tasks

The three task endpoints force flat tasks via TaskCollection so a kanban
view path no longer returns buckets; the dedicated buckets endpoint keeps
the polymorphic kanban branch and is not paginated (bounded by the view's
bucket config). Search is exposed as q; multi-value sort_by/order_by/expand
use ,explode. Hitting the buckets endpoint with a non-kanban view is a 400
rather than a type-mismatch 500.
2026-06-11 18:31:03 +00:00
kolaente 28af57bc93 feat(api/v2): add user account/settings on /api/v2
Port the current-user account and settings endpoints from /api/v1 to the
Huma-backed /api/v2, calling the shared orchestration extracted into
models/user/openid:

- GET    /user                            current user + settings + computed
                                          auth_provider/is_local_user/is_admin
- POST   /user/password                   change password (200, creates nothing)
- PUT    /user/settings/email             update email (kicks off confirmation)
- PUT    /user/settings/general           update general settings
- GET    /user/settings/avatar/provider   get avatar provider
- PUT    /user/settings/avatar/provider   set avatar provider
- GET    /user/timezones                  list available time zones

These are current-user-scoped custom handlers (no per-resource Can*): each
pulls the authed user from the request context and operates on it. The avatar
provider get/set live on /user/settings/avatar/provider because v2 already
maps /user/settings/avatar to the binary avatar upload (PUT).
2026-06-11 07:02:31 +00:00
kolaente b8894ac1c1 feat(api/v2): add user account-deletion flow on /api/v2 2026-06-10 19:15:05 +00:00
kolaente a610ccbbac feat(api/v2): add user webhooks on /api/v2
Port the per-user webhook endpoints (/user/settings/webhooks) from /api/v1 to
the Huma-backed /api/v2: list, available events, create, update, delete. They
are the project-less sibling of the project webhooks (#2858) and share the
webhooks.enabled gate, checked inside the registrar.

Webhook.ReadAll is extended to serve the user-level list (scoped to the
authenticated user) so the v2 list handler can go through handler.DoReadAll like
the project list; the project branch is unchanged. Credentials are masked on
read via the model's existing maskCredentials, matching #2858.
2026-06-10 19:12:41 +00:00
kolaente 190fab8e6d feat(api/v2): add TOTP 2FA on /api/v2
Ports the current-user TOTP (2FA) endpoints from /api/v1 to the Huma-backed
/api/v2: get status, enroll, enable, and disable. Each is a custom,
current-user-scoped handler that resolves the authenticated user and refuses
non-local (OIDC/LDAP) accounts, preserving v1's local-account-only guard.

The image/jpeg QR-code endpoint is intentionally not ported here; it is a
binary-streaming route deferred to a later wave.
2026-06-10 17:58:16 +00:00
kolaente a562f69f02 feat(api/v2): add CalDAV tokens on /api/v2 2026-06-10 17:55:52 +00:00
kolaente a1621fec37 feat(api/v2): add task attachments on /api/v2 2026-06-10 10:22:39 +00:00
kolaente 5cdc785b49 fix(api/v2): return ErrProjectDoesNotExist for unknown project identifiers 2026-06-10 10:12:09 +00:00
kolaente 0a879e56a8 feat(api/v2): add task CRUD on /api/v2 2026-06-10 10:12:09 +00:00
kolaente 328de89c0b feat(api/v2): add bulk label replacement on /api/v2 2026-06-10 11:56:05 +02:00
kolaente 25a294d7bc feat(api/v2): add task position updates on /api/v2 2026-06-10 11:55:51 +02:00
kolaente 1e82c62ff7 feat(api/v2): add reactions on /api/v2 2026-06-09 21:34:22 +00:00