Commit Graph

1218 Commits

Author SHA1 Message Date
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 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 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 3a84c491ae feat(models): let TaskCollection force a flat task list
v1's TaskCollection.ReadAll is polymorphic: a kanban view returns
[]*Bucket, everything else []*Task. v2 splits the task list into a
flat-tasks endpoint and a separate buckets-with-tasks endpoint, so the
flat endpoint needs ReadAll to return tasks even for a kanban view.
SetForceFlatTasks toggles that; v1 leaves it unset and keeps its shape.
2026-06-11 18:31:03 +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 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 dc935f263c docs(api/v2): tag task attachment fields for the v2 schema 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 4316554b27 docs(api/v2): tag task fields for the v2 schema 2026-06-10 10:12:09 +00:00
kolaente 0e0ececa2d docs(api/v2): tag bulk label fields for the v2 schema 2026-06-10 11:56:05 +02:00
kolaente a6a073329f docs(api/v2): tag task position fields for the v2 schema 2026-06-10 11:55:51 +02:00
kolaente f5e7e9ddde docs(api/v2): tag reaction fields for the v2 schema 2026-06-09 21:34:22 +00:00
kolaente da76d393d9 docs(api/v2): tag task relation fields for the v2 schema 2026-06-09 20:42:00 +00:00
kolaente 711545e9f2 docs(api/v2): tag bulk task fields for the v2 schema 2026-06-09 20:13:02 +00:00
kolaente d5bcbe39b4 docs(api/v2): tag project duplication fields for the v2 schema 2026-06-09 20:11:43 +00:00
kolaente 9eca20fe43 docs(api/v2): tag task bucket fields for the v2 schema 2026-06-09 20:01:20 +00:00
kolaente 732cd115a5 docs(api/v2): tag bulk assignee fields for the v2 schema 2026-06-09 19:42:16 +00:00
Claude c9c2c58c16 feat(labels): let bot owners manage labels created by their bots
Bot owners inherit read/update/delete permission on labels created by
bots they own, mirroring the bot-owner branch already used by API tokens
(see api_tokens_permissions.go). Without this, a label a bot creates is
permanently locked to that bot and the human owner cannot maintain it.

https://claude.ai/code/session_016x6mUPJuuQEeXpHY814iLh
2026-06-09 11:40:04 +00:00
kolaente 0bd7f956f5 fix(time-tracking): reject inverted time-entry intervals 2026-06-08 13:54:09 +00:00
kolaente 4a558fc57a fix(api/v2): expose v2-only token route groups via the routes endpoint 2026-06-08 13:54:09 +00:00
kolaente 74510bb00a fix(api/v2): group time-entries token routes under their own scope 2026-06-08 13:54:09 +00:00
kolaente b8b376c53a test(time-tracking): cover the time_entries model 2026-06-08 13:54:09 +00:00
kolaente e197b1912f feat(time-tracking): count tracked time entries per task 2026-06-08 13:54:09 +00:00
kolaente 0c5a0a99ec feat(time-tracking): dispatch time-entry events 2026-06-08 13:54:09 +00:00
kolaente 4bd6a6c4f7 feat(time-tracking): filter time entries with the task DSL 2026-06-08 13:54:09 +00:00
kolaente 42795518e9 feat(time-tracking): add the time_entries model 2026-06-08 13:54:09 +00:00
kolaente 26c067cc38 refactor: extract preprocessFilterString from task filter parsing 2026-06-08 13:54:09 +00:00
kolaente 627cd0a6f4 docs(api/v2): tag project team share fields for the v2 schema 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 ef256273e0 docs(api/v2): annotate TeamMember fields for the v2 schema 2026-06-07 10:48:23 +00:00
kolaente a52ee1593a docs(api/v2): tag SavedFilter fields for the v2 schema 2026-06-07 10:40:20 +00:00
kolaente 2c0608e47b docs(api/v2): tag project user share fields for the v2 schema 2026-06-07 10:37:59 +00:00
kolaente 05c9c07e19 docs(api/v2): add doc/readOnly tags to notification model fields 2026-06-07 10:05:24 +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 5855ccc1d4 docs(webhooks): version-qualify the events endpoint link in the events field doc
In the v2 OpenAPI context a bare /webhooks/events reads as /api/v2/webhooks/events,
which does not exist — the events listing endpoint lives only on /api/v1. Point the
doc string at the absolute v1 path so v2 clients are not misled.
2026-06-06 19:50:41 +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 3647551a79 docs(api/v2): tag Webhook fields for the v2 schema
Add doc tags to every exposed Webhook field, mark the server-controlled
ones (id, project_id, user_id, created_by, created, updated) readOnly,
and mark the secret and basic-auth credentials writeOnly. All three tags
are ignored by swaggo/XORM/govalidator, so v1 is unaffected.
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 f90868c595 docs(models): tag TaskAssignee fields for the v2 schema
Add doc: tags so Huma can describe user_id and created in the /api/v2
OpenAPI spec (it can't read Go comments), mark the server-set created
field readOnly, and give it an explicit json:"created" tag so it
serializes in snake_case like the rest of the v2 surface.
2026-06-06 19:06:12 +00:00
kolaente bcade97fa4 fix(link-sharing): resolve share read permission via project id so by-id reads work
LinkSharing.CanRead resolved the parent project from the share hash, but a
by-id read (GET /projects/{project}/shares/{share}) only carries the numeric
id, never the hash — so the project lookup returned ErrProjectShareDoesNotExist
and every read-one 404'd, even for the share's owner. This affected both v1 and
v2.

Resolve the project from ProjectID when it is set (the by-id read path), keeping
the hash lookup as a fallback for resolving a share purely by its public hash.
The permission semantic is unchanged — you can read a share if you can read its
parent project; only the project lookup changes. ReadOne still scopes by
id AND project_id, so a share id from another project the caller can access is
not leaked (404, no IDOR).

Flips the v2 webtest's pinned 404 cases to assert success and adds the
cross-project IDOR and non-member negatives.
2026-06-05 09:17:25 +00:00
kolaente 4e5751ebfe docs(api/v2): tag LinkSharing fields for the v2 schema
Add doc:/readOnly:/writeOnly: tags to the shared LinkSharing model so the
Huma-generated /api/v2 schema documents every exposed field. password is
write-only (set on create, never returned); hash, sharing_type, id,
created, updated and shared_by are server-controlled and marked read-only.
swaggo/XORM/govalidator ignore these tags, so v1 is unaffected.
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 c3c648f060 docs(models): tag APIToken fields for v2 OpenAPI reflection
Add doc:/readOnly: tags (and minLength on title) so the Huma-backed
/api/v2 surface documents and schema-validates APIToken. Tags are
inert for v1 (swaggo/XORM/govalidator ignore them).
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 6bbb700f36 docs(models): add doc and readOnly tags to Session fields for v2
Every Session field is server-controlled (sessions are created by login,
not CRUD), so all exposed fields get readOnly:"true". The doc tags feed
Huma's reflected /api/v2 schema; they are inert for v1.
2026-06-05 08:21:48 +00:00
kolaente af2482aab2 fix(labels): report owner-level max_permission
Label writes/deletes are owner-only (CanUpdate/CanDelete), but hasAccessToLabel
derived max_permission from the accessible task's permission with a read fallback
for the creator branch — so owners showed as read-only and a task-admin reading
a label via that task showed as a label admin. Derive it from ownership instead:
owner -> admin, otherwise read. Corrects the value CanRead returns for both v1's
x-max-permission header and the new v2 max_permission body field.
2026-06-04 21:16:51 +00:00
kolaente 984a2633cc docs(task-comments): trim comments to the non-obvious why
Cut narration a reader can infer from the code (envelope element type,
path-param binding, per-case test descriptions). Keep the non-obvious
rationale: IDOR scoping, RFC 9110 etag quoting, why the feature gate sits
in the registrar, and the author-only fixture crux.
2026-06-03 19:57:26 +00:00
kolaente 808ef2534e fix(task-comments): derive update event doer from authenticated user
TaskComment.Update used tc.Author as the TaskCommentUpdatedEvent doer, but
that field is bound from the request body. A client could omit it (nil doer,
breaking the event) or spoof another user. Resolve the doer from the session
auth via GetUserOrLinkShareUser instead, mirroring Create and Delete. CanUpdate
already guarantees the authenticated user is the comment's author, so this is
both correct and consistent. Affects v1 and v2, which share the model.
2026-06-03 19:57:26 +00:00
kolaente 3271a1e1af feat(api/v2): add nested task comment CRUD
Add TaskComment CRUD on /api/v2 under /tasks/{task}/comments, mirroring
the project_views nested-resource shape. The resource is feature-gated by
config.ServiceEnableTaskComments, checked inside the registrar so it runs
after config has loaded. Self-registers via init()+AddRouteRegistrar; no
routes.go change. ReadAll exposes the order_by (asc/desc) query param.

Adds doc:/readOnly: tags to the shared TaskComment model fields and a
TestHumaTaskComment webtest covering list/read/create/update/delete plus
negatives (non-author forbidden, comment under the wrong task -> 404).
2026-06-03 19:57:26 +00:00