From 1f1a1b770fd5ee63b4cd64c05a4d8df579dc2aff Mon Sep 17 00:00:00 2001 From: Nikolai Giman Date: Tue, 4 Aug 2026 20:40:31 +0200 Subject: [PATCH 1/2] fix: #98 --- ...FetchRolesPermissionsCollaborationRoles.ts | 16 +++ .../collaboration/CollaborationController.ts | 1 + .../collaboration/CollaborationManager.ts | 7 +- .../collaboration/CollaborationRepository.ts | 69 ++++++++-- docs/1.getting-started/1.index.md | 2 +- docs/2.features/10.organizations.md | 20 ++- docs/5.collaboration/1.members.md | 4 +- docs/6.faq/1.index.md | 4 +- .../collaboration-roles.access.test.ts | 109 ++++++++++++++++ .../src/api/__tests__/collaboration.test.ts | 121 ++++++++++++++++++ 10 files changed, 336 insertions(+), 17 deletions(-) create mode 100644 taskview-packages/taskview-api/src/api/__tests__/collaboration-roles.access.test.ts diff --git a/api/src/tv-modules/collaboration-roles/middlewares/CanFetchRolesPermissionsCollaborationRoles.ts b/api/src/tv-modules/collaboration-roles/middlewares/CanFetchRolesPermissionsCollaborationRoles.ts index d6d329f..4ddc288 100644 --- a/api/src/tv-modules/collaboration-roles/middlewares/CanFetchRolesPermissionsCollaborationRoles.ts +++ b/api/src/tv-modules/collaboration-roles/middlewares/CanFetchRolesPermissionsCollaborationRoles.ts @@ -1,5 +1,8 @@ import type { NextFunction, Request, Response } from 'express'; +import { GoalPermissionsFetcher } from '../../../core/GoalPermissionsFetcher'; import { $logger } from '../../../modules/logget'; +import { GoalPermissions } from '../../../types/auth.types'; +import { logError } from '../../../utils/api'; export const CanFetchRolesPermissionsCollaborationRoles = async (req: Request, res: Response, next: NextFunction) => { const goalId = req.body.goalId ? req.body.goalId : req.params.goalId; @@ -19,5 +22,18 @@ export const CanFetchRolesPermissionsCollaborationRoles = async (req: Request, r return next(); } + const permissions = await req.appUser.permissionsFetcher + .getPermissionsForType(Number(goalId), GoalPermissionsFetcher.PERMISSION_TYPE_FOR_GOAL) + .catch(logError); + + if (!permissions) { + $logger.error('Can not get permissions for CanFetchRolesPermissionsCollaborationRoles middleware'); + return res.status(500).end(); + } + + if (permissions.hasPermissions(GoalPermissions.GOAL_CAN_MANAGE_USERS)) { + return next(); + } + return res.status(403).end(); }; diff --git a/api/src/tv-modules/collaboration/CollaborationController.ts b/api/src/tv-modules/collaboration/CollaborationController.ts index 382298e..1520bdf 100644 --- a/api/src/tv-modules/collaboration/CollaborationController.ts +++ b/api/src/tv-modules/collaboration/CollaborationController.ts @@ -16,6 +16,7 @@ import { } from './collaboration.types'; export class CollaborationController { + /** @deprecated */ fetchAllUsers = async (req: Request, res: Response) => { const users = await req.appUser.collaborationManager.fetchAllUsers(); return res.tvJson(users); diff --git a/api/src/tv-modules/collaboration/CollaborationManager.ts b/api/src/tv-modules/collaboration/CollaborationManager.ts index d612842..6197c63 100644 --- a/api/src/tv-modules/collaboration/CollaborationManager.ts +++ b/api/src/tv-modules/collaboration/CollaborationManager.ts @@ -25,6 +25,7 @@ export class CollaborationManager { this.repository = new CollaborationRepository(); } + /** @deprecated */ async fetchAllUsers(): Promise { const sharedGoals = await this.user.goalsManager.fetchSharedGoals(); @@ -70,6 +71,7 @@ export class CollaborationManager { return Object.values(resultMap); } + /** @deprecated */ async fetchUsersForGoal(args: FetchGoalUsersArg): Promise { const users = await this.repository.fetchUsersForGoal(args.goalId); @@ -104,6 +106,7 @@ export class CollaborationManager { return Object.values(resultMap); } + /** @deprecated*/ async toggleUserRoles(args: ToggleUserRolesArg): Promise { return await this.repository.updateUserRoles(args.userId, args.roles); } @@ -181,7 +184,7 @@ export class CollaborationManager { return []; } - + const resultMap: Record = {}; users.forEach((item) => { @@ -214,7 +217,7 @@ export class CollaborationManager { return []; } - + const resultMap: Record = {}; users.forEach((item) => { diff --git a/api/src/tv-modules/collaboration/CollaborationRepository.ts b/api/src/tv-modules/collaboration/CollaborationRepository.ts index d73cc06..ff496ae 100644 --- a/api/src/tv-modules/collaboration/CollaborationRepository.ts +++ b/api/src/tv-modules/collaboration/CollaborationRepository.ts @@ -1,5 +1,6 @@ -import { and, eq, inArray } from 'drizzle-orm'; +import { and, eq, exists, inArray } from 'drizzle-orm'; import { + CollaborationRolesSchema, CollaborationUsersSchema, type CollaborationUsersSchemaTypeForSelect, CollaborationUsersToGoalsSchema, @@ -24,6 +25,7 @@ export class CollaborationRepository { this.db = Database.getInstance(); } + /** @deprecated */ async fetchAllUsers(goalIds: number[]): Promise { if (goalIds.length === 0) { return []; @@ -35,6 +37,10 @@ export class CollaborationRepository { FROM collaboration.users u left join collaboration.users_to_goals utg on u.id = utg.user_id LEFT JOIN collaboration.users_to_roles utr ON u.id = utr.user_id + AND EXISTS ( + SELECT 1 FROM collaboration.roles r + WHERE r.id = utr.role_id AND r.goal_id = utg.goal_id + ) WHERE utg.goal_id IN (${placeholders}) `; @@ -92,7 +98,8 @@ export class CollaborationRepository { return result.rows[0]; } - + + /** @deprecated */ async fetchUsersForGoal(goalId: number): Promise { const query = ` SELECT u.*, u.invitation_date::text, utr.role_id, utg.goal_id @@ -112,6 +119,7 @@ export class CollaborationRepository { return result.rows; } + /** @deprecated */ async fetchUsersForGoals(goalIds: number[]): Promise { if (goalIds.length === 0) { return []; @@ -146,6 +154,7 @@ export class CollaborationRepository { return !!(result.rowCount && result.rowCount > 0); } + /** @deprecated */ async updateUserRoles(userId: number, roles: number[]): Promise { const deleteQuery = `DELETE FROM collaboration.users_to_roles WHERE user_id = $1`; let i = 1; @@ -247,13 +256,29 @@ export class CollaborationRepository { async toggleUserRolesNew(args: CollaborationArgToggleUserRoles): Promise { const result = await callWithCatch(() => this.db.dbDrizzle.transaction(async (tx) => { - await tx - .delete(CollaborationUsersToRolesSchema) - .where(eq(CollaborationUsersToRolesSchema.userId, args.userId)); - if (args.roles.length > 0) { + + const goalRoles = await tx + .select({ id: CollaborationRolesSchema.id }) + .from(CollaborationRolesSchema) + .where(eq(CollaborationRolesSchema.goalId, args.goalId)); + const goalRoleIds = goalRoles.map((role) => role.id); + + if (goalRoleIds.length > 0) { + await tx + .delete(CollaborationUsersToRolesSchema) + .where( + and( + eq(CollaborationUsersToRolesSchema.userId, args.userId), + inArray(CollaborationUsersToRolesSchema.roleId, goalRoleIds) + ) + ); + } + + const rolesToAssign = args.roles.filter((roleId) => goalRoleIds.includes(roleId)); + if (rolesToAssign.length > 0) { return await tx .insert(CollaborationUsersToRolesSchema) - .values(args.roles.map((roleId) => ({ userId: args.userId, roleId }))) + .values(rolesToAssign.map((roleId) => ({ userId: args.userId, roleId }))) .returning(); } return []; @@ -284,7 +309,20 @@ export class CollaborationRepository { ) .leftJoin( CollaborationUsersToRolesSchema, - eq(CollaborationUsersSchema.id, CollaborationUsersToRolesSchema.userId) + and( + eq(CollaborationUsersSchema.id, CollaborationUsersToRolesSchema.userId), + exists( + this.db.dbDrizzle + .select() + .from(CollaborationRolesSchema) + .where( + and( + eq(CollaborationRolesSchema.id, CollaborationUsersToRolesSchema.roleId), + eq(CollaborationRolesSchema.goalId, CollaborationUsersToGoalsSchema.goalId) + ) + ) + ) + ) ) .where(inArray(CollaborationUsersToGoalsSchema.goalId, goalIds)) ); @@ -308,7 +346,20 @@ export class CollaborationRepository { ) .leftJoin( CollaborationUsersToRolesSchema, - eq(CollaborationUsersSchema.id, CollaborationUsersToRolesSchema.userId) + and( + eq(CollaborationUsersSchema.id, CollaborationUsersToRolesSchema.userId), + exists( + this.db.dbDrizzle + .select() + .from(CollaborationRolesSchema) + .where( + and( + eq(CollaborationRolesSchema.id, CollaborationUsersToRolesSchema.roleId), + eq(CollaborationRolesSchema.goalId, CollaborationUsersToGoalsSchema.goalId) + ) + ) + ) + ) ) .where(eq(CollaborationUsersToGoalsSchema.goalId, goalId)) ); diff --git a/docs/1.getting-started/1.index.md b/docs/1.getting-started/1.index.md index d5b3792..fc51326 100644 --- a/docs/1.getting-started/1.index.md +++ b/docs/1.getting-started/1.index.md @@ -1,6 +1,6 @@ --- title: What is TaskView -description: TaskView is an open-source, self-hosted project and task management platform. Features Kanban boards, dependency graphs, team collaboration, RBAC, GitHub/GitLab sync, and full data ownership. Free alternative to other PM for teams who need privacy and control. +description: TaskView is a source-available, self-hosted project and task management platform. Features Kanban boards, dependency graphs, team collaboration, RBAC, GitHub/GitLab sync, and full data ownership. Free alternative to other PM for teams who need privacy and control. navigation: icon: i-lucide-house --- diff --git a/docs/2.features/10.organizations.md b/docs/2.features/10.organizations.md index d33244b..78732d4 100644 --- a/docs/2.features/10.organizations.md +++ b/docs/2.features/10.organizations.md @@ -50,7 +50,7 @@ Only **owners** and **admins** can manage members. The **Members** tab is not vi 2. Go to the **Members** tab 3. Enter an email address and click **Add Member** -New members are added with the **member** role by default. You can change their role to **admin** using the role dropdown next to their name. Members can only be invited by email. The person needs to have a TaskView account with that email. +New members are added with the **member** role by default. You can change their role to **admin** using the role dropdown next to their name. Members are invited by email address. The person doesn't need a TaskView account yet - membership is stored against the email, so you can add someone in advance and they join the organization as soon as they sign up with that address. ::callout{icon="i-lucide-alert-triangle" color="warning"} When a project member with the **Manage users** permission invites someone into a project, that person is automatically added to the organization as a **member** - even though only admins and owners can add members directly. This is by design: a person can't be in a project without being in its organization. The auto-added member gets the minimum role and can't manage the organization. @@ -82,3 +82,21 @@ Organizations and projects have separate permission systems: Being an organization admin doesn't automatically give you permissions inside projects. You still need to be added to each project and assigned a project role. See [Roles and Permissions](/docs/collaboration/roles-and-permissions) for project-level access control. The member list API endpoint is restricted to owners and admins. Regular members cannot fetch the list of organization members. + +## Who can see which projects + +| Who | Sees | +|-----|------| +| **Organization owner** | **Every project of the organization**, including projects created by other members | +| **Organization admin** | Only the projects they were added to | +| **Organization member** | Only the projects they were added to | + +Projects created inside an organization belong to the organization, not to the person who created them: the organization owner is recorded as their owner. That is what keeps a project reachable when the person who created it leaves the company - nothing is lost with them. The flip side is that the owner sees every project of their organization, whoever created it. + +Everyone else - admins included - gets access to a project only by being added to it and given a project role. Being an organization admin means administering the organization (members, settings, SSO), not its content. + +::callout{icon="i-lucide-shield-alert" color="warning"} +**Access is granted explicitly, never inherited from a title.** There is deliberately no "admins can see all projects" switch: if a project should be visible to someone, they get invited to it. This keeps sensitive projects - finance, HR, salaries - private by default instead of silently opening them the moment someone is promoted to admin. + +The one exception is the organization owner, who sees everything by design (see above). If a project must stay private from the owner too, keep it in your **personal workspace** rather than in the organization. +:: diff --git a/docs/5.collaboration/1.members.md b/docs/5.collaboration/1.members.md index 873d34c..a6a6800 100644 --- a/docs/5.collaboration/1.members.md +++ b/docs/5.collaboration/1.members.md @@ -13,9 +13,9 @@ TaskView is built for teams. You can invite people to your projects, assign them 2. Enter the person's email address in the input field 3. Click **Add** -The person needs to have a TaskView account with that email. If they don't have one yet, they'll need to register first (using the same email you invited them with). +The person doesn't need a TaskView account yet - the invite is stored against the email address. If they already have an account, the project appears in their sidebar right away. If they don't, they simply register with that same email and find the project waiting for them. -Once added, they'll see the project in their sidebar and can start working immediately. +Inviting someone into a project also adds them to the project's organization as a **member**, since a person can't be in a project without being in its organization. ## Removing members diff --git a/docs/6.faq/1.index.md b/docs/6.faq/1.index.md index 5d9917e..dbc4392 100644 --- a/docs/6.faq/1.index.md +++ b/docs/6.faq/1.index.md @@ -1,6 +1,6 @@ --- title: Frequently Asked Questions -description: Common questions about TaskView - self-hosted open-source task and project management. Installation, features, security, Docker deployment, team collaboration, and more. +description: Common questions about TaskView - self-hosted source-available task and project management. Installation, features, security, Docker deployment, team collaboration, and more. navigation: icon: i-lucide-circle-help --- @@ -77,7 +77,7 @@ Yes. You can attach a monetary amount to any task and mark it as income or expen ### How do I invite team members? -Open a project, go to the Collaboration tab, and enter the person's email address. They need to have a TaskView account with that email. See [Team Members](/docs/collaboration/members). +Open a project, go to the Collaboration tab, and enter the person's email address. They don't need a TaskView account yet - if they register later with that same email, the project is already there. See [Team Members](/docs/collaboration/members). ### Does TaskView have role-based access control? diff --git a/taskview-packages/taskview-api/src/api/__tests__/collaboration-roles.access.test.ts b/taskview-packages/taskview-api/src/api/__tests__/collaboration-roles.access.test.ts new file mode 100644 index 0000000..981f69a --- /dev/null +++ b/taskview-packages/taskview-api/src/api/__tests__/collaboration-roles.access.test.ts @@ -0,0 +1,109 @@ +import { TvApi } from '@/tv' +import { TvPermissions } from '@/api/permissions' +import { describe, it, expect, beforeAll, afterAll } from 'vitest' +import { initApi } from './init-api' + +describe('Collaboration roles access control', () => { + let user1Api: TvApi + let user2Api: TvApi + let user2Email: string + let deleteAllGoals: () => Promise + let manageUsersPermissionId: number + + beforeAll(async () => { + const init = await initApi() + user1Api = init.$tvApi + user2Api = init.$tvApiForSecondUser + user2Email = init.user2Email + deleteAllGoals = init.deleteAllGoals + + const allPermissions = await user1Api.collaboration.fetchAllPermissions() + const found = allPermissions.find(p => p.name === TvPermissions.GOAL_CAN_MANAGE_USERS) + if (!found) throw new Error('Permission "goal_can_manage_users" is not in DB') + manageUsersPermissionId = found.id + }) + + afterAll(async () => { + await deleteAllGoals() + }) + + async function expectHttpStatus(promise: Promise, status: number): Promise { + try { + await promise + throw new Error(`Expected HTTP ${status} but request succeeded`) + } catch (e: any) { + if (typeof e.message === 'string' && e.message.startsWith('Expected HTTP')) throw e + expect(e.response?.status, `Expected ${status}, got ${e.response?.status}`).toBe(status) + } + } + + async function createGoalWithUser2(grantManageUsers: boolean) { + const goal = await user1Api.goals.createGoal({ name: `Roles access ${Date.now()}` }) + if (!goal) throw new Error('Failed to create goal') + + const collab = await user1Api.collaboration.inviteUserToGoal({ email: user2Email, goalId: goal.id }) + if (!collab) throw new Error('Failed to invite user2') + + const role = await user1Api.collaboration.createRoleForGoal({ + goalId: goal.id, + roleName: `Manager ${Date.now()}`, + }) + if (!role) throw new Error('Failed to create role') + + if (grantManageUsers) { + const toggled = await user1Api.collaboration.toggleRolePermission({ + roleId: role.id, + permissionId: manageUsersPermissionId, + }) + if (!toggled || toggled.add !== true) { + throw new Error(`Expected goal_can_manage_users to be added, got ${JSON.stringify(toggled)}`) + } + } + + await user1Api.collaboration.toggleUserRoles({ + goalId: goal.id, + userId: collab.id, + roles: [role.id], + }) + + return { goal, role } + } + + it('collaborator with goal_can_manage_users can read the role-to-permission matrix', async () => { + const { goal, role } = await createGoalWithUser2(true) + + const matrix = await user2Api.collaboration.fetchRoleToPermissionsForGoal(goal.id) + expect(matrix).toBeDefined() + // the granted permission is visible in the matrix of the role user2 holds + expect(matrix?.some(row => row.roleId === role.id && row.permissionId === manageUsersPermissionId)).toBe(true) + }) + + it('collaborator without goal_can_manage_users cannot read the matrix', async () => { + const { goal } = await createGoalWithUser2(false) + + await expectHttpStatus(user2Api.collaboration.fetchRoleToPermissionsForGoal(goal.id), 403) + }) + + it('changing role permissions stays owner-only', async () => { + const { goal, role } = await createGoalWithUser2(true) + + // reading is allowed... + const matrix = await user2Api.collaboration.fetchRoleToPermissionsForGoal(goal.id) + expect(matrix).toBeDefined() + + // ...but editing the matrix is not + await expectHttpStatus( + user2Api.collaboration.toggleRolePermission({ roleId: role.id, permissionId: manageUsersPermissionId }), + 403, + ) + + // owner still can edit + const ownerToggle = await user1Api.collaboration.toggleRolePermission({ + roleId: role.id, + permissionId: manageUsersPermissionId, + }) + expect(ownerToggle?.add).toBe(false) + + await expect(user1Api.goals.deleteGoal(goal.id)).resolves.toBeTruthy() + }) +}) diff --git a/taskview-packages/taskview-api/src/api/__tests__/collaboration.test.ts b/taskview-packages/taskview-api/src/api/__tests__/collaboration.test.ts index a6e78ea..b092a88 100644 --- a/taskview-packages/taskview-api/src/api/__tests__/collaboration.test.ts +++ b/taskview-packages/taskview-api/src/api/__tests__/collaboration.test.ts @@ -308,6 +308,127 @@ describe('Collaboration', () => { }); + // Regression for #98, the reporter's exact path: an org admin who is a collaborator + // in several projects created a new project in that org and lost every role he had. + // The trigger is creating a goal in an organization owned by SOMEONE ELSE - only then + // does the API add the creator as a collaborator, which used to wipe his other roles. + it('an org member creating a project in that org keeps his roles in other projects', async () => { + const { $tvApiForSecondUser, user2Email } = await initApi(); + + const org = await $api.organizations.create({ name: `Roles org ${Date.now()}` }); + expect(org?.id).toBeDefined(); + + // the reporter's case: the second user is an ADMIN of the organization + await $api.organizations.addMember({ + organizationId: org.id, + email: user2Email, + role: 'admin', + }); + + // owner creates two projects in the org and gives the admin roles in both + const projectA = await $api.goals.createGoal({ name: 'Org project A', organizationId: org.id }); + const projectB = await $api.goals.createGoal({ name: 'Org project B', organizationId: org.id }); + expect(projectA?.id).toBeDefined(); + expect(projectB?.id).toBeDefined(); + + const collabA = await $api.collaboration.inviteUserToGoal({ goalId: projectA!.id, email: user2Email }); + const collabB = await $api.collaboration.inviteUserToGoal({ goalId: projectB!.id, email: user2Email }); + expect(collabA?.id).toBeDefined(); + expect(collabB?.id).toEqual(collabA?.id); + + const editorA = (await $api.collaboration.fetchRolesForGoal(projectA!.id))?.find((r) => r.name === 'editor'); + const editorB = (await $api.collaboration.fetchRolesForGoal(projectB!.id))?.find((r) => r.name === 'editor'); + expect(editorA?.id).toBeDefined(); + expect(editorB?.id).toBeDefined(); + + await $api.collaboration.toggleUserRoles({ + goalId: projectA!.id, + userId: collabA!.id, + roles: [editorA!.id], + }); + await $api.collaboration.toggleUserRoles({ + goalId: projectB!.id, + userId: collabA!.id, + roles: [editorB!.id], + }); + + // ...the admin now creates his own project INSIDE the owner's organization + const ownProject = await $tvApiForSecondUser.goals.createGoal({ + name: 'Project created by the org admin', + organizationId: org.id, + }); + expect(ownProject?.id).toBeDefined(); + + // roles in the pre-existing projects must survive + const usersA = await $api.collaboration.fetchUsersForGoal(projectA!.id); + const usersB = await $api.collaboration.fetchUsersForGoal(projectB!.id); + expect(usersA?.find((u) => u.email === user2Email)?.roles).toEqual([editorA!.id]); + expect(usersB?.find((u) => u.email === user2Email)?.roles).toEqual([editorB!.id]); + + // and the new project is still usable by its creator + const ownGoals = await $tvApiForSecondUser.goals.fetchGoals(org.id); + expect(ownGoals?.some((g) => g.id === ownProject!.id)).toBe(true); + + await $api.organizations.delete(org.id).catch(() => { }); + }); + + // Regression for #98: toggling roles in one goal wiped the user's roles in every other goal + it('toggling roles in one goal must not touch the same user roles in another goal', async () => { + const email = `multi-goal-${Date.now()}@fff.com`; + + const goalA = await $api.goals.createGoal({ name: 'Roles isolation goal A' }); + const goalB = await $api.goals.createGoal({ name: 'Roles isolation goal B' }); + expect(goalA).toBeTruthy(); + expect(goalB).toBeTruthy(); + + const userInA = await $api.collaboration.inviteUserToGoal({ goalId: goalA!.id, email }); + const userInB = await $api.collaboration.inviteUserToGoal({ goalId: goalB!.id, email }); + expect(userInA?.id).toBeDefined(); + // the same collaboration user is shared between goals + expect(userInB?.id).toEqual(userInA?.id); + + const rolesA = await $api.collaboration.fetchRolesForGoal(goalA!.id); + const rolesB = await $api.collaboration.fetchRolesForGoal(goalB!.id); + const editorA = rolesA?.find((r) => r.name === 'editor'); + const editorB = rolesB?.find((r) => r.name === 'editor'); + expect(editorA?.id).toBeDefined(); + expect(editorB?.id).toBeDefined(); + + // assign a role in goal B first + const toggledB = await $api.collaboration.toggleUserRoles({ + goalId: goalB!.id, + userId: userInA?.id!, + roles: [editorB?.id!], + }); + expect(toggledB).toEqual([editorB?.id!]); + + // toggling roles in goal A must not clear the role in goal B + const toggledA = await $api.collaboration.toggleUserRoles({ + goalId: goalA!.id, + userId: userInA?.id!, + roles: [editorA?.id!], + }); + expect(toggledA).toEqual([editorA?.id!]); + + const usersInB = await $api.collaboration.fetchUsersForGoal(goalB!.id); + expect(usersInB?.find((u) => u.email === email)?.roles).toEqual([editorB?.id!]); + + // each goal's collaborator list shows only that goal's roles + const usersInA = await $api.collaboration.fetchUsersForGoal(goalA!.id); + expect(usersInA?.find((u) => u.email === email)?.roles).toEqual([editorA?.id!]); + + // a role id belonging to another goal must not be assignable through this goal + const toggledForeign = await $api.collaboration.toggleUserRoles({ + goalId: goalA!.id, + userId: userInA?.id!, + roles: [editorB?.id!], + }).catch(() => null); + expect(toggledForeign ?? []).toEqual([]); + + const usersInB2 = await $api.collaboration.fetchUsersForGoal(goalB!.id); + expect(usersInB2?.find((u) => u.email === email)?.roles).toEqual([editorB?.id!]); + }); + it('should handle inviting already existing collaborator', async () => { const addResult1 = await $api.collaboration.inviteUserToGoal({ goalId: collaborationGoal?.id!, From 784652ef5ba39a313940744715cc7814b69de249 Mon Sep 17 00:00:00 2001 From: Nikolai Giman Date: Tue, 4 Aug 2026 20:44:27 +0200 Subject: [PATCH 2/2] chore: version --- api/package.json | 2 +- package.json | 2 +- taskview-packages/taskview-mcp/package.json | 4 ++-- web/package.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api/package.json b/api/package.json index 7f2c6f7..67108c7 100644 --- a/api/package.json +++ b/api/package.json @@ -1,6 +1,6 @@ { "name": "taskview-ce-api-server", - "version": "1.50.4", + "version": "1.51.0", "scripts": { "dev": "bun run --watch ./server.ts", "start": "NODE_ENV=production node ./dist/taskview-server.js", diff --git a/package.json b/package.json index 1765eb4..6e6b0e4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "taskview-ce-monorepo", - "version": "1.50.4", + "version": "1.51.0", "private": true, "description": "TaskView CE monorepo containing web, API, and packages", "workspaces": [ diff --git a/taskview-packages/taskview-mcp/package.json b/taskview-packages/taskview-mcp/package.json index 8efef67..ea0eb8a 100644 --- a/taskview-packages/taskview-mcp/package.json +++ b/taskview-packages/taskview-mcp/package.json @@ -1,6 +1,6 @@ { "name": "taskview-mcp", - "version": "1.48.3", + "version": "1.51.0", "description": "MCP (Model Context Protocol) server for TaskView — lets AI assistants (Claude Code, Claude Desktop, etc.) manage projects and tasks via the TaskView API", "type": "module", "bin": { @@ -62,4 +62,4 @@ "vitest": "^4.1.2", "zod": "^3.23.8" } -} \ No newline at end of file +} diff --git a/web/package.json b/web/package.json index 7fbdbe5..2755a2e 100644 --- a/web/package.json +++ b/web/package.json @@ -2,7 +2,7 @@ "name": "web-nuxt-ui", "private": true, "type": "module", - "version": "1.50.10", + "version": "1.51.0", "scripts": { "dev": "vite", "build:packages": "pnpm --filter taskview-db-schemas build && pnpm --filter taskview-api build && pnpm --filter capacitor-widget-bridge build",