Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
228 lines
5.5 KiB
TypeScript
228 lines
5.5 KiB
TypeScript
'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 }
|
|
}
|