diff --git a/.kiro/specs/user-auth-and-sharing/tasks.md b/.kiro/specs/user-auth-and-sharing/tasks.md index e30d9d4..a978f91 100644 --- a/.kiro/specs/user-auth-and-sharing/tasks.md +++ b/.kiro/specs/user-auth-and-sharing/tasks.md @@ -208,7 +208,7 @@ - Add invitations page - _Requirements: 5.1, 5.4, 8.1, 8.2_ -- [-] 14. Implement profile management +- [x] 14. Implement profile management - Create profile page and ProfileForm component - Implement updateProfile server action - Create PasswordChangeForm component @@ -236,7 +236,7 @@ - **Property 29: Profile data exposure protection** - **Validates: Requirements 7.5** -- [ ] 15. Update existing components for multi-user +- [x] 15. Update existing components for multi-user - Modify ListCard to show sharing indicator - Update ListDetailView with share button and permission display - Modify ItemForm to disable editing for viewers diff --git a/app/list/[id]/item/[itemId]/edit/page.tsx b/app/list/[id]/item/[itemId]/edit/page.tsx index ad4a7fe..10cb47c 100644 --- a/app/list/[id]/item/[itemId]/edit/page.tsx +++ b/app/list/[id]/item/[itemId]/edit/page.tsx @@ -1,5 +1,7 @@ import { notFound } from 'next/navigation' import { getItem } from '@/lib/actions' +import { canEditList } from '@/lib/permissions' +import { auth } from '@/auth' import ItemForm from '@/components/ItemForm' import Link from 'next/link' import { ArrowLeft } from 'lucide-react' @@ -14,12 +16,20 @@ interface PageProps { } export default async function EditItemPage({ params }: PageProps) { + const session = await auth() + if (!session?.user?.id) { + notFound() + } + const item = await getItem(params.itemId) if (!item || item.listId !== params.id) { notFound() } + // Check if user can edit this list + const canEdit = await canEditList(session.user.id, params.id) + return (
{/* Header */} @@ -36,7 +46,7 @@ export default async function EditItemPage({ params }: PageProps) { {/* Form */}
- +
) diff --git a/app/list/[id]/item/[itemId]/page.tsx b/app/list/[id]/item/[itemId]/page.tsx index 498eb55..410759d 100644 --- a/app/list/[id]/item/[itemId]/page.tsx +++ b/app/list/[id]/item/[itemId]/page.tsx @@ -1,5 +1,7 @@ import { notFound } from 'next/navigation' import { getItem } from '@/lib/actions' +import { canEditList } from '@/lib/permissions' +import { auth } from '@/auth' import ItemDetail from '@/components/ItemDetail' import Link from 'next/link' import { ArrowLeft, Edit } from 'lucide-react' @@ -16,12 +18,29 @@ interface PageProps { } export default async function ItemDetailPage({ params }: PageProps) { - const item = await getItem(params.itemId) - - if (!item || item.listId !== params.id) { + const session = await auth() + if (!session?.user?.id) { notFound() } + const itemData = await getItem(params.itemId) + + if (!itemData || itemData.listId !== params.id) { + notFound() + } + + // Type assertion for the item with list relation + const item = itemData as typeof itemData & { + list: { + title: string + color: string + ownerId: string + } + } + + // Check if user can edit this list + const canEdit = await canEditList(session.user.id, params.id) + return (
{/* Header Actions */} @@ -35,21 +54,40 @@ export default async function ItemDetailPage({ params }: PageProps) {
- - - - Edit - - + {canEdit && ( + <> + + + + Edit + + + + )}
{/* Item Detail */}
- +
) diff --git a/app/list/[id]/page.tsx b/app/list/[id]/page.tsx index 0637101..3c9dd09 100644 --- a/app/list/[id]/page.tsx +++ b/app/list/[id]/page.tsx @@ -1,4 +1,6 @@ import { getListWithItems } from '@/lib/actions' +import { getPermission } from '@/lib/permissions' +import { auth } from '@/auth' import { notFound } from 'next/navigation' import Link from 'next/link' import { ArrowLeft } from 'lucide-react' @@ -8,12 +10,25 @@ import ListDetailView from '@/components/ListDetailView' export const dynamic = 'force-dynamic' export default async function ListDetailPage({ params }: { params: { id: string } }) { + const session = await auth() + if (!session?.user?.id) { + notFound() + } + const list = await getListWithItems(params.id) if (!list) { notFound() } + // Get user's permission for this list + const permission = await getPermission(session.user.id, params.id) + if (!permission) { + notFound() + } + + const canShare = permission === 'owner' + const canEdit = permission === 'owner' || permission === 'editor' const completedItems = list.items.filter(item => item.completed) return ( @@ -41,13 +56,28 @@ export default async function ListDetailPage({ params }: { params: { id: string

- {/* Inline Item Creation */} -
- -
+ {/* Inline Item Creation - only show if user can edit */} + {canEdit && ( +
+ +
+ )} {/* Search, Filter, and Items List */} - + ({ + ...c, + permission: c.permission as 'editor' | 'viewer' + }))} + /> ) } diff --git a/components/ItemForm.tsx b/components/ItemForm.tsx index 1fbaf32..2a58988 100644 --- a/components/ItemForm.tsx +++ b/components/ItemForm.tsx @@ -26,9 +26,10 @@ interface ItemFormProps { notes: string | null } mode?: 'create' | 'edit' + canEdit?: boolean } -export default function ItemForm({ listId, item, mode = 'edit' }: ItemFormProps) { +export default function ItemForm({ listId, item, mode = 'edit', canEdit = true }: ItemFormProps) { const router = useRouter() const { showToast } = useToast() const [isSubmitting, setIsSubmitting] = useState(false) @@ -157,6 +158,11 @@ export default function ItemForm({ listId, item, mode = 'edit' }: ItemFormProps) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() + if (!canEdit) { + showToast('You do not have permission to edit this item', 'error') + return + } + if (!validateForm()) { showToast('Please fix validation errors', 'error') return @@ -238,6 +244,14 @@ export default function ItemForm({ listId, item, mode = 'edit' }: ItemFormProps) return (
+ {!canEdit && ( +
+

+ You have view-only access to this list. You cannot edit items. +

+
+ )} + {/* Title */}
) diff --git a/components/ListCard.tsx b/components/ListCard.tsx index b3cb0dc..ad7a2d3 100644 --- a/components/ListCard.tsx +++ b/components/ListCard.tsx @@ -2,7 +2,7 @@ import { memo, useState } from 'react' import Link from 'next/link' -import { Edit2 } from 'lucide-react' +import { Edit2, Users } from 'lucide-react' import Modal from './Modal' import ListForm from './ListForm' import DeleteListButton from './DeleteListButton' @@ -15,6 +15,11 @@ interface List { id: string completed: boolean }> + collaborators?: Array<{ + id: string + userId: string + permission: string + }> } function ListCard({ list }: { list: List }) { @@ -22,6 +27,7 @@ function ListCard({ list }: { list: List }) { const completedCount = list.items.filter(item => item.completed).length const totalCount = list.items.length const progress = totalCount > 0 ? (completedCount / totalCount) * 100 : 0 + const hasCollaborators = list.collaborators && list.collaborators.length > 0 const handleEditClick = (e: React.MouseEvent) => { e.preventDefault() @@ -45,6 +51,12 @@ function ListCard({ list }: { list: List }) { style={{ backgroundColor: list.color }} />

{list.title}

+ {hasCollaborators && list.collaborators && ( +
+ + {list.collaborators.length} +
+ )}
+ )} +
+ )} + {/* Search Bar */} )} + + {/* Share Modal */} + setIsShareModalOpen(false)} + listId={listId} + listTitle={listTitle} + isOwner={permission === 'owner'} + ownerId={ownerId} + ownerName={ownerName} + ownerEmail={ownerEmail} + initialCollaborators={collaborators} + /> ) }