feat: implement list ownership and access control
- Modified createList to assign ownerId to authenticated user - Updated getLists to filter by user access (owner or collaborator) - Added ownership checks to updateList (owner or editor can update) - Added ownership checks to deleteList (only owner can delete) - Implemented access control for all item operations (create, update, delete, toggle) - Updated getItem, searchItems, and getItemsByFilter to enforce access control - Added dynamic rendering to pages that use authentication - Cascade deletion already handled by database schema (onDelete: Cascade) Requirements: 4.1, 4.2, 4.3, 4.5
This commit is contained in:
@@ -96,7 +96,7 @@
|
|||||||
- Redirect unauthenticated users to login
|
- Redirect unauthenticated users to login
|
||||||
- _Requirements: 3.3, 3.5_
|
- _Requirements: 3.3, 3.5_
|
||||||
|
|
||||||
- [ ] 8. Update list operations for ownership
|
- [x] 8. Update list operations for ownership
|
||||||
- Modify createList to assign ownerId
|
- Modify createList to assign ownerId
|
||||||
- Update getLists to filter by user access
|
- Update getLists to filter by user access
|
||||||
- Add ownership checks to updateList and deleteList
|
- Add ownership checks to updateList and deleteList
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import ItemForm from '@/components/ItemForm'
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { ArrowLeft } from 'lucide-react'
|
import { ArrowLeft } from 'lucide-react'
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
params: {
|
params: {
|
||||||
id: string
|
id: string
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { ArrowLeft, Edit } from 'lucide-react'
|
|||||||
import DeleteItemButton from '@/components/DeleteItemButton'
|
import DeleteItemButton from '@/components/DeleteItemButton'
|
||||||
import ToggleItemButton from '@/components/ToggleItemButton'
|
import ToggleItemButton from '@/components/ToggleItemButton'
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
params: {
|
params: {
|
||||||
id: string
|
id: string
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { ArrowLeft } from 'lucide-react'
|
|||||||
import InlineItemCreate from '@/components/InlineItemCreate'
|
import InlineItemCreate from '@/components/InlineItemCreate'
|
||||||
import ListDetailView from '@/components/ListDetailView'
|
import ListDetailView from '@/components/ListDetailView'
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
export default async function ListDetailPage({ params }: { params: { id: string } }) {
|
export default async function ListDetailPage({ params }: { params: { id: string } }) {
|
||||||
const list = await getListWithItems(params.id)
|
const list = await getListWithItems(params.id)
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import NewListButton from '@/components/NewListButton'
|
|||||||
import GlobalSearch from '@/components/GlobalSearch'
|
import GlobalSearch from '@/components/GlobalSearch'
|
||||||
import EmptyState from '@/components/EmptyState'
|
import EmptyState from '@/components/EmptyState'
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic'
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
const lists = await getLists()
|
const lists = await getLists()
|
||||||
|
|
||||||
|
|||||||
427
lib/actions.ts
427
lib/actions.ts
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { prisma } from './db'
|
import { prisma } from './db'
|
||||||
import { revalidatePath } from 'next/cache'
|
import { revalidatePath } from 'next/cache'
|
||||||
|
import { auth } from '@/auth'
|
||||||
import {
|
import {
|
||||||
validateListTitle,
|
validateListTitle,
|
||||||
validateItemTitle,
|
validateItemTitle,
|
||||||
@@ -47,10 +48,49 @@ async function retryOperation<T>(
|
|||||||
|
|
||||||
export async function getLists() {
|
export async function getLists() {
|
||||||
try {
|
try {
|
||||||
|
// Get current user session
|
||||||
|
const session = await auth()
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
throw new Error('Unauthorized: Please log in to view lists')
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = session.user.id
|
||||||
|
|
||||||
|
// Get lists where user is owner or collaborator
|
||||||
return await retryOperation(() =>
|
return await retryOperation(() =>
|
||||||
prisma.list.findMany({
|
prisma.list.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ ownerId: userId },
|
||||||
|
{
|
||||||
|
collaborators: {
|
||||||
|
some: {
|
||||||
|
userId: userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
include: {
|
include: {
|
||||||
items: true,
|
items: true,
|
||||||
|
owner: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
collaborators: {
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
updatedAt: 'desc',
|
updatedAt: 'desc',
|
||||||
@@ -59,12 +99,23 @@ export async function getLists() {
|
|||||||
)
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching lists:', error)
|
console.error('Error fetching lists:', error)
|
||||||
|
if (error instanceof Error && error.message.includes('Unauthorized')) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
throw new Error('Failed to load lists. Please check your connection and try again.')
|
throw new Error('Failed to load lists. Please check your connection and try again.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createList(title: string, color: string = '#3B82F6') {
|
export async function createList(title: string, color: string = '#3B82F6') {
|
||||||
try {
|
try {
|
||||||
|
// Get current user session
|
||||||
|
const session = await auth()
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
throw new Error('Unauthorized: Please log in to create lists')
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = session.user.id
|
||||||
|
|
||||||
// Validate and sanitize input
|
// Validate and sanitize input
|
||||||
const titleResult = validateListTitle(title)
|
const titleResult = validateListTitle(title)
|
||||||
if (!titleResult.isValid) {
|
if (!titleResult.isValid) {
|
||||||
@@ -83,6 +134,7 @@ export async function createList(title: string, color: string = '#3B82F6') {
|
|||||||
data: {
|
data: {
|
||||||
title: sanitizedTitle,
|
title: sanitizedTitle,
|
||||||
color,
|
color,
|
||||||
|
ownerId: userId,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -91,7 +143,7 @@ export async function createList(title: string, color: string = '#3B82F6') {
|
|||||||
return list
|
return list
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating list:', error)
|
console.error('Error creating list:', error)
|
||||||
if (error instanceof Error && (error.message.includes('required') || error.message.includes('must be'))) {
|
if (error instanceof Error && (error.message.includes('required') || error.message.includes('must be') || error.message.includes('Unauthorized'))) {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
throw new Error('Failed to create list. Please try again.')
|
throw new Error('Failed to create list. Please try again.')
|
||||||
@@ -104,27 +156,65 @@ export async function getListWithItems(listId: string) {
|
|||||||
throw new Error('List ID is required')
|
throw new Error('List ID is required')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get current user session
|
||||||
|
const session = await auth()
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
throw new Error('Unauthorized: Please log in to view lists')
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = session.user.id
|
||||||
|
|
||||||
const list = await retryOperation(() =>
|
const list = await retryOperation(() =>
|
||||||
prisma.list.findUnique({
|
prisma.list.findFirst({
|
||||||
where: { id: listId },
|
where: {
|
||||||
|
id: listId,
|
||||||
|
OR: [
|
||||||
|
{ ownerId: userId },
|
||||||
|
{
|
||||||
|
collaborators: {
|
||||||
|
some: {
|
||||||
|
userId: userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
include: {
|
include: {
|
||||||
items: {
|
items: {
|
||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: 'desc',
|
createdAt: 'desc',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
owner: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
collaborators: {
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!list) {
|
if (!list) {
|
||||||
throw new Error('List not found')
|
throw new Error('List not found or access denied')
|
||||||
}
|
}
|
||||||
|
|
||||||
return list
|
return list
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching list:', error)
|
console.error('Error fetching list:', error)
|
||||||
if (error instanceof Error && error.message === 'List not found') {
|
if (error instanceof Error && (error.message === 'List not found or access denied' || error.message.includes('Unauthorized'))) {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
throw new Error('Failed to load list. Please try again.')
|
throw new Error('Failed to load list. Please try again.')
|
||||||
@@ -137,11 +227,40 @@ export async function getItem(itemId: string) {
|
|||||||
throw new Error('Item ID is required')
|
throw new Error('Item ID is required')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get current user session
|
||||||
|
const session = await auth()
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
throw new Error('Unauthorized: Please log in to view items')
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = session.user.id
|
||||||
|
|
||||||
const item = await retryOperation(() =>
|
const item = await retryOperation(() =>
|
||||||
prisma.item.findUnique({
|
prisma.item.findUnique({
|
||||||
where: { id: itemId },
|
where: { id: itemId },
|
||||||
include: {
|
include: {
|
||||||
list: true,
|
list: {
|
||||||
|
include: {
|
||||||
|
owner: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
collaborators: {
|
||||||
|
include: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
email: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -150,10 +269,18 @@ export async function getItem(itemId: string) {
|
|||||||
throw new Error('Item not found')
|
throw new Error('Item not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if user has access to the parent list
|
||||||
|
const hasAccess = item.list.ownerId === userId ||
|
||||||
|
item.list.collaborators.some(c => c.userId === userId)
|
||||||
|
|
||||||
|
if (!hasAccess) {
|
||||||
|
throw new Error('You do not have permission to view this item')
|
||||||
|
}
|
||||||
|
|
||||||
return item
|
return item
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching item:', error)
|
console.error('Error fetching item:', error)
|
||||||
if (error instanceof Error && error.message === 'Item not found') {
|
if (error instanceof Error && (error.message === 'Item not found' || error.message.includes('Unauthorized') || error.message.includes('permission'))) {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
throw new Error('Failed to load item. Please try again.')
|
throw new Error('Failed to load item. Please try again.')
|
||||||
@@ -177,6 +304,38 @@ export async function createItem(
|
|||||||
throw new Error('List ID is required')
|
throw new Error('List ID is required')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get current user session
|
||||||
|
const session = await auth()
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
throw new Error('Unauthorized: Please log in to create items')
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = session.user.id
|
||||||
|
|
||||||
|
// Check if user has access to the list (owner or editor)
|
||||||
|
const list = await retryOperation(() =>
|
||||||
|
prisma.list.findFirst({
|
||||||
|
where: {
|
||||||
|
id: listId,
|
||||||
|
OR: [
|
||||||
|
{ ownerId: userId },
|
||||||
|
{
|
||||||
|
collaborators: {
|
||||||
|
some: {
|
||||||
|
userId: userId,
|
||||||
|
permission: 'editor',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!list) {
|
||||||
|
throw new Error('List not found or you do not have permission to add items')
|
||||||
|
}
|
||||||
|
|
||||||
const titleResult = validateItemTitle(title)
|
const titleResult = validateItemTitle(title)
|
||||||
if (!titleResult.isValid) {
|
if (!titleResult.isValid) {
|
||||||
throw new Error(titleResult.error)
|
throw new Error(titleResult.error)
|
||||||
@@ -214,7 +373,7 @@ export async function createItem(
|
|||||||
return item
|
return item
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating item:', error)
|
console.error('Error creating item:', error)
|
||||||
if (error instanceof Error && (error.message.includes('required') || error.message.includes('must be') || error.message.includes('Invalid'))) {
|
if (error instanceof Error && (error.message.includes('required') || error.message.includes('must be') || error.message.includes('Invalid') || error.message.includes('Unauthorized') || error.message.includes('permission'))) {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
throw new Error('Failed to create item. Please try again.')
|
throw new Error('Failed to create item. Please try again.')
|
||||||
@@ -227,14 +386,49 @@ export async function toggleItem(itemId: string) {
|
|||||||
throw new Error('Item ID is required')
|
throw new Error('Item ID is required')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get current user session
|
||||||
|
const session = await auth()
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
throw new Error('Unauthorized: Please log in to update items')
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = session.user.id
|
||||||
|
|
||||||
const item = await retryOperation(() =>
|
const item = await retryOperation(() =>
|
||||||
prisma.item.findUnique({ where: { id: itemId } })
|
prisma.item.findUnique({
|
||||||
|
where: { id: itemId },
|
||||||
|
include: { list: true },
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
throw new Error('Item not found')
|
throw new Error('Item not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if user has access to the parent list (owner or editor)
|
||||||
|
const hasAccess = await retryOperation(() =>
|
||||||
|
prisma.list.findFirst({
|
||||||
|
where: {
|
||||||
|
id: item.listId,
|
||||||
|
OR: [
|
||||||
|
{ ownerId: userId },
|
||||||
|
{
|
||||||
|
collaborators: {
|
||||||
|
some: {
|
||||||
|
userId: userId,
|
||||||
|
permission: 'editor',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!hasAccess) {
|
||||||
|
throw new Error('You do not have permission to update this item')
|
||||||
|
}
|
||||||
|
|
||||||
const updated = await retryOperation(() =>
|
const updated = await retryOperation(() =>
|
||||||
prisma.item.update({
|
prisma.item.update({
|
||||||
where: { id: itemId },
|
where: { id: itemId },
|
||||||
@@ -246,7 +440,7 @@ export async function toggleItem(itemId: string) {
|
|||||||
return updated
|
return updated
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error toggling item:', error)
|
console.error('Error toggling item:', error)
|
||||||
if (error instanceof Error && error.message === 'Item not found') {
|
if (error instanceof Error && (error.message === 'Item not found' || error.message.includes('Unauthorized') || error.message.includes('permission'))) {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
throw new Error('Failed to update item. Please try again.')
|
throw new Error('Failed to update item. Please try again.')
|
||||||
@@ -270,6 +464,14 @@ export async function updateItem(
|
|||||||
throw new Error('Item ID is required')
|
throw new Error('Item ID is required')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get current user session
|
||||||
|
const session = await auth()
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
throw new Error('Unauthorized: Please log in to update items')
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = session.user.id
|
||||||
|
|
||||||
// Validate and sanitize title if provided
|
// Validate and sanitize title if provided
|
||||||
if (data.title !== undefined) {
|
if (data.title !== undefined) {
|
||||||
const titleResult = validateItemTitle(data.title)
|
const titleResult = validateItemTitle(data.title)
|
||||||
@@ -299,12 +501,39 @@ export async function updateItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const item = await retryOperation(() =>
|
const item = await retryOperation(() =>
|
||||||
prisma.item.findUnique({ where: { id: itemId } })
|
prisma.item.findUnique({
|
||||||
|
where: { id: itemId },
|
||||||
|
include: { list: true },
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
throw new Error('Item not found')
|
throw new Error('Item not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if user has access to the parent list (owner or editor)
|
||||||
|
const hasAccess = await retryOperation(() =>
|
||||||
|
prisma.list.findFirst({
|
||||||
|
where: {
|
||||||
|
id: item.listId,
|
||||||
|
OR: [
|
||||||
|
{ ownerId: userId },
|
||||||
|
{
|
||||||
|
collaborators: {
|
||||||
|
some: {
|
||||||
|
userId: userId,
|
||||||
|
permission: 'editor',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!hasAccess) {
|
||||||
|
throw new Error('You do not have permission to update this item')
|
||||||
|
}
|
||||||
|
|
||||||
const updated = await retryOperation(() =>
|
const updated = await retryOperation(() =>
|
||||||
prisma.item.update({
|
prisma.item.update({
|
||||||
@@ -317,7 +546,7 @@ export async function updateItem(
|
|||||||
return updated
|
return updated
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating item:', error)
|
console.error('Error updating item:', error)
|
||||||
if (error instanceof Error && (error.message.includes('not found') || error.message.includes('required') || error.message.includes('cannot be') || error.message.includes('Invalid'))) {
|
if (error instanceof Error && (error.message.includes('not found') || error.message.includes('required') || error.message.includes('cannot be') || error.message.includes('Invalid') || error.message.includes('Unauthorized') || error.message.includes('permission'))) {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
throw new Error('Failed to update item. Please try again.')
|
throw new Error('Failed to update item. Please try again.')
|
||||||
@@ -330,13 +559,48 @@ export async function deleteItem(itemId: string) {
|
|||||||
throw new Error('Item ID is required')
|
throw new Error('Item ID is required')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get current user session
|
||||||
|
const session = await auth()
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
throw new Error('Unauthorized: Please log in to delete items')
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = session.user.id
|
||||||
|
|
||||||
const item = await retryOperation(() =>
|
const item = await retryOperation(() =>
|
||||||
prisma.item.findUnique({ where: { id: itemId } })
|
prisma.item.findUnique({
|
||||||
|
where: { id: itemId },
|
||||||
|
include: { list: true },
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
throw new Error('Item not found')
|
throw new Error('Item not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if user has access to the parent list (owner or editor)
|
||||||
|
const hasAccess = await retryOperation(() =>
|
||||||
|
prisma.list.findFirst({
|
||||||
|
where: {
|
||||||
|
id: item.listId,
|
||||||
|
OR: [
|
||||||
|
{ ownerId: userId },
|
||||||
|
{
|
||||||
|
collaborators: {
|
||||||
|
some: {
|
||||||
|
userId: userId,
|
||||||
|
permission: 'editor',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!hasAccess) {
|
||||||
|
throw new Error('You do not have permission to delete this item')
|
||||||
|
}
|
||||||
|
|
||||||
await retryOperation(() =>
|
await retryOperation(() =>
|
||||||
prisma.item.delete({
|
prisma.item.delete({
|
||||||
@@ -348,7 +612,7 @@ export async function deleteItem(itemId: string) {
|
|||||||
return { success: true }
|
return { success: true }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting item:', error)
|
console.error('Error deleting item:', error)
|
||||||
if (error instanceof Error && error.message === 'Item not found') {
|
if (error instanceof Error && (error.message === 'Item not found' || error.message.includes('Unauthorized') || error.message.includes('permission'))) {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
throw new Error('Failed to delete item. Please try again.')
|
throw new Error('Failed to delete item. Please try again.')
|
||||||
@@ -361,13 +625,26 @@ export async function deleteList(listId: string) {
|
|||||||
throw new Error('List ID is required')
|
throw new Error('List ID is required')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if list exists
|
// Get current user session
|
||||||
|
const session = await auth()
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
throw new Error('Unauthorized: Please log in to delete lists')
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = session.user.id
|
||||||
|
|
||||||
|
// Check if list exists and user is the owner (only owners can delete)
|
||||||
const list = await retryOperation(() =>
|
const list = await retryOperation(() =>
|
||||||
prisma.list.findUnique({ where: { id: listId } })
|
prisma.list.findFirst({
|
||||||
|
where: {
|
||||||
|
id: listId,
|
||||||
|
ownerId: userId,
|
||||||
|
},
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!list) {
|
if (!list) {
|
||||||
throw new Error('List not found')
|
throw new Error('List not found or you do not have permission to delete it')
|
||||||
}
|
}
|
||||||
|
|
||||||
await retryOperation(() =>
|
await retryOperation(() =>
|
||||||
@@ -380,7 +657,7 @@ export async function deleteList(listId: string) {
|
|||||||
return { success: true }
|
return { success: true }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting list:', error)
|
console.error('Error deleting list:', error)
|
||||||
if (error instanceof Error && error.message === 'List not found') {
|
if (error instanceof Error && (error.message.includes('not found') || error.message.includes('Unauthorized') || error.message.includes('permission'))) {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
throw new Error('Failed to delete list. Please try again.')
|
throw new Error('Failed to delete list. Please try again.')
|
||||||
@@ -399,6 +676,38 @@ export async function updateList(
|
|||||||
throw new Error('List ID is required')
|
throw new Error('List ID is required')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get current user session
|
||||||
|
const session = await auth()
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
throw new Error('Unauthorized: Please log in to update lists')
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = session.user.id
|
||||||
|
|
||||||
|
// Check if user has access to the list (owner or editor)
|
||||||
|
const list = await retryOperation(() =>
|
||||||
|
prisma.list.findFirst({
|
||||||
|
where: {
|
||||||
|
id: listId,
|
||||||
|
OR: [
|
||||||
|
{ ownerId: userId },
|
||||||
|
{
|
||||||
|
collaborators: {
|
||||||
|
some: {
|
||||||
|
userId: userId,
|
||||||
|
permission: 'editor',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!list) {
|
||||||
|
throw new Error('List not found or you do not have permission to update it')
|
||||||
|
}
|
||||||
|
|
||||||
// Validate and sanitize title if provided
|
// Validate and sanitize title if provided
|
||||||
if (data.title !== undefined) {
|
if (data.title !== undefined) {
|
||||||
const titleResult = validateListTitle(data.title)
|
const titleResult = validateListTitle(data.title)
|
||||||
@@ -428,7 +737,7 @@ export async function updateList(
|
|||||||
return updated
|
return updated
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating list:', error)
|
console.error('Error updating list:', error)
|
||||||
if (error instanceof Error && (error.message.includes('not found') || error.message.includes('required') || error.message.includes('cannot be') || error.message.includes('Invalid'))) {
|
if (error instanceof Error && (error.message.includes('not found') || error.message.includes('required') || error.message.includes('cannot be') || error.message.includes('Invalid') || error.message.includes('Unauthorized') || error.message.includes('permission'))) {
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
throw new Error('Failed to update list. Please try again.')
|
throw new Error('Failed to update list. Please try again.')
|
||||||
@@ -440,6 +749,14 @@ export async function searchItems(query: string, listId?: string) {
|
|||||||
if (!query || !query.trim()) {
|
if (!query || !query.trim()) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get current user session
|
||||||
|
const session = await auth()
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
throw new Error('Unauthorized: Please log in to search items')
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = session.user.id
|
||||||
|
|
||||||
const searchTerm = query.toLowerCase()
|
const searchTerm = query.toLowerCase()
|
||||||
|
|
||||||
@@ -465,6 +782,18 @@ export async function searchItems(query: string, listId?: string) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
list: {
|
||||||
|
OR: [
|
||||||
|
{ ownerId: userId },
|
||||||
|
{
|
||||||
|
collaborators: {
|
||||||
|
some: {
|
||||||
|
userId: userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add list scope if specified
|
// Add list scope if specified
|
||||||
@@ -485,15 +814,30 @@ export async function searchItems(query: string, listId?: string) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Also search in tags (stored as JSON)
|
// Also search in tags (stored as JSON)
|
||||||
|
const allItemsWhere: any = {
|
||||||
|
list: {
|
||||||
|
OR: [
|
||||||
|
{ ownerId: userId },
|
||||||
|
{
|
||||||
|
collaborators: {
|
||||||
|
some: {
|
||||||
|
userId: userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listId) {
|
||||||
|
allItemsWhere.listId = listId
|
||||||
|
}
|
||||||
|
|
||||||
const allItems = await retryOperation(() =>
|
const allItems = await retryOperation(() =>
|
||||||
listId
|
prisma.item.findMany({
|
||||||
? prisma.item.findMany({
|
where: allItemsWhere,
|
||||||
where: { listId },
|
include: { list: true },
|
||||||
include: { list: true },
|
})
|
||||||
})
|
|
||||||
: prisma.item.findMany({
|
|
||||||
include: { list: true },
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const tagMatches = allItems.filter((item) => {
|
const tagMatches = allItems.filter((item) => {
|
||||||
@@ -512,6 +856,9 @@ export async function searchItems(query: string, listId?: string) {
|
|||||||
return uniqueResults
|
return uniqueResults
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error searching items:', error)
|
console.error('Error searching items:', error)
|
||||||
|
if (error instanceof Error && error.message.includes('Unauthorized')) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
throw new Error('Failed to search items. Please try again.')
|
throw new Error('Failed to search items. Please try again.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -530,8 +877,29 @@ export async function getItemsByFilter(
|
|||||||
} = {}
|
} = {}
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
// Get current user session
|
||||||
|
const session = await auth()
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
throw new Error('Unauthorized: Please log in to filter items')
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = session.user.id
|
||||||
|
|
||||||
// Build the where clause with compound filters (AND logic)
|
// Build the where clause with compound filters (AND logic)
|
||||||
const whereClause: any = {}
|
const whereClause: any = {
|
||||||
|
list: {
|
||||||
|
OR: [
|
||||||
|
{ ownerId: userId },
|
||||||
|
{
|
||||||
|
collaborators: {
|
||||||
|
some: {
|
||||||
|
userId: userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
// Add list scope if specified
|
// Add list scope if specified
|
||||||
if (listId) {
|
if (listId) {
|
||||||
@@ -644,6 +1012,9 @@ export async function getItemsByFilter(
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error filtering items:', error)
|
console.error('Error filtering items:', error)
|
||||||
|
if (error instanceof Error && error.message.includes('Unauthorized')) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
throw new Error('Failed to filter items. Please try again.')
|
throw new Error('Failed to filter items. Please try again.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user