diff --git a/apps/web/src/actions/analytics/events.ts b/apps/web/src/actions/analytics/events.ts index d8a0bc77b8..683922a445 100644 --- a/apps/web/src/actions/analytics/events.ts +++ b/apps/web/src/actions/analytics/events.ts @@ -309,17 +309,24 @@ export const getTasks = async ({ userId, taskId, allowCrossUserAccess = false, + skipAuth = false, }: { orgId?: string | null; userId?: string | null; taskId?: string | null; allowCrossUserAccess?: boolean; + skipAuth?: boolean; }): Promise => { - const { effectiveUserId } = await authorizeAnalytics({ - requestedOrgId: orgId, - requestedUserId: userId, - allowCrossUserAccess, - }); + let effectiveUserId = userId; + + if (!skipAuth) { + const authResult = await authorizeAnalytics({ + requestedOrgId: orgId, + requestedUserId: userId, + allowCrossUserAccess, + }); + effectiveUserId = authResult.effectiveUserId; + } if (!orgId) { return []; diff --git a/apps/web/src/actions/taskSharing.ts b/apps/web/src/actions/taskSharing.ts index f5bee5485b..67ad06e709 100644 --- a/apps/web/src/actions/taskSharing.ts +++ b/apps/web/src/actions/taskSharing.ts @@ -7,7 +7,8 @@ import { createTaskShareSchema, shareIdSchema, } from '@/types'; -import type { SharedByUser } from '@/types'; +import type { SharedByUser } from '@/types/task-sharing'; +import { TaskShareVisibility } from '@/types/task-sharing'; import { type TaskShare, AuditLogTargetType } from '@/db'; import { client as db, taskShares, users } from '@/db/server'; import { handleError, generateShareToken } from '@/lib/server'; @@ -41,6 +42,7 @@ export async function canShareTask(taskId: string): Promise<{ orgRole?: string; }> { try { + // Get authentication info const authResult = await authorize(); if (!authResult.success) { @@ -56,6 +58,7 @@ export async function canShareTask(taskId: string): Promise<{ orgId, allowCrossUserAccess: true, }); + const task = tasks[0]; if (!task) { @@ -100,7 +103,11 @@ export async function createTaskShare(data: CreateTaskShareRequest) { return { success: false, error: 'Invalid request data' }; } - const { taskId, expirationDays } = result.data; + const { + taskId, + expirationDays, + visibility = TaskShareVisibility.ORGANIZATION, + } = result.data; const orgSettingsData = await getOrganizationSettings(); @@ -142,6 +149,7 @@ export async function createTaskShare(data: CreateTaskShareRequest) { orgId, createdByUserId: userId, shareToken, + visibility, expiresAt, }) .returning(); @@ -158,11 +166,12 @@ export async function createTaskShare(data: CreateTaskShareRequest) { newValue: { action: 'created', shareId: insertedShare[0].id, + visibility, expiresAt: expiresAt.toISOString(), taskOwnerId: task.userId, sharedByAdmin: orgRole === 'org:admin' && task.userId !== userId, }, - description: `Created task share for task ${taskId}${ + description: `Created ${visibility} task share for task ${taskId}${ orgRole === 'org:admin' && task.userId !== userId ? ` (admin sharing task created by ${task.user.name})` : '' @@ -182,8 +191,8 @@ export async function createTaskShare(data: CreateTaskShareRequest) { return { success: true, - message: 'Task share created successfully', data: { shareUrl, shareId: newShare.id, expiresAt }, + message: 'Task share created successfully', }; } catch (error) { return handleError(error, 'task_sharing'); @@ -198,18 +207,14 @@ export async function getTaskByShareToken(token: string): Promise<{ messages: Message[]; sharedBy: SharedByUser; sharedAt: Date; + visibility: string; } | null> { try { - const authResult = await authorize(); - - if (!authResult.success) { - throw new Error('Authentication required'); - } - if (!isValidShareToken(token)) { return null; } + // First, get the share without auth check to determine visibility const [shareWithUser] = await db .select({ share: taskShares, @@ -221,12 +226,7 @@ export async function getTaskByShareToken(token: string): Promise<{ }) .from(taskShares) .innerJoin(users, eq(taskShares.createdByUserId, users.id)) - .where( - and( - eq(taskShares.shareToken, token), - eq(taskShares.orgId, authResult.orgId), - ), - ) + .where(eq(taskShares.shareToken, token)) .limit(1); if (!shareWithUser) { @@ -239,12 +239,25 @@ export async function getTaskByShareToken(token: string): Promise<{ return null; } + // Check visibility and auth requirements + if (share.visibility === TaskShareVisibility.ORGANIZATION) { + const authResult = await authorize(); + const userId = authResult.success ? authResult.userId : null; + const orgId = authResult.success ? authResult.orgId : null; + + if (!userId || !orgId || orgId !== share.orgId) { + throw new Error('Authentication required for organization shares'); + } + } + // For public shares, no auth check needed + + // Get task data based on visibility const tasks = await getTasks({ taskId: share.taskId, orgId: share.orgId, allowCrossUserAccess: true, + skipAuth: share.visibility === TaskShareVisibility.PUBLIC, // Skip auth for public shares }); - const task = tasks[0]; if (!task) { @@ -258,6 +271,7 @@ export async function getTaskByShareToken(token: string): Promise<{ messages, sharedBy: sharedByUser, sharedAt: share.createdAt, + visibility: share.visibility, }; } catch (error) { console.error( @@ -281,6 +295,7 @@ export async function deleteTaskShare(shareId: string) { } const { userId, orgId, orgRole } = authResult; + const shareIdResult = shareIdSchema.safeParse(shareId); if (!shareIdResult.success) { diff --git a/apps/web/src/app/api/extension/share/route.ts b/apps/web/src/app/api/extension/share/route.ts index 2fc8f583ec..6824f831dd 100644 --- a/apps/web/src/app/api/extension/share/route.ts +++ b/apps/web/src/app/api/extension/share/route.ts @@ -1,18 +1,22 @@ import { NextRequest, NextResponse } from 'next/server'; import { z } from 'zod'; -import { authorizeApi } from '@/actions/auth'; import { createTaskShare } from '@/actions/taskSharing'; import { getTasks } from '@/actions/analytics'; import { getOrganizationSettings } from '@/actions/organizationSettings'; +import { authorize } from '@/actions/auth'; +import { TaskShareVisibility } from '@/types/task-sharing'; const createShareRequestSchema = z.object({ taskId: z.string().min(1, 'Task ID is required'), + visibility: z + .nativeEnum(TaskShareVisibility) + .default(TaskShareVisibility.ORGANIZATION), }); export async function POST(request: NextRequest) { try { - const authResult = await authorizeApi(request); + const authResult = await authorize(); if (!authResult.success) { return NextResponse.json( @@ -33,7 +37,7 @@ export async function POST(request: NextRequest) { ); } - const { taskId } = result.data; + const { taskId, visibility } = result.data; // Check if task sharing is enabled for the organization const orgSettings = await getOrganizationSettings(); @@ -59,7 +63,7 @@ export async function POST(request: NextRequest) { ); } - const shareResponse = await createTaskShare({ taskId }); + const shareResponse = await createTaskShare({ taskId, visibility }); if (!shareResponse.success || !shareResponse.data) { return NextResponse.json( diff --git a/apps/web/src/app/(authenticated)/share/[token]/page.tsx b/apps/web/src/app/share/[token]/page.tsx similarity index 58% rename from apps/web/src/app/(authenticated)/share/[token]/page.tsx rename to apps/web/src/app/share/[token]/page.tsx index 198734473c..0670e9b895 100644 --- a/apps/web/src/app/(authenticated)/share/[token]/page.tsx +++ b/apps/web/src/app/share/[token]/page.tsx @@ -1,19 +1,16 @@ import { notFound, redirect } from 'next/navigation'; -import { authorize } from '@/actions/auth'; import { getTaskByShareToken } from '@/actions/taskSharing'; - import { SharedTaskView } from '@/components/task-sharing/SharedTaskView'; +import { Badge } from '@/components/ui'; -type SharedTaskPageProps = { params: { token: string } }; +type SharedTaskPageProps = { + params: Promise<{ + token: string; + }>; +}; export default async function SharedTaskPage({ params }: SharedTaskPageProps) { - const authResult = await authorize(); - - if (!authResult.success) { - redirect('/select-org'); - } - try { const { token } = await params; const result = await getTaskByShareToken(token); @@ -22,13 +19,16 @@ export default async function SharedTaskPage({ params }: SharedTaskPageProps) { notFound(); } - const { task, messages, sharedBy, sharedAt } = result; + const { task, messages, sharedBy, sharedAt, visibility } = result; return (
Shared Task + {visibility === 'public' && ( + Public + )}
); } catch (error) { - if (error instanceof Error && error.message.includes('Access denied')) { - return ( -
-
-

- Access Denied -

-

- You must be a member of the organization to view this shared task. -

-

- Please contact the person who shared this link to ensure you have - the correct organization access. -

-
-
+ // Handle auth errors for org shares + if ( + error instanceof Error && + error.message.includes('Authentication required') + ) { + // For organization shares that require auth, redirect to sign-in + const { token } = await params; + redirect( + `/sign-in?redirect_url=${encodeURIComponent(`/share/${token}`)}`, ); } @@ -74,12 +67,14 @@ export async function generateMetadata({ params }: SharedTaskPageProps) { }; } - const { task } = result; + const { task, visibility } = result; const title = task.title || `Task by ${task.user.name}`; return { title: `Shared Task: ${title}`, - description: `View shared task details and conversation history`, + description: `View shared task details and conversation history${ + visibility === 'public' ? ' (Public)' : '' + }`, }; } catch (_error) { return { diff --git a/apps/web/src/components/task-sharing/ShareButton.tsx b/apps/web/src/components/task-sharing/ShareButton.tsx index 75966802fc..c52b31813f 100644 --- a/apps/web/src/components/task-sharing/ShareButton.tsx +++ b/apps/web/src/components/task-sharing/ShareButton.tsx @@ -1,11 +1,20 @@ 'use client'; import { useState } from 'react'; -import { Copy, Share, Trash2, ExternalLink } from 'lucide-react'; +import { + Copy, + Share, + Trash2, + ExternalLink, + Users, + Globe, + Check, +} from 'lucide-react'; import { toast } from 'sonner'; import type { TaskShare } from '@/db'; import type { TaskWithUser } from '@/actions/analytics'; +import { TaskShareVisibility } from '@/types/task-sharing'; import { createTaskShare, deleteTaskShare, @@ -25,6 +34,8 @@ import { DialogHeader, DialogTitle, DialogTrigger, + Label, + Badge, } from '@/components/ui'; type ShareButtonProps = { @@ -36,6 +47,9 @@ export const ShareButton = ({ task }: ShareButtonProps) => { const [isCreating, setIsCreating] = useState(false); const [shares, setShares] = useState([]); const [newShareUrl, setNewShareUrl] = useState(null); + const [visibility, setVisibility] = useState( + TaskShareVisibility.ORGANIZATION, + ); const { data: orgSettings } = useOrganizationSettings(); @@ -58,13 +72,17 @@ export const ShareButton = ({ task }: ShareButtonProps) => { if (open) { loadShares(); setNewShareUrl(null); + setVisibility(TaskShareVisibility.ORGANIZATION); // Reset to default when opening } }; const handleCreateShare = async () => { setIsCreating(true); try { - const response = await createTaskShare({ taskId: task.taskId }); + const response = await createTaskShare({ + taskId: task.taskId, + visibility, + }); if (response.success && response.data) { setNewShareUrl(response.data.shareUrl); @@ -119,20 +137,31 @@ export const ShareButton = ({ task }: ShareButtonProps) => { Share - + Share Task - Create a link to share this task with your team. + {newShareUrl + ? 'Your share link is ready to use.' + : visibility === TaskShareVisibility.ORGANIZATION + ? 'Create a link to share this task with your team.' + : 'Create a public link that anyone can access.'}
{/* Create New Share */} {newShareUrl ? ( -
-
-

Share link created!

+
+
+
+
+ +
+

+ Share link created! +

+
+
) : ( - + <> + {/* Visibility Selector */} +
+ +
+ + + +
+
+ + + )} {/* Existing Shares */} {shares.length > 0 && ( -
-

+
+

Previous Links ({shares.length})

@@ -179,19 +304,34 @@ export const ShareButton = ({ task }: ShareButtonProps) => { return (
+
+ {share.visibility === TaskShareVisibility.PUBLIC ? ( + + ) : ( + + )} +
-

- Created{' '} - {new Date(share.createdAt).toLocaleDateString()} -

+
+

+ Created{' '} + {new Date(share.createdAt).toLocaleDateString()} +

+ {share.visibility === TaskShareVisibility.PUBLIC && ( + + Public + + )} +
@@ -199,6 +339,7 @@ export const ShareButton = ({ task }: ShareButtonProps) => { variant="ghost" size="sm" onClick={() => handleDeleteShare(share.id)} + className="h-8 w-8 p-0" > @@ -207,7 +348,7 @@ export const ShareButton = ({ task }: ShareButtonProps) => { ); })} {shares.length > 3 && ( -

+

+{shares.length - 3} more links

)} @@ -216,10 +357,14 @@ export const ShareButton = ({ task }: ShareButtonProps) => { )} {/* Simple Info */} -

- Links expire in {expirationDays} days and are only accessible to - your organization members. -

+ {!newShareUrl && ( +

+ Links expire in {expirationDays} days + {visibility === TaskShareVisibility.ORGANIZATION + ? ' and are only accessible to your organization members.' + : '. Anyone with the link will be able to view this task.'} +

+ )}
diff --git a/apps/web/src/db/migrations/0007_tidy_blue_blade.sql b/apps/web/src/db/migrations/0007_tidy_blue_blade.sql new file mode 100644 index 0000000000..dab2f790bc --- /dev/null +++ b/apps/web/src/db/migrations/0007_tidy_blue_blade.sql @@ -0,0 +1,2 @@ +ALTER TABLE "task_shares" ADD COLUMN "visibility" text DEFAULT 'organization' NOT NULL;--> statement-breakpoint +CREATE INDEX "task_shares_visibility_idx" ON "task_shares" USING btree ("visibility"); \ No newline at end of file diff --git a/apps/web/src/db/migrations/meta/0007_snapshot.json b/apps/web/src/db/migrations/meta/0007_snapshot.json new file mode 100644 index 0000000000..a7c31febb1 --- /dev/null +++ b/apps/web/src/db/migrations/meta/0007_snapshot.json @@ -0,0 +1,973 @@ +{ + "id": "a6128d1e-dc00-490a-8c2c-70da368301bd", + "prevId": "d720a01c-3f4b-4000-a6fd-c5abfc6816b7", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.agent_request_logs": { + "name": "agent_request_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "response_time_ms": { + "name": "response_time_ms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_request_logs_agent_id_idx": { + "name": "agent_request_logs_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_request_logs_org_id_idx": { + "name": "agent_request_logs_org_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_request_logs_created_at_idx": { + "name": "agent_request_logs_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_request_logs_agent_id_agents_id_fk": { + "name": "agent_request_logs_agent_id_agents_id_fk", + "tableFrom": "agent_request_logs", + "tableTo": "agents", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_request_logs_organization_id_organizations_id_fk": { + "name": "agent_request_logs_organization_id_organizations_id_fk", + "tableFrom": "agent_request_logs", + "tableTo": "organizations", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agents_org_id_idx": { + "name": "agents_org_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_active_idx": { + "name": "agents_active_idx", + "columns": [ + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"agents\".\"is_active\" = 1", + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_last_used_idx": { + "name": "agents_last_used_idx", + "columns": [ + { + "expression": "last_used_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agents_organization_id_organizations_id_fk": { + "name": "agents_organization_id_organizations_id_fk", + "tableFrom": "agents", + "tableTo": "organizations", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agents_created_by_user_id_users_id_fk": { + "name": "agents_created_by_user_id_users_id_fk", + "tableFrom": "agents", + "tableTo": "users", + "columnsFrom": ["created_by_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audit_logs": { + "name": "audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_value": { + "name": "new_value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "audit_logs_user_id_idx": { + "name": "audit_logs_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_logs_organization_id_idx": { + "name": "audit_logs_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_logs_target_idx": { + "name": "audit_logs_target_idx", + "columns": [ + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "audit_logs_created_at_idx": { + "name": "audit_logs_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audit_logs_user_id_users_id_fk": { + "name": "audit_logs_user_id_users_id_fk", + "tableFrom": "audit_logs", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "audit_logs_organization_id_organizations_id_fk": { + "name": "audit_logs_organization_id_organizations_id_fk", + "tableFrom": "audit_logs", + "tableTo": "organizations", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_settings": { + "name": "organization_settings", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "cloud_settings": { + "name": "cloud_settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "default_settings": { + "name": "default_settings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "allow_list": { + "name": "allow_list", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{\"allowAll\":true,\"providers\":{}}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_settings_created_at_idx": { + "name": "organization_settings_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_settings_organization_id_organizations_id_fk": { + "name": "organization_settings_organization_id_organizations_id_fk", + "tableFrom": "organization_settings", + "tableTo": "organizations", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity": { + "name": "entity", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organizations_slug_idx": { + "name": "organizations_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "organizations_created_at_idx": { + "name": "organizations_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task_shares": { + "name": "task_shares", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "share_token": { + "name": "share_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'organization'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "task_shares_share_token_idx": { + "name": "task_shares_share_token_idx", + "columns": [ + { + "expression": "share_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_shares_task_id_idx": { + "name": "task_shares_task_id_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_shares_org_id_idx": { + "name": "task_shares_org_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_shares_expires_at_idx": { + "name": "task_shares_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_shares_created_by_user_id_idx": { + "name": "task_shares_created_by_user_id_idx", + "columns": [ + { + "expression": "created_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_shares_visibility_idx": { + "name": "task_shares_visibility_idx", + "columns": [ + { + "expression": "visibility", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "task_shares_organization_id_organizations_id_fk": { + "name": "task_shares_organization_id_organizations_id_fk", + "tableFrom": "task_shares", + "tableTo": "organizations", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "task_shares_created_by_user_id_users_id_fk": { + "name": "task_shares_created_by_user_id_users_id_fk", + "tableFrom": "task_shares", + "tableTo": "users", + "columnsFrom": ["created_by_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "task_shares_share_token_unique": { + "name": "task_shares_share_token_unique", + "nullsNotDistinct": false, + "columns": ["share_token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_role": { + "name": "organization_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity": { + "name": "entity", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_organization_id_idx": { + "name": "users_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_organization_role_idx": { + "name": "users_organization_role_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_email_idx": { + "name": "users_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_created_at_idx": { + "name": "users_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_organization_id_organizations_id_fk": { + "name": "users_organization_id_organizations_id_fk", + "tableFrom": "users", + "tableTo": "organizations", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/apps/web/src/db/migrations/meta/_journal.json b/apps/web/src/db/migrations/meta/_journal.json index bd7d3ecf6e..5dbff50f39 100644 --- a/apps/web/src/db/migrations/meta/_journal.json +++ b/apps/web/src/db/migrations/meta/_journal.json @@ -50,6 +50,13 @@ "when": 1750289379751, "tag": "0006_ambiguous_professor_monster", "breakpoints": true + }, + { + "idx": 7, + "version": "7", + "when": 1750363919215, + "tag": "0007_tidy_blue_blade", + "breakpoints": true } ] } diff --git a/apps/web/src/db/schema.ts b/apps/web/src/db/schema.ts index 225ba571b9..abe7a3c897 100644 --- a/apps/web/src/db/schema.ts +++ b/apps/web/src/db/schema.ts @@ -178,6 +178,7 @@ export const taskShares = pgTable( .notNull() .references(() => users.id), shareToken: text('share_token').notNull().unique(), + visibility: text('visibility').notNull().default('organization'), expiresAt: timestamp('expires_at'), createdAt: timestamp('created_at').notNull().defaultNow(), updatedAt: timestamp('updated_at').notNull().defaultNow(), @@ -188,6 +189,7 @@ export const taskShares = pgTable( index('task_shares_org_id_idx').on(table.orgId), index('task_shares_expires_at_idx').on(table.expiresAt), index('task_shares_created_by_user_id_idx').on(table.createdByUserId), + index('task_shares_visibility_idx').on(table.visibility), ], ); diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts index e389d4219b..8bbdc07b30 100644 --- a/apps/web/src/middleware.ts +++ b/apps/web/src/middleware.ts @@ -6,6 +6,7 @@ const isUnprotectedRoute = createRouteMatcher([ '/sign-up(.*)', '/extension/sign-in(.*)', '/api/marketplace(.*)', + '/share(.*)', '/', ]); diff --git a/apps/web/src/types/task-sharing.ts b/apps/web/src/types/task-sharing.ts index 11aa76132b..05bc9611b1 100644 --- a/apps/web/src/types/task-sharing.ts +++ b/apps/web/src/types/task-sharing.ts @@ -1,8 +1,16 @@ import { z } from 'zod'; +export enum TaskShareVisibility { + ORGANIZATION = 'organization', + PUBLIC = 'public', +} + export const createTaskShareSchema = z.object({ taskId: z.string().min(1, 'Task ID is required'), expirationDays: z.number().int().positive().max(365).optional(), + visibility: z + .nativeEnum(TaskShareVisibility) + .default(TaskShareVisibility.ORGANIZATION), }); export type CreateTaskShareRequest = z.infer;