Skip to content

Commit 1ab7822

Browse files
mrubenscte
andauthored
Add options to change share visibility (#108)
Co-authored-by: cte <[email protected]>
1 parent 008c548 commit 1ab7822

File tree

11 files changed

+1239
-80
lines changed

11 files changed

+1239
-80
lines changed

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

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -309,17 +309,24 @@ export const getTasks = async ({
309309
userId,
310310
taskId,
311311
allowCrossUserAccess = false,
312+
skipAuth = false,
312313
}: {
313314
orgId?: string | null;
314315
userId?: string | null;
315316
taskId?: string | null;
316317
allowCrossUserAccess?: boolean;
318+
skipAuth?: boolean;
317319
}): Promise<TaskWithUser[]> => {
318-
const { effectiveUserId } = await authorizeAnalytics({
319-
requestedOrgId: orgId,
320-
requestedUserId: userId,
321-
allowCrossUserAccess,
322-
});
320+
let effectiveUserId = userId;
321+
322+
if (!skipAuth) {
323+
const authResult = await authorizeAnalytics({
324+
requestedOrgId: orgId,
325+
requestedUserId: userId,
326+
allowCrossUserAccess,
327+
});
328+
effectiveUserId = authResult.effectiveUserId;
329+
}
323330

324331
if (!orgId) {
325332
return [];

apps/web/src/actions/taskSharing.ts

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import {
77
createTaskShareSchema,
88
shareIdSchema,
99
} from '@/types';
10-
import type { SharedByUser } from '@/types';
10+
import type { SharedByUser } from '@/types/task-sharing';
11+
import { TaskShareVisibility } from '@/types/task-sharing';
1112
import { type TaskShare, AuditLogTargetType } from '@/db';
1213
import { client as db, taskShares, users } from '@/db/server';
1314
import { handleError, generateShareToken } from '@/lib/server';
@@ -41,6 +42,7 @@ export async function canShareTask(taskId: string): Promise<{
4142
orgRole?: string;
4243
}> {
4344
try {
45+
// Get authentication info
4446
const authResult = await authorize();
4547

4648
if (!authResult.success) {
@@ -56,6 +58,7 @@ export async function canShareTask(taskId: string): Promise<{
5658
orgId,
5759
allowCrossUserAccess: true,
5860
});
61+
5962
const task = tasks[0];
6063

6164
if (!task) {
@@ -100,7 +103,11 @@ export async function createTaskShare(data: CreateTaskShareRequest) {
100103
return { success: false, error: 'Invalid request data' };
101104
}
102105

103-
const { taskId, expirationDays } = result.data;
106+
const {
107+
taskId,
108+
expirationDays,
109+
visibility = TaskShareVisibility.ORGANIZATION,
110+
} = result.data;
104111

105112
const orgSettingsData = await getOrganizationSettings();
106113

@@ -142,6 +149,7 @@ export async function createTaskShare(data: CreateTaskShareRequest) {
142149
orgId,
143150
createdByUserId: userId,
144151
shareToken,
152+
visibility,
145153
expiresAt,
146154
})
147155
.returning();
@@ -158,11 +166,12 @@ export async function createTaskShare(data: CreateTaskShareRequest) {
158166
newValue: {
159167
action: 'created',
160168
shareId: insertedShare[0].id,
169+
visibility,
161170
expiresAt: expiresAt.toISOString(),
162171
taskOwnerId: task.userId,
163172
sharedByAdmin: orgRole === 'org:admin' && task.userId !== userId,
164173
},
165-
description: `Created task share for task ${taskId}${
174+
description: `Created ${visibility} task share for task ${taskId}${
166175
orgRole === 'org:admin' && task.userId !== userId
167176
? ` (admin sharing task created by ${task.user.name})`
168177
: ''
@@ -182,8 +191,8 @@ export async function createTaskShare(data: CreateTaskShareRequest) {
182191

183192
return {
184193
success: true,
185-
message: 'Task share created successfully',
186194
data: { shareUrl, shareId: newShare.id, expiresAt },
195+
message: 'Task share created successfully',
187196
};
188197
} catch (error) {
189198
return handleError(error, 'task_sharing');
@@ -198,18 +207,14 @@ export async function getTaskByShareToken(token: string): Promise<{
198207
messages: Message[];
199208
sharedBy: SharedByUser;
200209
sharedAt: Date;
210+
visibility: string;
201211
} | null> {
202212
try {
203-
const authResult = await authorize();
204-
205-
if (!authResult.success) {
206-
throw new Error('Authentication required');
207-
}
208-
209213
if (!isValidShareToken(token)) {
210214
return null;
211215
}
212216

217+
// First, get the share without auth check to determine visibility
213218
const [shareWithUser] = await db
214219
.select({
215220
share: taskShares,
@@ -221,12 +226,7 @@ export async function getTaskByShareToken(token: string): Promise<{
221226
})
222227
.from(taskShares)
223228
.innerJoin(users, eq(taskShares.createdByUserId, users.id))
224-
.where(
225-
and(
226-
eq(taskShares.shareToken, token),
227-
eq(taskShares.orgId, authResult.orgId),
228-
),
229-
)
229+
.where(eq(taskShares.shareToken, token))
230230
.limit(1);
231231

232232
if (!shareWithUser) {
@@ -239,12 +239,25 @@ export async function getTaskByShareToken(token: string): Promise<{
239239
return null;
240240
}
241241

242+
// Check visibility and auth requirements
243+
if (share.visibility === TaskShareVisibility.ORGANIZATION) {
244+
const authResult = await authorize();
245+
const userId = authResult.success ? authResult.userId : null;
246+
const orgId = authResult.success ? authResult.orgId : null;
247+
248+
if (!userId || !orgId || orgId !== share.orgId) {
249+
throw new Error('Authentication required for organization shares');
250+
}
251+
}
252+
// For public shares, no auth check needed
253+
254+
// Get task data based on visibility
242255
const tasks = await getTasks({
243256
taskId: share.taskId,
244257
orgId: share.orgId,
245258
allowCrossUserAccess: true,
259+
skipAuth: share.visibility === TaskShareVisibility.PUBLIC, // Skip auth for public shares
246260
});
247-
248261
const task = tasks[0];
249262

250263
if (!task) {
@@ -258,6 +271,7 @@ export async function getTaskByShareToken(token: string): Promise<{
258271
messages,
259272
sharedBy: sharedByUser,
260273
sharedAt: share.createdAt,
274+
visibility: share.visibility,
261275
};
262276
} catch (error) {
263277
console.error(
@@ -281,6 +295,7 @@ export async function deleteTaskShare(shareId: string) {
281295
}
282296

283297
const { userId, orgId, orgRole } = authResult;
298+
284299
const shareIdResult = shareIdSchema.safeParse(shareId);
285300

286301
if (!shareIdResult.success) {

apps/web/src/app/api/extension/share/route.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
import { NextRequest, NextResponse } from 'next/server';
22
import { z } from 'zod';
33

4-
import { authorizeApi } from '@/actions/auth';
54
import { createTaskShare } from '@/actions/taskSharing';
65
import { getTasks } from '@/actions/analytics';
76
import { getOrganizationSettings } from '@/actions/organizationSettings';
7+
import { authorize } from '@/actions/auth';
8+
import { TaskShareVisibility } from '@/types/task-sharing';
89

910
const createShareRequestSchema = z.object({
1011
taskId: z.string().min(1, 'Task ID is required'),
12+
visibility: z
13+
.nativeEnum(TaskShareVisibility)
14+
.default(TaskShareVisibility.ORGANIZATION),
1115
});
1216

1317
export async function POST(request: NextRequest) {
1418
try {
15-
const authResult = await authorizeApi(request);
19+
const authResult = await authorize();
1620

1721
if (!authResult.success) {
1822
return NextResponse.json(
@@ -33,7 +37,7 @@ export async function POST(request: NextRequest) {
3337
);
3438
}
3539

36-
const { taskId } = result.data;
40+
const { taskId, visibility } = result.data;
3741

3842
// Check if task sharing is enabled for the organization
3943
const orgSettings = await getOrganizationSettings();
@@ -59,7 +63,7 @@ export async function POST(request: NextRequest) {
5963
);
6064
}
6165

62-
const shareResponse = await createTaskShare({ taskId });
66+
const shareResponse = await createTaskShare({ taskId, visibility });
6367

6468
if (!shareResponse.success || !shareResponse.data) {
6569
return NextResponse.json(

apps/web/src/app/(authenticated)/share/[token]/page.tsx renamed to apps/web/src/app/share/[token]/page.tsx

Lines changed: 23 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
import { notFound, redirect } from 'next/navigation';
22

3-
import { authorize } from '@/actions/auth';
43
import { getTaskByShareToken } from '@/actions/taskSharing';
5-
64
import { SharedTaskView } from '@/components/task-sharing/SharedTaskView';
5+
import { Badge } from '@/components/ui';
76

8-
type SharedTaskPageProps = { params: { token: string } };
7+
type SharedTaskPageProps = {
8+
params: Promise<{
9+
token: string;
10+
}>;
11+
};
912

1013
export default async function SharedTaskPage({ params }: SharedTaskPageProps) {
11-
const authResult = await authorize();
12-
13-
if (!authResult.success) {
14-
redirect('/select-org');
15-
}
16-
1714
try {
1815
const { token } = await params;
1916
const result = await getTaskByShareToken(token);
@@ -22,13 +19,16 @@ export default async function SharedTaskPage({ params }: SharedTaskPageProps) {
2219
notFound();
2320
}
2421

25-
const { task, messages, sharedBy, sharedAt } = result;
22+
const { task, messages, sharedBy, sharedAt, visibility } = result;
2623

2724
return (
2825
<div className="container mx-auto py-6">
2926
<div className="mb-4">
3027
<div className="flex items-center gap-2 text-sm text-muted-foreground">
3128
<span>Shared Task</span>
29+
{visibility === 'public' && (
30+
<Badge variant="secondary">Public</Badge>
31+
)}
3232
</div>
3333
</div>
3434
<SharedTaskView
@@ -40,22 +40,15 @@ export default async function SharedTaskPage({ params }: SharedTaskPageProps) {
4040
</div>
4141
);
4242
} catch (error) {
43-
if (error instanceof Error && error.message.includes('Access denied')) {
44-
return (
45-
<div className="container mx-auto py-6">
46-
<div className="text-center">
47-
<h1 className="text-2xl font-bold text-destructive mb-4">
48-
Access Denied
49-
</h1>
50-
<p className="text-muted-foreground mb-4">
51-
You must be a member of the organization to view this shared task.
52-
</p>
53-
<p className="text-sm text-muted-foreground">
54-
Please contact the person who shared this link to ensure you have
55-
the correct organization access.
56-
</p>
57-
</div>
58-
</div>
43+
// Handle auth errors for org shares
44+
if (
45+
error instanceof Error &&
46+
error.message.includes('Authentication required')
47+
) {
48+
// For organization shares that require auth, redirect to sign-in
49+
const { token } = await params;
50+
redirect(
51+
`/sign-in?redirect_url=${encodeURIComponent(`/share/${token}`)}`,
5952
);
6053
}
6154

@@ -74,12 +67,14 @@ export async function generateMetadata({ params }: SharedTaskPageProps) {
7467
};
7568
}
7669

77-
const { task } = result;
70+
const { task, visibility } = result;
7871
const title = task.title || `Task by ${task.user.name}`;
7972

8073
return {
8174
title: `Shared Task: ${title}`,
82-
description: `View shared task details and conversation history`,
75+
description: `View shared task details and conversation history${
76+
visibility === 'public' ? ' (Public)' : ''
77+
}`,
8378
};
8479
} catch (_error) {
8580
return {

0 commit comments

Comments
 (0)