Commit Graph

331 Commits

Author SHA1 Message Date
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 e25f997281 refactor(admin): extract shared admin overview, user-create and user-view helpers
Move the admin overview computation and struct into models.BuildOverview /
models.Overview, the admin create-user flow into models.CreateUserAsAdmin /
models.CreateUserBody, and the admin user response view into a new
pkg/routes/api/shared package (shared.AdminUser / shared.NewAdminUser) so both
the v1 and v2 admin routes call the same code. The v1 handlers are refactored
onto these helpers and stay byte-identical on the wire.
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 46b07a019c refactor(user): extract shared account orchestration into models/user/shared for v1+v2
Pull the business logic out of the v1 current-user account/settings handlers
into reusable functions so both v1 and the upcoming v2 handlers call one
implementation. No behavior change — the v1 handlers keep their HTTP-layer
quirks (input binding, validation, error mapping); only orchestration moves.

Homes are forced by the import graph:
- shared.GetAuthProviderName  (new pkg/routes/api/shared, above openid+user so it
                              can combine both without a cycle; routes-only helper)
- user.ChangeUserEmail        (CheckUserCredentials + UpdateEmail, both in user)
- models.ChangeUserPassword   (needs models.DeleteAllUserSessions; user can't import models)
- models.UpdateUserGeneralSettings / UpdateUserAvatarProvider
                              (need avatar.FlushAllCaches; user can't import avatar)

The general settings get a single shared wire struct, models.UserGeneralSettings
(tagged for both swaggo/govalidator and Huma): it is the update request body and
the nested settings on GET /user for v1 (replacing v1's UserSettings) and v2.
ExtraSettingsLinks is readOnly — populated from the user on read, ignored on
write. A dedicated struct is required because user.User's settings fields are
json:"-" so they don't leak when it is embedded in other responses.
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 cec74717fc refactor(task-attachment): share upload+download via pkg/web/files for v1+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
kolaente 2e02fe11ac feat(api/v2): add task relations on /api/v2 2026-06-09 20:42:00 +00:00
kolaente 5c960fccd5 feat(api/v2): add bulk task updates on /api/v2 2026-06-09 20:13:02 +00:00
kolaente 1aa9493bc3 feat(api/v2): add project duplication on /api/v2 2026-06-09 20:11:43 +00:00
kolaente 51e5c86f69 feat(api/v2): add kanban task-bucket moves on /api/v2 2026-06-09 20:01:20 +00:00
kolaente aa144b9a39 feat(api/v2): add task read-status marking on /api/v2 2026-06-09 19:50:58 +00:00
kolaente bf2a65dcaf feat(api/v2): add bulk assignee replacement on /api/v2 2026-06-09 19:42:16 +00:00
kolaente 9454cd3ec5 feat(time-tracking): expose time entries on the v2 API 2026-06-08 13:54:09 +00:00
kolaente c2e1b078ce feat(api/v2): add project team shares CRUD on /api/v2 2026-06-07 15:33:20 +00:00
kolaente c2d1e48c8c feat(api/v2): add team members (add/remove/admin-toggle) on /api/v2
The admin-toggle handler delegates to handler.DoUpdate — the same pipeline
v1's UpdateWeb wraps — instead of re-implementing the session/permission/commit
orchestration. TeamMember.Update now carries the persisted row back onto the
receiver so both v1 and v2 responses include id/created.
2026-06-07 10:48:23 +00:00
kolaente ed4ae0cd43 feat(api/v2): add saved filter CRUD on /api/v2 2026-06-07 10:40:20 +00:00
kolaente 9cddc137c5 feat(api/v2): add project user shares CRUD on /api/v2 2026-06-07 10:37:59 +00:00
kolaente 7158334699 fix(api/v2): return 200 from notifications mark-all (creates nothing) 2026-06-07 10:05:24 +00:00
kolaente 604e5850bc docs: trim wordy comments in v2 notifications 2026-06-07 10:05:24 +00:00
kolaente 1ca5367f27 feat(api/v2): add notifications list/mark-read + mark-all on /api/v2
Ports the v1 DatabaseNotifications routes to the Huma /api/v2 API:
- GET /notifications lists the caller's own notifications (paginated)
- PUT /notifications/{notificationid} marks one (un-)read
- POST /notifications is a custom action marking all as read; the
  link-share guard, session and commit live in the handler since there
  is no CRUDable Do* for a bulk mark.

Adds fixture rows and a webtest matrix mirroring the v1 model behaviour
(own-only visibility, mark-(un)read, link-share refusal on every route).
2026-06-07 10:05:24 +00:00
kolaente fb4bca34dd docs: trim wordy comments to load-bearing whys 2026-06-07 09:57:51 +00:00
kolaente 1b47932916 feat(api/v2): add subscribe/unsubscribe on /api/v2
Port the Subscription resource from /api/v1 to the Huma-backed /api/v2:
POST /subscriptions/{entity}/{entityID} subscribes, DELETE unsubscribes.

The {entity} discriminator is bound as a string path param with an
enum:"project,task" tag; the model's CanCreate/CanDelete derive the numeric
EntityType from it and reject unknown kinds. Permissions and the
already-subscribed/forbidden checks come from the shared model via DoCreate/
DoDelete, identical to v1's generic handler. Mark the model's server-controlled
fields readOnly and add doc tags for the v2 schema.
2026-06-07 09:57:51 +00:00
kolaente 24188480c4 feat(api/v2): return 422 with invalid_fields for validation errors 2026-06-06 21:09:56 +00:00
kolaente 45e05a5d27 feat(api/v2): enforce validation centrally in the Register wrapper 2026-06-06 21:09:56 +00:00
kolaente aac0322975 refactor(webhooks): mask write-only credentials in the model so create/update never echo them
Webhook.ReadAll already cleared the secret and basic-auth from responses,
but Create and Update did not, so the v2 handler patched the gap with a
maskWebhookCredentials helper. Centralize the masking in the model via a
maskCredentials helper called after every DB write (ReadAll, Create,
Update) and drop the v2 handler helper.

The credentials are client-provided, not server-generated: the DB row
keeps them and outgoing deliveries reload + HMAC-sign from the DB copy,
so clearing the returned in-memory struct is correct write-only handling.

Webhook is a shared model, so v1's create/update responses also stop
echoing the submitted secret/auth — intended, and approved by the
maintainer.
2026-06-06 19:50:41 +00:00
kolaente cf1f7c3309 feat(api/v2): add project webhooks CRUD on /api/v2
Port the project-webhook routes under /projects/{project}/webhooks to
the Huma /api/v2: list, create, update (events only), delete. There is
no ReadOne — webhooks carry secrets — so no max_permission and no
AutoPatch PATCH; update is PUT only, mirroring v1.

The resource self-registers and is gated by the webhooks.enabled config
flag inside the registrar (RegisterAll runs after config loads). The
write-only secret and basic-auth credentials are cleared from
create/update responses, matching how ReadAll masks them.
2026-06-06 19:50:41 +00:00
kolaente d76c009808 fix(api/v2): map ValidationHTTPError to its HTTP status
translateDomainError only recognized web.HTTPErrorProcessor, so a
ValidationHTTPError from InvalidFieldError (e.g. an unknown webhook
event) leaked as a 500 instead of the 412 v1 returns. It carries the
status via GetHTTPCode() but cannot implement HTTPErrorProcessor because
the embedded web.HTTPError field shadows the method name. Add a
GetHTTPCode/GetCode branch so v2 surfaces the right status and preserves
the v1 numeric code on the body.
2026-06-06 19:50:41 +00:00
kolaente 43bbeed1c8 feat(api/v2): add task assignees (create/list/delete) on /api/v2
Port the v1 /tasks/{projecttask}/assignees routes to the Huma-backed
/api/v2. The resource self-registers (RegisterTaskAssigneeRoutes) and
reuses the model's Can* methods via the generic Do* handlers:

- POST /tasks/{projecttask}/assignees  → assign a user (body: user_id)
- GET  /tasks/{projecttask}/assignees  → list assignees (as users)
- DELETE /tasks/{projecttask}/assignees/{user} → un-assign

The list element type is []*user.User (assignees are returned as the
assigned users), which differs from the create body (a TaskAssginee
carrying user_id); the list handler type-asserts to []*user.User.
create/delete require write access to the task's project, list requires
read — enforced at the model level.

The webtest re-proves the full v1 permission matrix on the v2 surface
(read-only shares forbidden, write/admin allowed for create and delete;
already-assigned, no-project-access, missing-user, and missing-task
error codes) so v1's routes can be removed later.
2026-06-06 19:06:12 +00:00
kolaente b107685063 feat(api/v2): add link sharing (create/read/list/delete)
Port the LinkSharing resource from /api/v1 to the Huma-backed /api/v2 under
/projects/{project}/shares. Self-registers via AddRouteRegistrar and is gated
on ServiceEnableLinkSharing, checked inside the registrar so a disabled
instance exposes no routes.

There is no update operation, mirroring v1: a share is created, read, listed
or deleted, never modified in place. Permissions stay at the model level via
the generic Do* handlers (project write to create read/write shares and to
delete; project admin to create an admin share and to list).

ReadOne is ported faithfully including a latent v1 quirk: CanRead resolves the
parent project from the share hash, which the by-id route never carries, so a
by-id read always 404s. The webtest pins this so a future fix is deliberate.
2026-06-05 09:17:25 +00:00
kolaente cae89caef2 feat(api/v2): add bot user CRUD on /api/v2
Port the BotUser resource from /api/v1's /user/bots routes to the
Huma-backed /api/v2, preserving every v1 behavior:

- Full CRUD at /user/bots and /user/bots/{bot} with v2 verbs (POST
  creates, PUT updates; PATCH is synthesised by AutoPatch).
- ReadAll returns only the caller's own bots; read/update/delete of an
  unowned or missing bot is refused with 403, since ownership is resolved
  by loading the user (no existence disclosure, no 404 branch).
- Create requires a real user account and rejects link shares, the
  bot- username prefix is enforced, and bots are created without an
  email or password — all delegated to the unchanged model layer.
- ReadOne surfaces max_permission via the shared value-embed pattern and
  carries an ETag for conditional requests.

doc/readOnly tags are added to the exposed user.User fields the bot
response surfaces, and to BotUser.Status, so the v2 OpenAPI schema is
documented. The model and v1 routes are untouched.

The webtest ports the v1 model-level permission matrix to the v2 HTTP
surface and adds the v2-only ETag/304 and merge-patch coverage.
2026-06-05 08:51:39 +00:00
kolaente 9e234911f2 feat(api/v2): add API token list/create/delete on /api/v2
Port the APIToken resource from /api/v1 to the Huma-backed /api/v2 at
top-level /tokens. List/create/delete only — no ReadOne, no Update,
matching v1. The list operation accepts an owner_id query param to list
a caller-owned bot's tokens; create returns the cleartext token exactly
once. Permissions are enforced by the model via the shared Do* helpers.

The webtest ports the v1 model-level permission matrix onto the v2 HTTP
surface (owner isolation, exact list cardinality, bot-owner authz,
validation, forbidden delete) so v2 proves the contract independently.
2026-06-05 08:49:23 +00:00
kolaente 413006e9ba feat(api/v2): add task labels (create/list/delete) on /api/v2
Port the LabelTask resource (labels attached to a task) from the frozen
/api/v1 to the Huma-backed /api/v2 as nested routes under
/tasks/{projecttask}/labels:

- GET    list the labels on a task (read access to the task)
- POST   attach a label to a task (write access to the task + access to the label)
- DELETE detach a label from a task (write access to the task)

There is no read-one or update for a label-task relation, so no
max_permission. Adds doc tags and marks the server-set created timestamp
readOnly on the shared model. Permissions stay enforced at the model
layer via the existing Can* methods through handler.Do*.
2026-06-05 08:33:47 +00:00
kolaente 171d14d7b8 feat(api/v2): add session list/delete on /api/v2
Ports the Session resource from /api/v1 to the Huma-backed /api/v2 with
list and delete only — sessions are created by the login flow, not CRUD,
so there is no create/read-one/update (and no max_permission or AutoPatch).

The delete path param is a string UUID (path:"session"), not an int64 id,
mapping to the model's string ID. ReadAll is type-asserted to
[]*models.Session; permissions stay at the model level via DoReadAll/DoDelete.

The v2 webtest mirrors v1's session-CRUD matrix (list own vs others',
delete own, non-owner forbidden, nonexistent 404). The login/refresh
auth-flow cases stay on v1.
2026-06-05 08:21:48 +00:00
kolaente 184384b68c feat(api/v2): report max_permission on team reads 2026-06-05 08:06:54 +00:00
kolaente 9a184fdfab feat(api/v2): report max_permission on task comment reads
Convert taskCommentsRead to the labelReadBody embed pattern: return a
taskCommentReadBody that embeds models.TaskComment and adds a read-only
max_permission field, folded into the ETag via conditionalReadResponse so
a permission change invalidates a cached read. The update handler takes the
same read-shaped body so AutoPatch's GET->PUT echo of max_permission validates.
2026-06-05 07:43:38 +00:00