* feat: add ability to move boards between workspaces
Implements the "Move to workspace" feature (#344) allowing users to
relocate a board and all its contents (lists, cards, labels, checklists,
comments, activity) to a different workspace.
Key design decisions:
- Card member assignments are cleared on move (they reference
workspace-scoped members that may not exist in the target workspace)
- Comments and activity history are preserved (they reference global
user IDs, not workspace members)
- Slug conflicts in the target workspace are auto-resolved by
appending a UID suffix
- Permission model: requires board:edit in source workspace and
board:create in target workspace
- Templates and archived boards cannot be moved
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor: consolidate board queries in move mutation
Address review feedback:
- Consolidate 3 separate board queries into a single findFirst()
that fetches all needed fields (id, name, slug, type, isArchived,
workspaceId, createdBy)
- Fix slug fallback to use board.name instead of publicId for
human-readable URLs
Co-Authored-By: Claude <noreply@anthropic.com>
* fix: filter guest workspaces from move board destination list
Guests typically lack board:create permission in the target workspace,
so showing them as destinations leads to a confusing server rejection.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: extract getBoardForMove repo function
Moves the inline board query from the move mutation into the repo
layer, consistent with how every other board mutation fetches data.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add unit tests for board.move mutation
10 test cases covering auth, validation, permissions, slug conflict
resolution, and the happy path. Follows webhook.test.ts patterns.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: mark locale files as linguist-generated
GitHub will now auto-collapse compiled translation files (messages.json,
messages.ts, messages.po) in PR diffs and exclude them from language
stats. This makes PRs that touch i18n strings much easier to review.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: remove locale file changes from PR
Reverts locale file diffs and .gitattributes to match main, per review
feedback. The locale changes were unrelated translation updates that
inflated the PR diff.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: remove locale file changes from PR
Per @hjball's review: locale compilation/translations are handled
automatically on merge to main, so this PR shouldn't carry them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: align locale files with upstream/main
Previous removal commit used local main, which had drifted from
upstream. Re-syncing to upstream/main so the PR carries no locale diff.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(board-move): tighten deletedAt handling per review
Three changes addressing @hjball's review comments, all about the
schema treating deletedAt as optional metadata while the move-board
flow needs it as a load-bearing invariant.
1. getBoardForMove now filters isNull(deletedAt). Moving a tombstoned
board has no defensible semantics. Replaces the implicit
"the board exists in the table" check with an explicit
"the board is not soft-deleted" check.
2. Move-flow's clearing of cardToWorkspaceMembers now spans every
card under every list ever associated with this board, including
soft-deleted ones. If we leave member assignments on a deleted
card and that card is later restored, the assignments would
resurrect rogue references to workspace members from the OLD
workspace. Removed the isNull filters on both lists and cards in
that loop.
3. Move-flow now refuses to move into a soft-deleted target
workspace. workspaceRepo.getByPublicId did not previously project
deletedAt; extended its column selection so the call-site guard
in board.move can check it. (A wider fix to make the repo treat
deleted-as-not-found across all 14+ callers is left for a
separate PR — narrow scope here.)
Plus one regression test: throws NOT_FOUND when target workspace is
soft-deleted. All 11 board-move tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* Add cardPublicId to card webhook payloads
* fix: include card publicId in webhook payloads
Add data.card.publicId to webhook payloads while keeping data.card.id for compatibility, and update card/webhook call sites and tests so webhook consumers can reliably use public IDs.
* feat: add ticket numbers to cards with workspace prefix
- Add cardNumber to cards
- Add cardPrefix/cardCounter to workspace
- Populate initial prefixes and numbers via new migration
- Introduce generateWorkspacePrefix util and export it
- Include cardNumber in card-related API responses and search results
- Render and display tickets as PREFIX-NUMBER in UI
- Update Card component to accept ticketNumber
- Update CardModal and board cards to show number when available
- Add cardNumber to CommandPalette search results type
* feat: regen migration
* feat: enhance card and workspace schemas with cardNumber and indexing
- Updated the card repository to allocate card numbers atomically per workspace.
- Modified the card schema to include a cardNumber field and added an index on listId and cardNumber for improved query performance.
- Enhanced the workspace schema to include an index on cardPrefix for optimized lookups.
- Adjusted the migration journal to reflect the new schema changes and their timestamps.
- Updated the regex in workspace repository to allow alphanumeric prefixes in ticket IDs.
---------
Co-authored-by: Henry <henry_ball@hotmail.co.uk>
* feat: add card context menu and duplication functionality
* Implemented a context menu for cards allowing actions such as copying links, duplicating cards, and managing members, labels, and due dates
* Added modals for card duplication and context actions.
* Updated API with a new endpoint for duplicating cards, including options for copying labels, members, and checklists
* refactor: remove cardPublicId from context menu and related components
* Removed cardPublicId prop from CardContextMenu and CardContextMembersModal for cleaner context handling
* Updated scrollbar styling
* feat: add delete card functionality to context menu
* feat: workspace start of week column
* feat(l10n): add workspace setting for the first day of the week
Fixes#361
* feat: add Saturday as option
* chore: fix migration order
* chore: fix merge
---------
Co-authored-by: Henry <henry_ball@hotmail.co.uk>
* feat(api): add webhook CRUD API router and tests
Add tRPC router for managing workspace webhooks:
- list, create, update, delete endpoints (admin role required)
- test endpoint to send a synthetic payload to a webhook URL
- URL validation, event subscription filtering
- Unit tests for all router procedures
- Integration tests with PGlite test database
- Add vitest config and test infrastructure for API package
Depends on #391 (DB schema & repository).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(api): use assertPermission instead of assertUserInWorkspace
Replace assertUserInWorkspace with assertPermission("workspace:manage")
per project conventions. The permissions system is the preferred
authorization approach for new code.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(api): use @kan/db alias instead of relative imports in tests
Replace relative path imports (../../db/src/...) with the @kan/db
alias configured in vitest.config.ts for consistency and robustness.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(api): use webhookUrlSchema in router input validation
Cherry-pick router-related changes from b2cc9ac:
- Use extracted webhookUrlSchema zod validator in create/update
input schemas for consistent SSRF checks
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(api): replace dynamic import with static import for webhook utility
Add packages/api/src/utils/webhook.ts with sendWebhookToUrl,
createCardWebhookPayload, and webhookUrlSchema. Replace the dynamic
import() in the test endpoint with a static import at the top of the
file for better tree-shaking, type-checking, and readability.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(api): align sendWebhooksForWorkspace tests with merged PR #392
The merged delivery utility uses client-side event filtering
(getActiveByWorkspaceId takes 2 args, not 3). Update test assertions
to match the actual implementation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Henry <30578846+hjball@users.noreply.github.com>
* feat(api): add webhook delivery utility and card event integration
Add the core webhook delivery logic and wire it into card mutations:
- Add sendWebhookToUrl() with HMAC-SHA256 signing, 10s timeout
- Add sendWebhooksForWorkspace() for fan-out delivery (fire-and-forget)
- Add createCardWebhookPayload() for building webhook payloads
- Fire webhooks on card create, update, move, and delete events
- Add unit tests for webhook utility functions
Depends on #391 (DB schema & repository).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(api): use correct boardId in webhook payloads and add rejection safety
- Fix bug where workspaceId was incorrectly passed as boardId in all
webhook payloads — now uses board's publicId via boardPublicId
- Replace void sendWebhooksForWorkspace() with .catch() to prevent
unhandled promise rejections if the DB query inside fails
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(api): add SSRF protection to webhook delivery
Block webhook URLs targeting internal networks:
- Require HTTPS (reject HTTP)
- Block localhost, 127.0.0.1, ::1, 0.0.0.0
- Block cloud metadata endpoints (169.254.169.254, metadata.google.internal)
- Block private IP ranges (10.x, 172.16-31.x, 192.168.x)
- Add tests for all blocked URL patterns
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(api): use WebhookEvent type from schema instead of duplicating
Replace the hardcoded WebhookEventType union with the canonical
WebhookEvent type from @kan/db/schema, addressing reviewer feedback
on PR #392.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(api): improve webhook delivery safety and validation
Cherry-pick delivery-related changes from b2cc9ac:
- Extract URL validation into reusable webhookUrlSchema zod validator
for SSRF checks
- Wrap sendWebhooksForWorkspace in try/catch to prevent unhandled
promise rejections
- Document SSRF risk mitigation on sendWebhookToUrl
- Add corresponding tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* init: schema migration, isArchived added to
board table. refactor: board repo for isArchived filtering
* init: archived, unarchived API procedures. refactor: all query
* fix: migration error
* feat: add tabbed navigation for boards view
* Implemented a Listbox for mobile and a tabbed navigation for desktop to switch between "Boards" and "Archived" views.
* Introduced state management for active tab selection.
* Updated UI components to reflect the new navigation structure.
* init: frontend/boards lists & tabs
* chore: fixed font styling and spacing
* init:boardDropdown / boardView.
* chore:added translations
* Remove .cursor plan file from repo
* fix:build erros
* revert: remove locales changes
* fix:reverted changes under locales, replaced the archive and unarchive endpoints. Reorder migrations
* fix:migration issue
* fix: update journal.json
---------
Co-authored-by: Henry <henry_ball@hotmail.co.uk>
The checklist.update procedure was inheriting the default
protectedProcedure meta (GET /protected) instead of declaring
its own OpenAPI route, making it unreachable via the REST API.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Use `||` instead of `??` so empty strings also fall back to email.
The nullish coalescing operator (`??`) only catches null/undefined,
so users with an empty name string would appear nameless in emails.
The board.byId query was generating presigned S3 URLs for workspace
member avatars but not for card member avatars. This caused card avatars
to silently fall back to initials since the frontend helper returns ""
for raw S3 keys.
Closes#387
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add notifications schema
* chore: gen migration
* feat: add mention email template
* feat: add sendMentionEmail func
* feat: add repo funcs
* feat: update card router to send emails on mention
* fix: update the editor suggestion to show all members
* feat: skip pending members in sendMentionEmails
* feat: update comments to use tiptap editor
* refactor: replace presigned URL uploads with backend upload endpoints
* feat: update avatar upload to use new endpoint
* refactor: use createS3Client in auth hooks
* feat: generate presigned URLs for avatars
* fix: show avatar image in user menu
* fix: hide tooltip if content is empty
* fix: support external avatar URLs in generateAvatarUrl
* fix: remove content type restriction on attachments
* feat: setup schema for workspace roles
* chore: regen migration
* feat: add publicId to workspace roles
* feat: setup default permissions
* feat: add repo funcs
* feat: setup basic router interactions
* feat: add card permissions
* feat: assert permissions for lists
* feat: assert board permissions
* feat: assert permission for remaining routes
* feat: add permissions page to settings
* feat: enable updating member roles
* feat: order members by role and createdAt
* feat: allow editing individual permissions
* feat: reset role defaults
* feat: clear all permission overrides
* feat: allow users to delete entities they have created
* feat: set roleId when inviting new members
* feat: disable UI elements if user does not have permissions
* feat: allow admins to assign the admin role to other users
* feat: allow delete:list as default
* refactor: centre permissions modal
* chore: translations
* feat(workspace): sanitize member emails for non-admin users in workspace router
* feat(workspace): add email visibility setting and update member display logic
* fix(workspace): update default value for showEmailsToMembers column to true
* - Removed unnecessary type assertions
- Added anonymous name handling (anonymous_[publicId]) in the API
- Removed placeholder logic
- Reverted null check
- Avoided as unknown cast