Skip to content
Closed
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
9 changes: 9 additions & 0 deletions convex/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,9 @@ export const getMachineStats = query({
region: machine.region ?? null,
city: machine.city ?? null,
screen: machine.screen ?? null,
userEmail: machine.userEmail ?? null,
userName: machine.userName ?? null,
authProvider: machine.authProvider ?? null,
Comment on lines +283 to +285

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Gate the new PII before returning it.

getMachineStats, getPageVisitors, and searchMachines now return email/name/provider, but the supplied dashboard handlers are public querys without an auth or tenant check. Anyone able to call these endpoints can retrieve user PII; add authorization and tenant scoping, or keep identity fields behind authenticated endpoints. (raw.githubusercontent.com)

Also applies to: 387-389, 442-444

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@convex/dashboard.ts` around lines 283 - 285, Restrict the new userEmail,
userName, and authProvider fields returned by getMachineStats, getPageVisitors,
and searchMachines to authenticated, tenant-scoped callers. Add the existing
authorization and tenant checks to these public query handlers, or remove/gate
the identity fields so unauthenticated callers cannot retrieve PII.

},
stats: {
totalSessions,
Expand Down Expand Up @@ -381,6 +384,9 @@ export const getPageVisitors = query({
country: machine?.country ?? null,
platform: machine?.platform ?? null,
userAgent: machine?.userAgent ?? null,
userEmail: machine?.userEmail ?? null,
userName: machine?.userName ?? null,
authProvider: machine?.authProvider ?? null,
};
})
);
Expand Down Expand Up @@ -433,6 +439,9 @@ export const searchMachines = query({
country: m.country ?? null,
platform: m.platform ?? null,
lastSeenAt: m.lastSeenAt,
userEmail: m.userEmail ?? null,
userName: m.userName ?? null,
authProvider: m.authProvider ?? null,
}));
},
});
Expand Down
25 changes: 23 additions & 2 deletions convex/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ async function upsertMachine(
timestamp: number;
meta?: Record<string, unknown>;
geo?: { ip?: string; country?: string; region?: string; city?: string };
userInfo?: { email?: string; name?: string; provider?: string };
}
) {
const existing = await ctx.db
Expand All @@ -51,6 +52,9 @@ async function upsertMachine(
country: args.geo?.country,
region: args.geo?.region,
city: args.geo?.city,
userEmail: args.userInfo?.email,
userName: args.userInfo?.name,
authProvider: args.userInfo?.provider,
});
return { isNewMachine: true };
}
Expand All @@ -62,6 +66,9 @@ async function upsertMachine(
platform: args.meta?.platform ?? existing.platform,
referrer: args.meta?.referrer ?? existing.referrer,
screen: args.meta?.screen ?? existing.screen,
userEmail: args.userInfo?.email ?? existing.userEmail,
userName: args.userInfo?.name ?? existing.userName,
authProvider: args.userInfo?.provider ?? existing.authProvider,
});
return { isNewMachine: false };
}
Expand Down Expand Up @@ -122,20 +129,32 @@ async function upsertSession(
}
}

function extractUserInfo(events: Array<{ name: string; payload?: unknown }>): { email?: string; name?: string; provider?: string } | undefined {
const identifyEvent = events.find((e) => e.name === "session_identify");
if (!identifyEvent?.payload || typeof identifyEvent.payload !== "object") return undefined;
const p = identifyEvent.payload as Record<string, unknown>;
const email = typeof p.email === "string" ? p.email : undefined;
const name = typeof p.name === "string" ? p.name : undefined;
const provider = typeof p.provider === "string" ? p.provider : undefined;
return email || name || provider ? { email, name, provider } : undefined;
}

export const recordBatch = mutation({
args: { events: v.array(eventValidator) },
handler: async (ctx, { events }) => {
// Process oldest-first so ordering-dependent bookkeeping (session creation, counters) is correct.
const sorted = [...events].sort((a, b) => a.timestamp - b.timestamp);
const userInfo = extractUserInfo(sorted);

for (const event of sorted) {
const isBookkeeping = event.name === "session_start";
const isBookkeeping = event.name === "session_start" || event.name === "session_identify";

await upsertMachine(ctx, {
machineId: event.machineId,
userId: event.userId,
timestamp: event.timestamp,
meta: isBookkeeping ? event.payload : undefined,
userInfo,
Comment on lines +132 to +157

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Scope identity extraction to the machine/session being processed.

extractUserInfo(sorted) finds one session_identify event across the entire batch, then both handlers pass that object to every upsertMachine. A batch containing multiple machines or identity changes will assign the first user’s PII to unrelated machines and ignore later identities. Maintain identity state per target while processing events, and add multi-machine/multi-identity tests.

Also applies to: 233-244

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@convex/events.ts` around lines 132 - 157, The recordBatch and corresponding
event-processing handler must scope extracted identity data to each
machine/session instead of reusing one batch-wide extractUserInfo result. Track
identity state per target while iterating events, update it for each
session_identify event, and pass only that target’s current identity to
upsertMachine; add tests covering multiple machines and identity changes within
a batch.

});

await upsertSession(ctx, {
Expand Down Expand Up @@ -211,16 +230,18 @@ export const recordBatchWithGeo = internalMutation({
},
handler: async (ctx, { events, geo }) => {
const sorted = [...events].sort((a, b) => a.timestamp - b.timestamp);
const userInfo = extractUserInfo(sorted);

for (const event of sorted) {
const isBookkeeping = event.name === "session_start";
const isBookkeeping = event.name === "session_start" || event.name === "session_identify";

await upsertMachine(ctx, {
machineId: event.machineId,
userId: event.userId,
timestamp: event.timestamp,
meta: isBookkeeping ? event.payload : undefined,
geo,
userInfo,
});

await upsertSession(ctx, {
Expand Down
3 changes: 3 additions & 0 deletions convex/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export default defineSchema({
region: v.optional(v.string()),
city: v.optional(v.string()),
screen: v.optional(v.string()),
userEmail: v.optional(v.string()),
userName: v.optional(v.string()),
authProvider: v.optional(v.string()),
})
.index("by_machineId", ["machineId"])
.index("by_userId", ["userId"])
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"@radix-ui/react-use-layout-effect": "^1.1.1",
"@radix-ui/react-use-previous": "^1.1.1",
"@radix-ui/react-visually-hidden": "^1.2.4",
"@renderdragonorg/wisp": "^0.1.1",
"@renderdragonorg/wisp": "^0.2.0",
"@sentry/react": "^10.38.0",
"@supabase/supabase-js": "^2.95.3",
"@tabler/icons-react": "^3.36.1",
Expand Down
Loading
Loading