feat: [US-044] - Project sharing and collaborator management
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
46b681462d
commit
37031c3889
@@ -0,0 +1,227 @@
|
||||
'use server'
|
||||
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
|
||||
export type Collaborator = {
|
||||
id: string
|
||||
user_id: string
|
||||
role: 'owner' | 'editor' | 'viewer'
|
||||
invited_at: string
|
||||
accepted_at: string | null
|
||||
display_name: string | null
|
||||
email: string | null
|
||||
}
|
||||
|
||||
export async function getCollaborators(
|
||||
projectId: string
|
||||
): Promise<{ success: boolean; data?: Collaborator[]; error?: string }> {
|
||||
const supabase = await createClient()
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
|
||||
if (!user) {
|
||||
return { success: false, error: 'Not authenticated' }
|
||||
}
|
||||
|
||||
// Verify user owns the project
|
||||
const { data: project } = await supabase
|
||||
.from('projects')
|
||||
.select('id, user_id')
|
||||
.eq('id', projectId)
|
||||
.single()
|
||||
|
||||
if (!project) {
|
||||
return { success: false, error: 'Project not found' }
|
||||
}
|
||||
|
||||
const isOwner = project.user_id === user.id
|
||||
|
||||
// Check if user is a collaborator
|
||||
if (!isOwner) {
|
||||
const { data: collab } = await supabase
|
||||
.from('project_collaborators')
|
||||
.select('id')
|
||||
.eq('project_id', projectId)
|
||||
.eq('user_id', user.id)
|
||||
.single()
|
||||
|
||||
if (!collab) {
|
||||
return { success: false, error: 'Access denied' }
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch collaborators with profile info
|
||||
const { data: collaborators, error } = await supabase
|
||||
.from('project_collaborators')
|
||||
.select('id, user_id, role, invited_at, accepted_at, profiles(display_name, email)')
|
||||
.eq('project_id', projectId)
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
|
||||
const result: Collaborator[] = (collaborators || []).map((c) => {
|
||||
const profile = c.profiles as unknown as { display_name: string | null; email: string | null } | null
|
||||
return {
|
||||
id: c.id,
|
||||
user_id: c.user_id,
|
||||
role: c.role as 'owner' | 'editor' | 'viewer',
|
||||
invited_at: c.invited_at,
|
||||
accepted_at: c.accepted_at,
|
||||
display_name: profile?.display_name || null,
|
||||
email: profile?.email || null,
|
||||
}
|
||||
})
|
||||
|
||||
return { success: true, data: result }
|
||||
}
|
||||
|
||||
export async function inviteCollaborator(
|
||||
projectId: string,
|
||||
email: string,
|
||||
role: 'editor' | 'viewer'
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
const supabase = await createClient()
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
|
||||
if (!user) {
|
||||
return { success: false, error: 'Not authenticated' }
|
||||
}
|
||||
|
||||
// Verify user owns the project
|
||||
const { data: project } = await supabase
|
||||
.from('projects')
|
||||
.select('id, user_id')
|
||||
.eq('id', projectId)
|
||||
.eq('user_id', user.id)
|
||||
.single()
|
||||
|
||||
if (!project) {
|
||||
return { success: false, error: 'Only the project owner can invite collaborators' }
|
||||
}
|
||||
|
||||
// Find the user by email in profiles
|
||||
const { data: targetProfile } = await supabase
|
||||
.from('profiles')
|
||||
.select('id, email')
|
||||
.eq('email', email)
|
||||
.single()
|
||||
|
||||
if (!targetProfile) {
|
||||
return { success: false, error: 'No user found with that email address' }
|
||||
}
|
||||
|
||||
// Cannot invite yourself
|
||||
if (targetProfile.id === user.id) {
|
||||
return { success: false, error: 'You cannot invite yourself' }
|
||||
}
|
||||
|
||||
// Check if already a collaborator
|
||||
const { data: existing } = await supabase
|
||||
.from('project_collaborators')
|
||||
.select('id')
|
||||
.eq('project_id', projectId)
|
||||
.eq('user_id', targetProfile.id)
|
||||
.single()
|
||||
|
||||
if (existing) {
|
||||
return { success: false, error: 'This user is already a collaborator' }
|
||||
}
|
||||
|
||||
// Insert collaborator
|
||||
const { error: insertError } = await supabase
|
||||
.from('project_collaborators')
|
||||
.insert({
|
||||
project_id: projectId,
|
||||
user_id: targetProfile.id,
|
||||
role,
|
||||
invited_at: new Date().toISOString(),
|
||||
accepted_at: new Date().toISOString(), // Auto-accept for now
|
||||
})
|
||||
|
||||
if (insertError) {
|
||||
return { success: false, error: insertError.message }
|
||||
}
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function updateCollaboratorRole(
|
||||
projectId: string,
|
||||
collaboratorId: string,
|
||||
role: 'editor' | 'viewer'
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
const supabase = await createClient()
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
|
||||
if (!user) {
|
||||
return { success: false, error: 'Not authenticated' }
|
||||
}
|
||||
|
||||
// Verify user owns the project
|
||||
const { data: project } = await supabase
|
||||
.from('projects')
|
||||
.select('id, user_id')
|
||||
.eq('id', projectId)
|
||||
.eq('user_id', user.id)
|
||||
.single()
|
||||
|
||||
if (!project) {
|
||||
return { success: false, error: 'Only the project owner can change roles' }
|
||||
}
|
||||
|
||||
const { error } = await supabase
|
||||
.from('project_collaborators')
|
||||
.update({ role })
|
||||
.eq('id', collaboratorId)
|
||||
.eq('project_id', projectId)
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
export async function removeCollaborator(
|
||||
projectId: string,
|
||||
collaboratorId: string
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
const supabase = await createClient()
|
||||
const {
|
||||
data: { user },
|
||||
} = await supabase.auth.getUser()
|
||||
|
||||
if (!user) {
|
||||
return { success: false, error: 'Not authenticated' }
|
||||
}
|
||||
|
||||
// Verify user owns the project
|
||||
const { data: project } = await supabase
|
||||
.from('projects')
|
||||
.select('id, user_id')
|
||||
.eq('id', projectId)
|
||||
.eq('user_id', user.id)
|
||||
.single()
|
||||
|
||||
if (!project) {
|
||||
return { success: false, error: 'Only the project owner can remove collaborators' }
|
||||
}
|
||||
|
||||
const { error } = await supabase
|
||||
.from('project_collaborators')
|
||||
.delete()
|
||||
.eq('id', collaboratorId)
|
||||
.eq('project_id', projectId)
|
||||
|
||||
if (error) {
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
Reference in New Issue
Block a user