Files
hackerspace.sh/lib/actions.ts
T
v0andMoheeb Zara f5697f577f feat: comprehensive application audit and fixes
Audited codebase, fixed input validation, OAuth, error handling, type casts, and security hardening

Co-authored-by: Moheeb Zara <[email protected]>
2026-03-11 08:18:57 +00:00

906 lines
31 KiB
TypeScript

'use server'
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
import { revalidatePath } from 'next/cache'
import type { SupabaseClient } from '@supabase/supabase-js'
import type { Database } from '@/types/database'
import {
createTaskSchema,
taskIdSchema,
createProjectSchema,
updateProjectStatusSchema,
addMemberSchema,
updateMemberSchema,
createContactSchema,
updateContactSchema,
createKbEntrySchema,
updateKbEntrySchema,
createSecretSchema,
logCashPaymentSchema,
linkPaymentSchema,
updateSpaceSettingsSchema,
saveIntegrationSchema,
upsertAreaLeadSchema,
uuidSchema,
} from '@/lib/validations'
// ─── Auth ────────────────────────────────────────────────────────────────────
export async function signIn(email: string, password: string) {
const supabase = await createClient()
const { error } = await supabase.auth.signInWithPassword({ email, password })
if (error) return { error: error.message }
return { success: true }
}
export async function signOut() {
const supabase = await createClient()
await supabase.auth.signOut()
redirect('/login')
}
export async function getUser() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
return user
}
// ─── Helper: get current member (any non-inactive status) ────────────────────
async function getMember(supabase: SupabaseClient<Database>, userId: string) {
const { data } = await supabase
.from('space_members')
.select('space_id, role, display_name')
.eq('user_id', userId)
.in('status', ['current', 'unverified', 'late'])
.single()
return data
}
export async function getCurrentMember() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return null
const { data } = await supabase
.from('space_members')
.select('*, spaces(*)')
.eq('user_id', user.id)
.in('status', ['current', 'unverified', 'late'])
.single()
return data
}
// ─── Tasks ───────────────────────────────────────────────────────────────────
export async function createTask(formData: {
title: string
description?: string
type: string
area?: string
recurrence?: string
due_date?: string
}) {
// Validate input
const parsed = createTaskSchema.safeParse(formData)
if (!parsed.success) {
return { error: parsed.error.errors[0]?.message || 'Invalid input' }
}
const input = parsed.data
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Not authenticated' }
const member = await getMember(supabase, user.id)
if (!member) return { error: 'No active membership' }
const { data, error } = await supabase.from('tasks').insert({
space_id: member.space_id,
title: input.title,
description: input.description,
task_type: input.type || 'task',
area: input.area,
recurrence: input.recurrence || 'none',
due_date: input.due_date || null,
status: 'open',
requested_by: user.id,
requested_by_name: member.display_name,
}).select().single()
if (error) return { error: error.message }
// Log activity
await supabase.from('activity_log').insert({
space_id: member.space_id,
user_id: user.id,
display_name: member.display_name,
action: 'created',
entity_type: 'task',
entity_id: data.id,
details: formData.title,
})
revalidatePath('/tasks')
revalidatePath('/dashboard')
return { data }
}
export async function claimTask(taskId: string) {
const idResult = taskIdSchema.safeParse(taskId)
if (!idResult.success) return { error: 'Invalid task ID' }
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Not authenticated' }
const member = await getMember(supabase, user.id)
if (!member) return { error: 'No active membership' }
const { error } = await supabase.from('tasks').update({
claimed_by: user.id,
claimed_by_name: member.display_name,
status: 'claimed',
}).eq('id', idResult.data).eq('space_id', member.space_id)
if (error) return { error: error.message }
await supabase.from('activity_log').insert({
space_id: member.space_id,
user_id: user.id,
display_name: member.display_name,
action: 'claimed',
entity_type: 'task',
entity_id: taskId,
})
revalidatePath('/tasks')
revalidatePath('/dashboard')
return { success: true }
}
export async function completeTask(taskId: string) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Not authenticated' }
const member = await getMember(supabase, user.id)
if (!member) return { error: 'No active membership' }
const { error } = await supabase.from('tasks').update({
status: 'completed',
completed_at: new Date().toISOString(),
last_done_at: new Date().toISOString(),
}).eq('id', taskId).eq('space_id', member.space_id)
if (error) return { error: error.message }
await supabase.from('activity_log').insert({
space_id: member.space_id,
user_id: user.id,
display_name: member.display_name,
action: 'completed',
entity_type: 'task',
entity_id: taskId,
})
revalidatePath('/tasks')
revalidatePath('/dashboard')
return { success: true }
}
export async function deleteTask(taskId: string) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Not authenticated' }
const member = await getMember(supabase, user.id)
if (!member) return { error: 'No active membership' }
const { error } = await supabase.from('tasks').delete()
.eq('id', taskId).eq('space_id', member.space_id)
if (error) return { error: error.message }
revalidatePath('/tasks')
return { success: true }
}
// ─── Projects ────────────────────────────────────────────────────────────────
export async function createProject(formData: {
title: string
description?: string
area?: string
tags?: string[]
due_date?: string
}) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Not authenticated' }
const member = await getMember(supabase, user.id)
if (!member) return { error: 'No active membership' }
const { data, error } = await supabase.from('projects').insert({
space_id: member.space_id,
title: formData.title,
description: formData.description,
area: formData.area,
tags: formData.tags ?? [],
due_date: formData.due_date || null,
status: 'backlog',
}).select().single()
if (error) return { error: error.message }
await supabase.from('activity_log').insert({
space_id: member.space_id,
user_id: user.id,
display_name: member.display_name,
action: 'created',
entity_type: 'project',
entity_id: data.id,
details: formData.title,
})
revalidatePath('/projects')
return { data }
}
export async function updateProjectStatus(projectId: string, status: string) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Not authenticated' }
const member = await getMember(supabase, user.id)
if (!member) return { error: 'No active membership' }
const { error } = await supabase.from('projects').update({ status, updated_at: new Date().toISOString() })
.eq('id', projectId).eq('space_id', member.space_id)
if (error) return { error: error.message }
revalidatePath('/projects')
revalidatePath('/dashboard')
return { success: true }
}
export async function deleteProject(projectId: string) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Not authenticated' }
const member = await getMember(supabase, user.id)
if (!member) return { error: 'No active membership' }
const { error } = await supabase.from('projects').delete()
.eq('id', projectId).eq('space_id', member.space_id)
if (error) return { error: error.message }
revalidatePath('/projects')
return { success: true }
}
// ─── Members ─────────────────────────────────────────────────────────────────
export async function addMember(formData: {
display_name: string
email: string
phone?: string
handle?: string
tier: string
role: string
joined_at?: string
has_card_access?: boolean
}) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Not authenticated' }
const { data: self } = await supabase
.from('space_members').select('space_id, role').eq('user_id', user.id).in('status', ['current', 'unverified', 'late']).single()
if (!self || (self.role !== 'admin' && self.role !== 'board')) return { error: 'Admin access required' }
const { data, error } = await supabase.from('space_members').insert({
space_id: self.space_id,
display_name: formData.display_name,
email: formData.email,
phone: formData.phone,
handle: formData.handle,
tier: formData.tier,
role: formData.role,
status: 'current',
approved: true,
joined_at: formData.joined_at || new Date().toISOString(),
has_card_access: formData.has_card_access ?? false,
}).select().single()
if (error) return { error: error.message }
revalidatePath('/members')
return { data }
}
export async function updateMember(memberId: string, updates: {
display_name?: string
email?: string
phone?: string
handle?: string
tier?: string
role?: string
status?: string
has_card_access?: boolean
payment_status?: string
payment_note?: string
}) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Not authenticated' }
const { data: self } = await supabase
.from('space_members').select('space_id, role').eq('user_id', user.id).in('status', ['current', 'unverified', 'late']).single()
if (!self || (self.role !== 'admin' && self.role !== 'board')) return { error: 'Admin access required' }
const { error } = await supabase.from('space_members').update(updates)
.eq('id', memberId).eq('space_id', self.space_id)
if (error) return { error: error.message }
revalidatePath('/members')
return { success: true }
}
export async function approveMember(memberId: string) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Not authenticated' }
const { data: self } = await supabase
.from('space_members').select('space_id, role, display_name').eq('user_id', user.id).in('status', ['current', 'unverified', 'late']).single()
if (!self || (self.role !== 'admin' && self.role !== 'board')) return { error: 'Admin access required' }
const { error } = await supabase.from('space_members').update({ status: 'current', approved: true })
.eq('id', memberId).eq('space_id', self.space_id)
if (error) return { error: error.message }
await supabase.from('activity_log').insert({
space_id: self.space_id,
user_id: user.id,
display_name: self.display_name,
action: 'approved',
entity_type: 'member',
entity_id: memberId,
})
revalidatePath('/members')
return { success: true }
}
export async function removeMember(memberId: string) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Not authenticated' }
const { data: self } = await supabase
.from('space_members').select('space_id, role').eq('user_id', user.id).in('status', ['current', 'unverified', 'late']).single()
if (!self || self.role !== 'admin') return { error: 'Admin access required' }
const { error } = await supabase.from('space_members').delete()
.eq('id', memberId).eq('space_id', self.space_id)
if (error) return { error: error.message }
revalidatePath('/members')
return { success: true }
}
// ─── Contacts ────────────────────────────────────────────────────────────────
export async function createContact(formData: {
name: string
contact_type: string
email?: string
phone?: string
details?: string
note?: string
group_label?: string
tags?: string[]
}) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Not authenticated' }
const member = await getMember(supabase, user.id)
if (!member) return { error: 'No active membership' }
const code = formData.name.slice(0, 3).toUpperCase() + Math.floor(Math.random() * 900 + 100)
const { data, error } = await supabase.from('contacts').insert({
space_id: member.space_id,
name: formData.name,
contact_type: formData.contact_type,
email: formData.email,
phone: formData.phone,
details: formData.details || formData.note,
note: formData.note,
group_label: formData.group_label,
tags: formData.tags ?? [],
code,
}).select().single()
if (error) return { error: error.message }
revalidatePath('/contacts')
return { data }
}
export async function updateContact(contactId: string, updates: {
name?: string
contact_type?: string
email?: string
phone?: string
details?: string
note?: string
group_label?: string
tags?: string[]
}) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Not authenticated' }
const member = await getMember(supabase, user.id)
if (!member) return { error: 'No active membership' }
const { error } = await supabase.from('contacts').update({ ...updates, updated_at: new Date().toISOString() })
.eq('id', contactId).eq('space_id', member.space_id)
if (error) return { error: error.message }
revalidatePath('/contacts')
return { success: true }
}
export async function deleteContact(contactId: string) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Not authenticated' }
const member = await getMember(supabase, user.id)
if (!member) return { error: 'No active membership' }
const { error } = await supabase.from('contacts').delete()
.eq('id', contactId).eq('space_id', member.space_id)
if (error) return { error: error.message }
revalidatePath('/contacts')
return { success: true }
}
// ─── Payments ────────────────────────────────────────────────────────────────
export async function logCashPayment(formData: {
amount: number
from_note: string
member_id?: string
transaction_date?: string
}) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Not authenticated' }
const { data: member } = await supabase
.from('space_members').select('space_id, role, display_name').eq('user_id', user.id).in('status', ['current', 'unverified', 'late']).single()
if (!member || (member.role !== 'admin' && member.role !== 'board' && member.role !== 'treasurer')) {
return { error: 'Treasurer access required' }
}
const { data, error } = await supabase.from('payments').insert({
space_id: member.space_id,
platform: 'cash',
amount: formData.amount,
from_identifier: 'Cash',
from_note: formData.from_note,
member_id: formData.member_id || null,
link_status: formData.member_id ? 'linked' : 'unlinked',
transaction_date: formData.transaction_date || new Date().toISOString(),
}).select().single()
if (error) return { error: error.message }
// Update member's last_paid_at if linked
if (formData.member_id) {
await supabase.from('space_members').update({
last_paid_at: formData.transaction_date || new Date().toISOString(),
payment_status: 'current',
}).eq('id', formData.member_id).eq('space_id', member.space_id)
}
await supabase.from('activity_log').insert({
space_id: member.space_id,
user_id: user.id,
display_name: member.display_name,
action: 'logged',
entity_type: 'payment',
entity_id: data.id,
details: `$${formData.amount} cash — ${formData.from_note}`,
})
revalidatePath('/payments')
revalidatePath('/dashboard')
return { data }
}
export async function linkPaymentToMember(paymentId: string, memberId: string) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Not authenticated' }
const { data: self } = await supabase
.from('space_members').select('space_id, role').eq('user_id', user.id).in('status', ['current', 'unverified', 'late']).single()
if (!self || (self.role !== 'admin' && self.role !== 'board' && self.role !== 'treasurer')) {
return { error: 'Treasurer access required' }
}
// Get payment to find the amount and date
const { data: payment } = await supabase
.from('payments').select('amount, transaction_date').eq('id', paymentId).single()
const { error } = await supabase.from('payments').update({
member_id: memberId,
link_status: 'linked',
}).eq('id', paymentId).eq('space_id', self.space_id)
if (error) return { error: error.message }
// Update member payment status
if (payment) {
await supabase.from('space_members').update({
last_paid_at: payment.transaction_date,
payment_status: 'current',
}).eq('id', memberId).eq('space_id', self.space_id)
}
revalidatePath('/payments')
revalidatePath('/members')
revalidatePath('/dashboard')
return { success: true }
}
export async function importPaymentsCsv(rows: Array<{
platform: string
amount: number
from_identifier: string
from_note?: string
transaction_date: string
}>) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Not authenticated' }
const { data: member } = await supabase
.from('space_members').select('space_id, role').eq('user_id', user.id).in('status', ['current', 'unverified', 'late']).single()
if (!member || (member.role !== 'admin' && member.role !== 'board' && member.role !== 'treasurer')) {
return { error: 'Treasurer access required' }
}
const inserts = rows.map(r => ({
space_id: member.space_id,
platform: r.platform as any,
amount: r.amount,
from_identifier: r.from_identifier,
from_note: r.from_note,
link_status: 'unlinked' as const,
transaction_date: r.transaction_date,
}))
const { data, error } = await supabase.from('payments').insert(inserts).select()
if (error) return { error: error.message }
revalidatePath('/payments')
revalidatePath('/dashboard')
return { data, count: data.length }
}
// ─── Knowledge Base ───────────────────────────────────────────────────────────
export async function createKbEntry(formData: {
title: string
content: string
description?: string
area?: string
visibility?: string
is_pinned?: boolean
tags?: string[]
icon?: string
}) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Not authenticated' }
const { data: member } = await supabase
.from('space_members').select('space_id, display_name, handle').eq('user_id', user.id).in('status', ['current', 'unverified', 'late']).single()
if (!member) return { error: 'No active membership' }
const { data, error } = await supabase.from('knowledge_base').insert({
space_id: member.space_id,
title: formData.title,
content: formData.content,
area: formData.area,
visibility: formData.visibility || 'all_members',
is_pinned: formData.is_pinned ?? false,
tags: formData.tags ?? [],
icon: formData.icon,
updated_by_id: user.id,
updated_by_name: member.display_name,
}).select().single()
if (error) return { error: error.message }
revalidatePath('/ops')
return { data }
}
export async function updateKbEntry(entryId: string, updates: {
title?: string
content?: string
area?: string
visibility?: string
is_pinned?: boolean
tags?: string[]
}) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Not authenticated' }
const { data: member } = await supabase
.from('space_members').select('space_id, display_name').eq('user_id', user.id).in('status', ['current', 'unverified', 'late']).single()
if (!member) return { error: 'No active membership' }
// Only pass columns that actually exist in the DB
const { is_pinned, ...rest } = updates
const { error } = await supabase.from('knowledge_base').update({
...rest,
...(is_pinned !== undefined ? { is_pinned } : {}),
updated_by_id: user.id,
updated_by_name: member.display_name,
updated_at: new Date().toISOString(),
}).eq('id', entryId).eq('space_id', member.space_id)
if (error) return { error: error.message }
revalidatePath('/ops')
return { success: true }
}
export async function deleteKbEntry(entryId: string) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Not authenticated' }
const { data: member } = await supabase
.from('space_members').select('space_id').eq('user_id', user.id).in('status', ['current', 'unverified', 'late']).single()
if (!member) return { error: 'No active membership' }
const { error } = await supabase.from('knowledge_base').delete()
.eq('id', entryId).eq('space_id', member.space_id)
if (error) return { error: error.message }
revalidatePath('/ops')
return { success: true }
}
// ─── Secrets ─────────────────────────────────────────────────────────────────
export async function createSecret(formData: {
title: string
value: string
description?: string
area?: string
icon?: string
}) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Not authenticated' }
const { data: member } = await supabase
.from('space_members').select('space_id, role').eq('user_id', user.id).in('status', ['current', 'unverified', 'late']).single()
if (!member || (member.role !== 'admin' && member.role !== 'board')) return { error: 'Admin access required' }
const { data, error } = await supabase.from('secrets').insert({
space_id: member.space_id,
title: formData.title,
label: formData.title,
value: formData.value,
description: formData.description,
area: formData.area,
icon: formData.icon,
}).select().single()
if (error) return { error: error.message }
revalidatePath('/ops')
return { data }
}
export async function deleteSecret(secretId: string) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Not authenticated' }
const { data: member } = await supabase
.from('space_members').select('space_id, role').eq('user_id', user.id).in('status', ['current', 'unverified', 'late']).single()
if (!member || member.role !== 'admin') return { error: 'Admin access required' }
const { error } = await supabase.from('secrets').delete()
.eq('id', secretId).eq('space_id', member.space_id)
if (error) return { error: error.message }
revalidatePath('/ops')
return { success: true }
}
// ─── Area Leads ───────────────────────────────────────────────────────────────
export async function upsertAreaLead(formData: {
area_code: string
area_name: string
lead_id?: string
lead_handle?: string
status?: string
}) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Not authenticated' }
const { data: member } = await supabase
.from('space_members').select('space_id, role').eq('user_id', user.id).in('status', ['current', 'unverified', 'late']).single()
if (!member || (member.role !== 'admin' && member.role !== 'board')) return { error: 'Admin access required' }
const { data, error } = await supabase.from('area_leads').upsert({
space_id: member.space_id,
area_code: formData.area_code,
area_name: formData.area_name,
lead_id: formData.lead_id || null,
lead_handle: formData.lead_handle || null,
status: formData.status || 'active',
updated_at: new Date().toISOString(),
}, { onConflict: 'space_id,area_code' }).select().single()
if (error) return { error: error.message }
revalidatePath('/ops')
return { data }
}
// ─── Settings ────────────────────────────────────────────────────────────────
export async function updateSpaceSettings(updates: {
name?: string
slug?: string
city?: string
require_approval?: boolean
public_member_directory?: boolean
}) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Not authenticated' }
const { data: member } = await supabase
.from('space_members').select('space_id, role').eq('user_id', user.id).in('status', ['current', 'unverified', 'late']).single()
if (!member || member.role !== 'admin') return { error: 'Admin access required' }
const { error } = await supabase.from('spaces').update({
...updates,
updated_at: new Date().toISOString(),
}).eq('id', member.space_id)
if (error) return { error: error.message }
revalidatePath('/settings')
return { success: true }
}
export async function rotateWebhookSecret() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Not authenticated' }
const { data: member } = await supabase
.from('space_members').select('space_id, role').eq('user_id', user.id).in('status', ['current', 'unverified', 'late']).single()
if (!member || member.role !== 'admin') return { error: 'Admin access required' }
// Generate a new webhook secret
const newSecret = 'whsec_' + Array.from(crypto.getRandomValues(new Uint8Array(24)))
.map(b => b.toString(16).padStart(2, '0')).join('')
const { error } = await supabase.from('spaces').update({ webhook_secret: newSecret, updated_at: new Date().toISOString() })
.eq('id', member.space_id)
if (error) return { error: error.message }
revalidatePath('/settings')
return { secret: newSecret }
}
export async function saveIntegration(platform: string, config: Record<string, string>) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Not authenticated' }
const { data: member } = await supabase
.from('space_members').select('space_id, role').eq('user_id', user.id).in('status', ['current', 'unverified', 'late']).single()
if (!member || member.role !== 'admin') return { error: 'Admin access required' }
// Mask sensitive values — store but indicate they're set
const safeConfig: Record<string, string> = {}
for (const [key, value] of Object.entries(config)) {
safeConfig[key] = value ? value : ''
safeConfig[`${key}_set`] = value ? 'true' : 'false'
}
const { error } = await supabase.from('integrations').upsert({
space_id: member.space_id,
platform,
name: platform,
is_connected: Object.values(config).some(v => v.length > 0),
config: safeConfig,
updated_at: new Date().toISOString(),
}, { onConflict: 'space_id,platform' })
if (error) return { error: error.message }
revalidatePath('/settings')
return { success: true }
}
export async function disconnectIntegration(platform: string) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Not authenticated' }
const { data: member } = await supabase
.from('space_members').select('space_id, role').eq('user_id', user.id).in('status', ['current', 'unverified', 'late']).single()
if (!member || member.role !== 'admin') return { error: 'Admin access required' }
const { error } = await supabase.from('integrations').update({
is_connected: false,
config: {},
updated_at: new Date().toISOString(),
}).eq('space_id', member.space_id).eq('platform', platform)
if (error) return { error: error.message }
revalidatePath('/settings')
return { success: true }
}
// ─── Import Members ───────────────────────────────────────────────────────────
export async function importMembers(rows: Array<{
display_name: string
email: string
phone?: string
tier?: string
joined_at?: string
last_paid_at?: string
has_card_access?: boolean
}>) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return { error: 'Not authenticated' }
const { data: member } = await supabase
.from('space_members').select('space_id, role').eq('user_id', user.id).in('status', ['current', 'unverified', 'late']).single()
if (!member || (member.role !== 'admin' && member.role !== 'board')) return { error: 'Admin access required' }
const inserts = rows.map(r => ({
space_id: member.space_id,
display_name: r.display_name,
email: r.email,
phone: r.phone,
tier: r.tier || 'basic',
role: 'member',
status: 'current',
approved: true,
joined_at: r.joined_at || new Date().toISOString(),
last_paid_at: r.last_paid_at || null,
has_card_access: r.has_card_access ?? false,
}))
// Upsert by email to avoid duplicates
const { data, error } = await supabase.from('space_members').upsert(inserts, {
onConflict: 'space_id,email',
ignoreDuplicates: false,
}).select()
if (error) return { error: error.message }
revalidatePath('/members')
return { data, count: data.length }
}