Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions src/app/teams/[teamId]/hooks/use-create-role.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -222,10 +222,6 @@ export function useCreateRole({
});
setNodes(updatedNodes);
}

// Invalidate caches for background refresh
void utils.team.getById.invalidate({ id: teamId });
void utils.role.getByTeamId.invalidate({ teamId });
},
onError: (error, _variables, context) => {
if (context?.previousRoles !== undefined) {
Expand Down
5 changes: 0 additions & 5 deletions src/app/teams/[teamId]/hooks/use-delete-role.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,5 @@ export function useDeleteRole(teamId: string) {
setEdges?.(context.previousEdges);
}
},
onSettled: () => {
void utils.role.getByTeamId.invalidate({ teamId });
// Invalidate team.getById cache to ensure fresh data on next fetch
void utils.team.getById.invalidate({ id: teamId });
},
});
}
7 changes: 0 additions & 7 deletions src/components/role/role-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,6 @@ function RoleCardComponent({
utils.role.getByTeamId.setData({ teamId }, context.previousRoles);
}
},
onSettled: () => {
if (teamId) {
void utils.role.getByTeamId.invalidate({ teamId });
void utils.team.getById.invalidate({ id: teamId });
void utils.dashboard.getDashboardCharts.invalidate({ teamId });
}
},
});

const handleDelete = useCallback(async () => {
Expand Down
7 changes: 0 additions & 7 deletions src/hooks/use-optimistic-role-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,13 +218,6 @@ export function useOptimisticRoleUpdate(teamId: string) {
return chart;
});
});

// Delayed invalidation for eventual consistency
// Wait for Prisma Accelerate cache propagation before background refresh
setTimeout(() => {
void utils.role.getByTeamId.invalidate({ teamId });
void utils.dashboard.getDashboardCharts.invalidate({ teamId });
}, 5000);
},

onError: (error, _vars, context) => {
Expand Down
8 changes: 6 additions & 2 deletions src/server/api/routers/metric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import {
getMetricAndVerifyAccess,
getTeamAndVerifyAccess,
} from "@/server/api/utils/authorization";
import { invalidateDashboardCache } from "@/server/api/utils/cache-strategy";
import {
invalidateCacheByTags,
invalidateDashboardCache,
} from "@/server/api/utils/cache-strategy";
Comment on lines +12 to +15
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the metric.ts file to see the imports and delete mutation
head -30 src/server/api/routers/metric.ts

Repository: drifter089/orgOS

Length of output: 1140


🏁 Script executed:

# Check the delete mutation around lines 273-280
sed -n '260,290p' src/server/api/routers/metric.ts

Repository: drifter089/orgOS

Length of output: 988


🏁 Script executed:

# Look for the metric create input schema to check teamId nullability
rg -A 20 "z\.object.*metric" src/server/api/routers/metric.ts | head -50

Repository: drifter089/orgOS

Length of output: 1229


🏁 Script executed:

# Search for cache tag patterns used in the codebase
rg "invalidateCacheByTags.*team_" src/server/api/ --context 2

Repository: drifter089/orgOS

Length of output: 1960


🏁 Script executed:

# Check the metric model/schema definition for teamId
fd "metric" src/server --type f -name "*.ts" | xargs grep -l "teamId.*z\." | head -5

Repository: drifter089/orgOS

Length of output: 290


🏁 Script executed:

# Get a larger window around the delete mutation to see if there's authz check
sed -n '250,285p' src/server/api/routers/metric.ts

Repository: drifter089/orgOS

Length of output: 1147


🏁 Script executed:

# Search for the full delete mutation definition
rg -B 10 -A 15 "\.delete.*workspaceProcedure" src/server/api/routers/metric.ts

Repository: drifter089/orgOS

Length of output: 42


🏁 Script executed:

# Check the metric schema for teamId optionality
rg -A 30 "export.*metric.*z\.object" src/server/db/schema.ts 2>/dev/null || rg -r "prisma" -A 5 "teamId" src/server/db/schema.prisma 2>/dev/null | head -20

Repository: drifter089/orgOS

Length of output: 42


🏁 Script executed:

# Look for metric create input schema
rg -B 5 -A 20 "input.*z\.object.*metricId.*z" src/server/api/routers/metric.ts

Repository: drifter089/orgOS

Length of output: 1396


🏁 Script executed:

# Check if org-level metrics (teamId=null) exist in schema
rg "teamId.*@db\.String" src/server/db/schema.prisma || rg "teamId.*String" src/server/db/schema.prisma

Repository: drifter089/orgOS

Length of output: 195


🏁 Script executed:

# Find Prisma schema file
find . -name "*.prisma" -type f

Repository: drifter089/orgOS

Length of output: 82


🏁 Script executed:

# Check if there's authorization verification for metrics elsewhere
rg "getMetricAndVerifyAccess" src/server/api/routers/metric.ts -B 5 -A 5

Repository: drifter089/orgOS

Length of output: 1019


🏁 Script executed:

# Look for update/create mutations in metric.ts to see if they use authz helpers
rg -B 5 -A 15 "create.*workspaceProcedure\|update.*workspaceProcedure" src/server/api/routers/metric.ts | head -60

Repository: drifter089/orgOS

Length of output: 42


🏁 Script executed:

# Check the Metric model definition in Prisma
rg -A 20 "^model Metric" prisma/schema.prisma 2>/dev/null || find . -path "*/prisma/*.prisma" -exec rg "model Metric" {} \;

Repository: drifter089/orgOS

Length of output: 3188


🏁 Script executed:

# Verify delete mutation is missing authz by comparing with update mutation
sed -n '230,280p' src/server/api/routers/metric.ts

Repository: drifter089/orgOS

Length of output: 1384


🏁 Script executed:

# Check how invalidateDashboardCache handles null teamId
rg -A 20 "function invalidateDashboardCache\|export.*invalidateDashboardCache" src/server/api/utils/cache-strategy.ts

Repository: drifter089/orgOS

Length of output: 42


🏁 Script executed:

# Check how invalidateCacheByTags handles the tags
rg -B 5 -A 25 "function invalidateCacheByTags\|export.*invalidateCacheByTags" src/server/api/utils/cache-strategy.ts

Repository: drifter089/orgOS

Length of output: 42


🏁 Script executed:

# Verify workspaceProcedure adds organization-level filtering
rg -A 10 "workspaceProcedure" src/server/api/trpc.ts | head -30

Repository: drifter089/orgOS

Length of output: 190


🏁 Script executed:

# Search for cache-strategy file and its functions
cat src/server/api/utils/cache-strategy.ts

Repository: drifter089/orgOS

Length of output: 6332


🏁 Script executed:

# Check if there are any other delete mutations in routers to see the pattern
rg -B 5 -A 20 "\.delete.*workspaceProcedure" src/server/api/routers/ | head -100

Repository: drifter089/orgOS

Length of output: 42


🏁 Script executed:

# Verify the exact issue: delete doesn't verify org access like update does
rg -B 20 "delete.*workspaceProcedure" src/server/api/routers/metric.ts | grep -E "getMetricAndVerifyAccess|workspaceProcedure|delete:"

Repository: drifter089/orgOS

Length of output: 143


Add authorization check and guard cache tag invalidation in delete mutation.

The delete mutation is missing the getMetricAndVerifyAccess check that the update mutation uses. This allows any authenticated workspace user to delete any metric in the organization, regardless of team access. Additionally, when teamId is null (org-level metrics), the cache tag becomes team_null, wasting rate limits.

Suggested fix
  delete: workspaceProcedure
    .input(z.object({ id: z.string() }))
    .mutation(async ({ ctx, input }) => {
-     // Get metric info before deletion for cache invalidation
-     const metric = await ctx.db.metric.findUnique({
-       where: { id: input.id },
-       select: { teamId: true, organizationId: true },
-     });
-
-     if (!metric) {
-       throw new TRPCError({
-         code: "NOT_FOUND",
-         message: "Metric not found",
-       });
-     }
+     // Verify metric belongs to user's organization
+     const metric = await getMetricAndVerifyAccess(
+       ctx.db,
+       input.id,
+       ctx.workspace.organizationId,
+     );

      // Roles with this metric will have metricId set to null (onDelete: SetNull)
      await ctx.db.metric.delete({
        where: { id: input.id },
      });

      // Invalidate Prisma cache for dashboard and role queries
      await invalidateDashboardCache(
        ctx.db,
        metric.organizationId,
        metric.teamId,
      );
-     await invalidateCacheByTags(ctx.db, [`team_${metric.teamId}`]);
+     if (metric.teamId) {
+       await invalidateCacheByTags(ctx.db, [`team_${metric.teamId}`]);
+     }

      return { success: true };
    }),
🤖 Prompt for AI Agents
In @src/server/api/routers/metric.ts around lines 12 - 15, The delete mutation
in src/server/api/routers/metric.ts must perform the same authorization as the
update path: call getMetricAndVerifyAccess(...) to verify the current user/team
can delete the metric before proceeding (mirror the update mutation's check and
error handling). After successful deletion, change the cache invalidation so it
doesn't emit a tag like team_null: only include the team tag when metric.teamId
is non-null (e.g., push/invalidate `team_${teamId}` only if teamId exists) and
always include org-level tags as done elsewhere; update the calls to
invalidateCacheByTags and invalidateDashboardCache accordingly so org-level
metrics do not generate a team_null tag.


import { runBackgroundTask } from "./pipeline";

Expand Down Expand Up @@ -267,12 +270,13 @@ export const metricRouter = createTRPCRouter({
where: { id: input.id },
});

// Invalidate Prisma cache for dashboard queries
// Invalidate Prisma cache for dashboard and role queries
await invalidateDashboardCache(
ctx.db,
metric.organizationId,
metric.teamId,
);
await invalidateCacheByTags(ctx.db, [`team_${metric.teamId}`]);

return { success: true };
}),
Expand Down