Commit Graph

14153 Commits

Author SHA1 Message Date
surfingbytes 2eb594634a chore: merge upstream/main into landing-page 2026-06-08 05:58:25 +00:00
kolaente c2e1b078ce feat(api/v2): add project team shares CRUD on /api/v2 2026-06-07 15:33:20 +00:00
kolaente 627cd0a6f4 docs(api/v2): tag project team share fields for the v2 schema 2026-06-07 15:33:20 +00:00
Frederick [Bot] a2be36b5fe [skip ci] Updated swagger docs 2026-06-07 11:36:48 +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 ed4ae0cd43 feat(api/v2): add saved filter CRUD on /api/v2 2026-06-07 10:40:20 +00:00
kolaente a52ee1593a docs(api/v2): tag SavedFilter fields for the v2 schema 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 2c0608e47b docs(api/v2): tag project user share fields for the v2 schema 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 05c9c07e19 docs(api/v2): add doc/readOnly tags to notification model fields 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 67bc3ff4f1 test(api/v2): cover central validation (422, invalid_fields, full-body webhook updates) 2026-06-06 21:09:56 +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 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 98741d8171 test(api/v2): webhook CRUD permission matrix and config gate
Port the v1 webhook webtest to /api/v2 and extend it to the full
permission gradient the model enforces: list needs read access while
create/update/delete need write (Project.CanWrite), exercised across an
owned project and read/write/admin shares plus a no-access project. Also
assert credential masking, events-only updates, the 412 validation path,
and that the routes 404 when webhooks.enabled is false.

Add fixture webhooks 2-5 in projects 9/10/11/2 to back the matrix; they
do not collide with the e2e tests, which scope to project 1.
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 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 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 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
renovate[bot] 43d6e14289 chore(deps): update dev-dependencies 2026-06-06 19:05:39 +00:00
Claude a35518a099 docs: redirect translation requests to translation guide 2026-06-06 21:05:21 +02:00
renovate[bot] 8e09b69fb3 chore(deps): update dev-dependencies to v26.14.0 2026-06-06 11:18:01 +00:00
Frederick [Bot] 380e0afb86 [skip ci] Updated swagger docs 2026-06-05 10:13:32 +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 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 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 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 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 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 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 a763fed573 test(api/v2): assert both ETags non-empty in team permission test 2026-06-05 08:06:54 +00:00
kolaente 58d882d36d test(api/v2): assert team max_permission + etag reflects permission 2026-06-05 08:06:54 +00:00
kolaente 184384b68c feat(api/v2): report max_permission on team reads 2026-06-05 08:06:54 +00:00
kolaente 2fc11630b4 test(api/v2): assert task comment max_permission and per-caller ETag
Assert the read-one body carries max_permission, and add
TestHumaTaskComment_ETagReflectsPermission proving two users with different
permission on a comment's parent task (project 9: owner user6 vs read-share
user1, comment 6 on task 18) receive different ETags.
2026-06-05 07:43:38 +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
kolaente 62979ff342 test(api/v2): strengthen project max_permission assertions
Decode the ReadOne/Normal body and assert MaxPermission equals the real
permission (admin for the owner) instead of substring-matching, so a
regression to 0 or null is caught precisely.

Add TestHumaProject_NullMaxPermissionRoundTrips: create/update return
max_permission:null, and PUTting that response body back verbatim must
succeed (200, not 422). max_permission is readOnly so Huma ignores it on
write, and Permission.UnmarshalJSON treats JSON null as a no-op.
2026-06-05 07:40:07 +00:00
kolaente 33b9aa6292 test(api/v2): isolate project tests per-handler, not via shared harness
The project test port had added db.LoadFixtures() into the shared
webHandlerTestV2.serve(), reloading fixtures before every request. That
wiped runtime-created rows between requests within a test, breaking the
create-then-read-back contract every v2 resource relies on (e.g.
TestHumaTeam/Create/Public read its freshly-created team back and got 403).

Revert that shared-harness change and isolate the project/archived tests
the way the team and label tests do: each subtest builds its own handler
via handlerFor, so it runs against freshly loaded fixtures (setupTestEnv
reloads once per handler), while a create-then-read-back sequence reuses
one handler within the subtest.
2026-06-05 07:40:07 +00:00
kolaente bec991288b refactor(api/v2): align project max_permission to the shared embed pattern
Read-one now returns a projectReadBody embedding models.Project with
max_permission always populated from CanRead, matching the labels/views
value-embed pattern instead of gating it behind expand=permissions.
CanRead yields a real permission for every readable project (Favorites
pseudo-project and saved-filter-backed ones included), so the field is
always meaningful on a read.

Project remains the no-ETag exception: the response carries user-scoped
favorite/subscription state that changes without bumping Updated, so it
is served fresh.

Update routes its body through the read shape so AutoPatch's GET→PUT echo
of the read-only max_permission validates. Create/Update return null for
max_permission (not computed there) rather than a misleading 0 (=read).
2026-06-05 07:40:07 +00:00
kolaente 25665f887f test(api/v2): port full v1 project coverage (permission matrix, archived)
Bring the v2 project webtest to 1:1 parity with v1's TestProject and
TestArchived so the v2 routes independently prove everything v1 proved:

- Full sharing matrix on ReadOne/Update/Delete across team, user,
  parent-team and parent-user shares x read/write/admin, asserting
  allow/deny and (for ReadOne) the granted max_permission level via
  expand=permissions (v2's replacement for v1's x-max-permission header).
- Create permission matrix via parent_project_id (forbidden parent,
  parent-team/user write+admin allowed, read-only denied), nonexistent
  parent (404), and title-too-long (422) on both Create and Update.
- Create response assertions (owner echo, description, tasks not embedded).
- ReadAll search (q=) with exact cardinality and archived propagation to
  child project 21.
- New TestHumaArchived ports the HTTP-observable archived behaviours:
  no edit/unarchive under an archived parent, self-archived edit denied
  but unarchive allowed, and archiving a project (412 / ErrCodeProjectIsArchived).

Make webHandlerTestV2.serve reload fixtures per request, mirroring v1's
per-request fixture reload, so mutating subtests don't leak state across
the shared Echo instance.
2026-06-05 07:40:07 +00:00
kolaente a3370a9a49 fix(api/v2): drop ETag/conditional read on project get
The project read response is enriched with user-scoped, derived state
(subscription, favorite, views, computed archived state) that can change
without bumping project.Updated. An ETag built only from Updated would
therefore hand out stale 304s and hide those changes from the client.

Serve project reads fresh on every call by returning the no-ETag
singleBody envelope and dropping the conditional.Params input. Labels
keep their ETag because their response has no such volatile derived
fields. Update the ReadOne/Normal webtest to assert no ETag is sent.
2026-06-05 07:40:07 +00:00