Implement permission utilities (task 9)
- Created lib/permissions.ts with four utility functions: - canAccessList: Check if user can view a list - canEditList: Check if user can edit a list or its items - canDeleteList: Check if user can delete a list (owner only) - getPermission: Get user's permission level for a list - Added comprehensive unit tests in lib/__tests__/permissions.test.ts - All 24 tests passing - Validates Requirements 4.3, 6.2, 6.3
This commit is contained in:
@@ -119,7 +119,7 @@
|
|||||||
- **Property 16: Account deletion cascades to lists**
|
- **Property 16: Account deletion cascades to lists**
|
||||||
- **Validates: Requirements 4.5**
|
- **Validates: Requirements 4.5**
|
||||||
|
|
||||||
- [ ] 9. Implement permission utilities
|
- [x] 9. Implement permission utilities
|
||||||
- Create canAccessList function
|
- Create canAccessList function
|
||||||
- Create canEditList function
|
- Create canEditList function
|
||||||
- Create canDeleteList function
|
- Create canDeleteList function
|
||||||
|
|||||||
268
lib/__tests__/permissions.test.ts
Normal file
268
lib/__tests__/permissions.test.ts
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
/**
|
||||||
|
* Tests for permission utility functions
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||||
|
import { prisma } from '../db'
|
||||||
|
import {
|
||||||
|
canAccessList,
|
||||||
|
canEditList,
|
||||||
|
canDeleteList,
|
||||||
|
getPermission,
|
||||||
|
} from '../permissions'
|
||||||
|
import bcrypt from 'bcryptjs'
|
||||||
|
|
||||||
|
describe('Permission Utilities', () => {
|
||||||
|
let testUser1Id: string
|
||||||
|
let testUser2Id: string
|
||||||
|
let testListId: string
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Create test users
|
||||||
|
const passwordHash = await bcrypt.hash('password123', 10)
|
||||||
|
|
||||||
|
const user1 = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
email: `test-perm-user1-${Date.now()}@example.com`,
|
||||||
|
passwordHash,
|
||||||
|
name: 'Test User 1',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
testUser1Id = user1.id
|
||||||
|
|
||||||
|
const user2 = await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
email: `test-perm-user2-${Date.now()}@example.com`,
|
||||||
|
passwordHash,
|
||||||
|
name: 'Test User 2',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
testUser2Id = user2.id
|
||||||
|
|
||||||
|
// Create a test list owned by user1
|
||||||
|
const list = await prisma.list.create({
|
||||||
|
data: {
|
||||||
|
title: 'Test List',
|
||||||
|
color: '#3B82F6',
|
||||||
|
ownerId: testUser1Id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
testListId = list.id
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
// Clean up test data
|
||||||
|
await prisma.list.deleteMany({
|
||||||
|
where: {
|
||||||
|
id: testListId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await prisma.user.deleteMany({
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
in: [testUser1Id, testUser2Id],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('canAccessList', () => {
|
||||||
|
it('should return true for list owner', async () => {
|
||||||
|
const result = await canAccessList(testUser1Id, testListId)
|
||||||
|
expect(result).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false for non-owner without collaboration', async () => {
|
||||||
|
const result = await canAccessList(testUser2Id, testListId)
|
||||||
|
expect(result).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return true for editor collaborator', async () => {
|
||||||
|
// Add user2 as editor
|
||||||
|
await prisma.collaborator.create({
|
||||||
|
data: {
|
||||||
|
userId: testUser2Id,
|
||||||
|
listId: testListId,
|
||||||
|
permission: 'editor',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await canAccessList(testUser2Id, testListId)
|
||||||
|
expect(result).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return true for viewer collaborator', async () => {
|
||||||
|
// Add user2 as viewer
|
||||||
|
await prisma.collaborator.create({
|
||||||
|
data: {
|
||||||
|
userId: testUser2Id,
|
||||||
|
listId: testListId,
|
||||||
|
permission: 'viewer',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await canAccessList(testUser2Id, testListId)
|
||||||
|
expect(result).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false for invalid user ID', async () => {
|
||||||
|
const result = await canAccessList('', testListId)
|
||||||
|
expect(result).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false for invalid list ID', async () => {
|
||||||
|
const result = await canAccessList(testUser1Id, '')
|
||||||
|
expect(result).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('canEditList', () => {
|
||||||
|
it('should return true for list owner', async () => {
|
||||||
|
const result = await canEditList(testUser1Id, testListId)
|
||||||
|
expect(result).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false for non-owner without collaboration', async () => {
|
||||||
|
const result = await canEditList(testUser2Id, testListId)
|
||||||
|
expect(result).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return true for editor collaborator', async () => {
|
||||||
|
// Add user2 as editor
|
||||||
|
await prisma.collaborator.create({
|
||||||
|
data: {
|
||||||
|
userId: testUser2Id,
|
||||||
|
listId: testListId,
|
||||||
|
permission: 'editor',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await canEditList(testUser2Id, testListId)
|
||||||
|
expect(result).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false for viewer collaborator', async () => {
|
||||||
|
// Add user2 as viewer
|
||||||
|
await prisma.collaborator.create({
|
||||||
|
data: {
|
||||||
|
userId: testUser2Id,
|
||||||
|
listId: testListId,
|
||||||
|
permission: 'viewer',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await canEditList(testUser2Id, testListId)
|
||||||
|
expect(result).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false for invalid user ID', async () => {
|
||||||
|
const result = await canEditList('', testListId)
|
||||||
|
expect(result).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false for invalid list ID', async () => {
|
||||||
|
const result = await canEditList(testUser1Id, '')
|
||||||
|
expect(result).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('canDeleteList', () => {
|
||||||
|
it('should return true for list owner', async () => {
|
||||||
|
const result = await canDeleteList(testUser1Id, testListId)
|
||||||
|
expect(result).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false for non-owner', async () => {
|
||||||
|
const result = await canDeleteList(testUser2Id, testListId)
|
||||||
|
expect(result).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false for editor collaborator', async () => {
|
||||||
|
// Add user2 as editor
|
||||||
|
await prisma.collaborator.create({
|
||||||
|
data: {
|
||||||
|
userId: testUser2Id,
|
||||||
|
listId: testListId,
|
||||||
|
permission: 'editor',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await canDeleteList(testUser2Id, testListId)
|
||||||
|
expect(result).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false for viewer collaborator', async () => {
|
||||||
|
// Add user2 as viewer
|
||||||
|
await prisma.collaborator.create({
|
||||||
|
data: {
|
||||||
|
userId: testUser2Id,
|
||||||
|
listId: testListId,
|
||||||
|
permission: 'viewer',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await canDeleteList(testUser2Id, testListId)
|
||||||
|
expect(result).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false for invalid user ID', async () => {
|
||||||
|
const result = await canDeleteList('', testListId)
|
||||||
|
expect(result).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false for invalid list ID', async () => {
|
||||||
|
const result = await canDeleteList(testUser1Id, '')
|
||||||
|
expect(result).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('getPermission', () => {
|
||||||
|
it('should return "owner" for list owner', async () => {
|
||||||
|
const result = await getPermission(testUser1Id, testListId)
|
||||||
|
expect(result).toBe('owner')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return null for non-owner without collaboration', async () => {
|
||||||
|
const result = await getPermission(testUser2Id, testListId)
|
||||||
|
expect(result).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return "editor" for editor collaborator', async () => {
|
||||||
|
// Add user2 as editor
|
||||||
|
await prisma.collaborator.create({
|
||||||
|
data: {
|
||||||
|
userId: testUser2Id,
|
||||||
|
listId: testListId,
|
||||||
|
permission: 'editor',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await getPermission(testUser2Id, testListId)
|
||||||
|
expect(result).toBe('editor')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return "viewer" for viewer collaborator', async () => {
|
||||||
|
// Add user2 as viewer
|
||||||
|
await prisma.collaborator.create({
|
||||||
|
data: {
|
||||||
|
userId: testUser2Id,
|
||||||
|
listId: testListId,
|
||||||
|
permission: 'viewer',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await getPermission(testUser2Id, testListId)
|
||||||
|
expect(result).toBe('viewer')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return null for invalid user ID', async () => {
|
||||||
|
const result = await getPermission('', testListId)
|
||||||
|
expect(result).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return null for invalid list ID', async () => {
|
||||||
|
const result = await getPermission(testUser1Id, '')
|
||||||
|
expect(result).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
164
lib/permissions.ts
Normal file
164
lib/permissions.ts
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
'use server'
|
||||||
|
|
||||||
|
import { prisma } from './db'
|
||||||
|
|
||||||
|
export type Permission = 'owner' | 'editor' | 'viewer'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a user can access a list (view it)
|
||||||
|
* Users can access a list if they are the owner or a collaborator
|
||||||
|
*
|
||||||
|
* @param userId - The ID of the user
|
||||||
|
* @param listId - The ID of the list
|
||||||
|
* @returns true if the user can access the list, false otherwise
|
||||||
|
*/
|
||||||
|
export async function canAccessList(
|
||||||
|
userId: string,
|
||||||
|
listId: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
if (!userId || !listId) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = await prisma.list.findFirst({
|
||||||
|
where: {
|
||||||
|
id: listId,
|
||||||
|
OR: [
|
||||||
|
{ ownerId: userId },
|
||||||
|
{
|
||||||
|
collaborators: {
|
||||||
|
some: {
|
||||||
|
userId: userId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return list !== null
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking list access:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a user can edit a list or its items
|
||||||
|
* Users can edit if they are the owner or have editor permission
|
||||||
|
*
|
||||||
|
* @param userId - The ID of the user
|
||||||
|
* @param listId - The ID of the list
|
||||||
|
* @returns true if the user can edit the list, false otherwise
|
||||||
|
*/
|
||||||
|
export async function canEditList(
|
||||||
|
userId: string,
|
||||||
|
listId: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
if (!userId || !listId) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = await prisma.list.findFirst({
|
||||||
|
where: {
|
||||||
|
id: listId,
|
||||||
|
OR: [
|
||||||
|
{ ownerId: userId },
|
||||||
|
{
|
||||||
|
collaborators: {
|
||||||
|
some: {
|
||||||
|
userId: userId,
|
||||||
|
permission: 'editor',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return list !== null
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking list edit permission:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a user can delete a list
|
||||||
|
* Only the owner can delete a list
|
||||||
|
*
|
||||||
|
* @param userId - The ID of the user
|
||||||
|
* @param listId - The ID of the list
|
||||||
|
* @returns true if the user can delete the list, false otherwise
|
||||||
|
*/
|
||||||
|
export async function canDeleteList(
|
||||||
|
userId: string,
|
||||||
|
listId: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
if (!userId || !listId) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = await prisma.list.findFirst({
|
||||||
|
where: {
|
||||||
|
id: listId,
|
||||||
|
ownerId: userId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return list !== null
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking list delete permission:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the permission level a user has for a list
|
||||||
|
*
|
||||||
|
* @param userId - The ID of the user
|
||||||
|
* @param listId - The ID of the list
|
||||||
|
* @returns The permission level ('owner', 'editor', 'viewer') or null if no access
|
||||||
|
*/
|
||||||
|
export async function getPermission(
|
||||||
|
userId: string,
|
||||||
|
listId: string
|
||||||
|
): Promise<Permission | null> {
|
||||||
|
try {
|
||||||
|
if (!userId || !listId) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user is the owner
|
||||||
|
const ownedList = await prisma.list.findFirst({
|
||||||
|
where: {
|
||||||
|
id: listId,
|
||||||
|
ownerId: userId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (ownedList) {
|
||||||
|
return 'owner'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user is a collaborator
|
||||||
|
const collaborator = await prisma.collaborator.findFirst({
|
||||||
|
where: {
|
||||||
|
userId: userId,
|
||||||
|
listId: listId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (collaborator) {
|
||||||
|
return collaborator.permission as Permission
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting user permission:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user