Skip to content

Commit 9e8588a

Browse files
authored
Personal accounts (#118)
1 parent 6f3db07 commit 9e8588a

File tree

21 files changed

+440
-193
lines changed

21 files changed

+440
-193
lines changed

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,5 @@ Thumbs.db
2020
.vercel
2121

2222
# docker
23-
.docker/data
24-
.docker/logs
23+
**/.docker/data
24+
**/.docker/logs

apps/web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"@radix-ui/react-toast": "^1.2.14",
3333
"@radix-ui/react-tooltip": "^1.2.7",
3434
"@roo-code-cloud/db": "workspace:^",
35-
"@roo-code/types": "^1.26.0",
35+
"@roo-code/types": "^1.28.0",
3636
"@sentry/nextjs": "^9.23.0",
3737
"@t3-oss/env-nextjs": "^0.13.6",
3838
"@tailwindcss/postcss": "^4.1.8",

apps/web/src/actions/agents.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ export async function createAgent(
2929

3030
const { userId, orgId, orgRole } = authResult;
3131

32+
// Agents are only available for organizations, not personal accounts
33+
if (!orgId || !orgRole) {
34+
return {
35+
success: false,
36+
error: 'Agents are only available for organization accounts.',
37+
};
38+
}
39+
3240
if (orgRole !== 'org:admin') {
3341
return {
3442
success: false,
@@ -105,6 +113,14 @@ export async function revokeAgent(
105113

106114
const { orgId, orgRole } = authResult;
107115

116+
// Agents are only available for organizations, not personal accounts
117+
if (!orgId || !orgRole) {
118+
return {
119+
success: false,
120+
error: 'Agents are only available for organization accounts.',
121+
};
122+
}
123+
108124
if (orgRole !== 'org:admin') {
109125
return {
110126
success: false,

apps/web/src/actions/analytics/events.ts

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -92,17 +92,24 @@ export const getUsage = async ({
9292
requestedUserId: userId,
9393
});
9494

95-
if (!orgId) {
96-
return {};
95+
// For personal accounts, query by userId instead of orgId
96+
if (!orgId && !effectiveUserId) {
97+
return {}; // Personal accounts must have a userId
9798
}
9899

99100
const userFilter = effectiveUserId ? 'AND userId = {userId: String}' : '';
100101

102+
// Build query conditions based on account type
103+
const orgCondition = !orgId ? 'orgId IS NULL' : 'orgId = {orgId: String}';
104+
101105
const queryParams: Record<string, string | number> = {
102-
orgId: orgId!,
103106
timePeriod,
104107
};
105108

109+
if (orgId) {
110+
queryParams.orgId = orgId;
111+
}
112+
106113
if (effectiveUserId) {
107114
queryParams.userId = effectiveUserId;
108115
}
@@ -117,7 +124,7 @@ export const getUsage = async ({
117124
SUM(COALESCE(cost, 0)) AS cost
118125
FROM events
119126
WHERE
120-
orgId = {orgId: String}
127+
${orgCondition}
121128
AND timestamp >= toUnixTimestamp(now() - INTERVAL {timePeriod: Int32} DAY)
122129
${userFilter}
123130
GROUP BY 1
@@ -332,8 +339,10 @@ export const getTasks = async ({
332339
effectiveUserId = authResult.effectiveUserId;
333340
}
334341

335-
if (!orgId) {
336-
return [];
342+
// For personal accounts, query by userId instead of orgId
343+
// Exception: when skipAuth is true (for public shares), we can query without userId
344+
if (!orgId && !effectiveUserId && !skipAuth) {
345+
return []; // Personal accounts must have a userId unless we're skipping auth
337346
}
338347

339348
const userFilter = effectiveUserId ? 'AND e.userId = {userId: String}' : '';
@@ -345,15 +354,25 @@ export const getTasks = async ({
345354

346355
const messageTaskFilter = taskId ? 'AND taskId = {taskId: String}' : '';
347356

357+
// Build query conditions based on account type
358+
const orgCondition = !orgId ? 'e.orgId IS NULL' : 'e.orgId = {orgId: String}';
359+
360+
const messageOrgCondition = !orgId
361+
? 'orgId IS NULL'
362+
: 'orgId = {orgId: String}';
363+
348364
const queryParams: Record<string, string | string[]> = {
349-
orgId: orgId!,
350365
types: [
351366
TelemetryEventName.TASK_CREATED,
352367
TelemetryEventName.TASK_COMPLETED,
353368
TelemetryEventName.LLM_COMPLETION,
354369
],
355370
};
356371

372+
if (orgId) {
373+
queryParams.orgId = orgId;
374+
}
375+
357376
if (effectiveUserId) {
358377
queryParams.userId = effectiveUserId;
359378
}
@@ -370,7 +389,7 @@ export const getTasks = async ({
370389
argMin(text, ts) as title,
371390
argMin(mode, ts) as mode
372391
FROM messages
373-
WHERE orgId = {orgId: String}
392+
WHERE ${messageOrgCondition}
374393
${messageUserFilter}
375394
${messageTaskFilter}
376395
GROUP BY taskId
@@ -389,7 +408,7 @@ export const getTasks = async ({
389408
FROM events e
390409
LEFT JOIN first_messages fm ON e.taskId = fm.taskId
391410
WHERE
392-
e.orgId = {orgId: String}
411+
${orgCondition}
393412
AND e.type IN ({types: Array(String)})
394413
AND e.modelId IS NOT NULL
395414
${userFilter}
@@ -440,14 +459,17 @@ export const getHourlyUsageByUser = async ({
440459
requestedUserId: userId,
441460
});
442461

443-
if (!orgId) {
444-
return [];
462+
// For personal accounts, query by userId instead of orgId
463+
if (!orgId && !effectiveUserId) {
464+
return []; // Personal accounts must have a userId
445465
}
446466

447467
const userFilter = effectiveUserId ? 'AND userId = {userId: String}' : '';
448468

469+
// Build query conditions based on account type
470+
const orgCondition = !orgId ? 'orgId IS NULL' : 'orgId = {orgId: String}';
471+
449472
const queryParams: Record<string, string | number | string[]> = {
450-
orgId: orgId!,
451473
timePeriod,
452474
types: [
453475
TelemetryEventName.TASK_CREATED,
@@ -456,6 +478,10 @@ export const getHourlyUsageByUser = async ({
456478
],
457479
};
458480

481+
if (orgId) {
482+
queryParams.orgId = orgId;
483+
}
484+
459485
if (effectiveUserId) {
460486
queryParams.userId = effectiveUserId;
461487
}
@@ -470,7 +496,7 @@ export const getHourlyUsageByUser = async ({
470496
SUM(CASE WHEN type = '${TelemetryEventName.LLM_COMPLETION}' THEN COALESCE(cost, 0) ELSE 0 END) AS cost
471497
FROM events
472498
WHERE
473-
orgId = {orgId: String}
499+
${orgCondition}
474500
AND timestamp >= toUnixTimestamp(now() - INTERVAL {timePeriod: Int32} DAY)
475501
AND type IN ({types: Array(String)})
476502
${userFilter}

apps/web/src/actions/analytics/messages.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { z } from 'zod';
44

55
import { analytics } from '@/lib/server';
6+
import { authorizeAnalytics } from '@/actions/auth';
67

78
/**
89
* getMessages
@@ -26,17 +27,44 @@ const messageSchema = z.object({
2627

2728
export type Message = z.infer<typeof messageSchema>;
2829

29-
export const getMessages = async (taskId: string): Promise<Message[]> => {
30+
export const getMessages = async (
31+
taskId: string,
32+
orgId?: string | null,
33+
userId?: string | null,
34+
skipAuth = false,
35+
): Promise<Message[]> => {
36+
// Authorize the request - this will handle both personal and org contexts
37+
// Skip auth for public shares viewed by unauthenticated users
38+
if (!skipAuth) {
39+
await authorizeAnalytics({
40+
requestedOrgId: orgId,
41+
requestedUserId: userId,
42+
allowCrossUserAccess: true, // Allow viewing messages within authorized tasks
43+
});
44+
}
45+
46+
// For personal accounts, query with orgId IS NULL
47+
// For organizations, query with specific orgId
48+
const orgCondition = !orgId ? 'orgId IS NULL' : 'orgId = {orgId: String}';
49+
50+
const queryParams: Record<string, string> = { taskId };
51+
if (orgId) {
52+
queryParams.orgId = orgId;
53+
}
54+
3055
const results = await analytics.query({
3156
query: `
3257
SELECT *
3358
FROM messages
3459
WHERE taskId = {taskId: String}
60+
AND ${orgCondition}
3561
ORDER BY ts ASC
3662
`,
3763
format: 'JSONEachRow',
38-
query_params: { taskId },
64+
query_params: queryParams,
3965
});
4066

41-
return z.array(messageSchema).parse(await results.json());
67+
const messages = z.array(messageSchema).parse(await results.json());
68+
69+
return messages;
4270
};

apps/web/src/actions/auth.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,15 @@ export async function authorize(): Promise<AuthResult> {
1818
return { success: false, error: 'Unauthorized: User required' };
1919
}
2020

21+
// Personal context is valid - no org required
2122
if (!orgId) {
22-
return { success: false, error: 'Unauthorized: Organization required' };
23+
return {
24+
success: true,
25+
userType: 'user',
26+
userId,
27+
orgId: null,
28+
orgRole: null,
29+
};
2330
}
2431

2532
return {
@@ -111,6 +118,27 @@ export async function authorizeAnalytics({
111118
}) {
112119
const { orgId: authOrgId, orgRole, userId: authUserId } = await auth();
113120

121+
if (!authUserId) {
122+
throw new Error('Unauthorized: User required');
123+
}
124+
125+
// Handle personal context
126+
if (!authOrgId && !requestedOrgId) {
127+
// Personal context - user can only access their own data
128+
if (requestedUserId && requestedUserId !== authUserId) {
129+
throw new Error(
130+
'Unauthorized: Personal users can only access their own data',
131+
);
132+
}
133+
134+
return {
135+
authOrgId: null,
136+
authUserId,
137+
orgRole: null,
138+
effectiveUserId: authUserId,
139+
};
140+
}
141+
114142
// Ensure user is authenticated and belongs to the organization
115143
if (!authOrgId || !authUserId || authOrgId !== requestedOrgId) {
116144
throw new Error('Unauthorized: Invalid organization access');

apps/web/src/actions/organizationSettings.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ export async function getOrganizationSettings(): Promise<OrganizationSettings> {
2424
throw new Error('Unauthorized');
2525
}
2626

27+
// Organization settings are only available for organizations, not personal accounts
28+
if (!authResult.orgId) {
29+
return ORGANIZATION_DEFAULT;
30+
}
31+
2732
const settings = await db
2833
.select()
2934
.from(orgSettings)
@@ -64,6 +69,13 @@ export async function updateOrganization(data: UpdateOrganizationRequest) {
6469

6570
const { userId, orgId } = authResult;
6671

72+
// Organization settings are only available for organizations, not personal accounts
73+
if (!orgId) {
74+
throw new Error(
75+
'Organization settings are only available for organization accounts',
76+
);
77+
}
78+
6779
const validatedData = updateOrganizationSchema.parse(data);
6880

6981
// Perform database update in a transaction

0 commit comments

Comments
 (0)