854 lines
17 KiB
Markdown
854 lines
17 KiB
Markdown
# Hackerspace.sh - API Reference
|
|
|
|
> **Last Updated**: 2026-03-10
|
|
> Complete reference for all server actions, database operations, and API patterns.
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [Server Actions Overview](#1-server-actions-overview)
|
|
2. [Authentication Actions](#2-authentication-actions)
|
|
3. [Task Actions](#3-task-actions)
|
|
4. [Project Actions](#4-project-actions)
|
|
5. [Member Actions](#5-member-actions)
|
|
6. [Payment Actions](#6-payment-actions)
|
|
7. [Knowledge Base Actions](#7-knowledge-base-actions)
|
|
8. [Contact Actions](#8-contact-actions)
|
|
9. [Settings Actions](#9-settings-actions)
|
|
10. [Data Fetching Patterns](#10-data-fetching-patterns)
|
|
11. [Real-time Subscriptions](#11-real-time-subscriptions)
|
|
12. [Error Handling](#12-error-handling)
|
|
|
|
---
|
|
|
|
## 1. Server Actions Overview
|
|
|
|
All mutations use Next.js Server Actions with `'use server'` directive. Actions:
|
|
- Authenticate via `supabase.auth.getUser()`
|
|
- Validate membership and roles
|
|
- Return `{ data?, error?, success? }` objects
|
|
- Call `revalidatePath()` for cache invalidation
|
|
|
|
### Import Pattern
|
|
|
|
```typescript
|
|
// In client components
|
|
import { createTask, claimTask } from '@/lib/actions'
|
|
import { signIn, signOut } from '@/lib/auth-actions'
|
|
|
|
// Usage
|
|
const result = await createTask({ title: 'Clean laser', type: 'chore' })
|
|
if (result.error) {
|
|
setError(result.error)
|
|
} else {
|
|
// Success - result.data contains the new task
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 2. Authentication Actions
|
|
|
|
**File**: `lib/auth-actions.ts`
|
|
|
|
### `signIn(email, password)`
|
|
|
|
Authenticates user with email/password.
|
|
|
|
```typescript
|
|
async function signIn(email: string, password: string): Promise<{
|
|
data?: AuthData
|
|
error?: string
|
|
}>
|
|
```
|
|
|
|
**Parameters**:
|
|
- `email` (string, required): User email
|
|
- `password` (string, required): User password
|
|
|
|
**Returns**:
|
|
- `{ data: AuthData }` on success
|
|
- `{ error: string }` on failure
|
|
|
|
**Example**:
|
|
```typescript
|
|
const result = await signIn('[email protected]', 'password123')
|
|
if (result.error) {
|
|
toast.error(result.error)
|
|
} else {
|
|
router.push('/dashboard')
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### `signUp(formData)`
|
|
|
|
Creates new account and optionally creates/joins a space.
|
|
|
|
```typescript
|
|
async function signUp(formData: {
|
|
email: string
|
|
password: string
|
|
fullName: string
|
|
action: 'create' | 'join'
|
|
spaceName?: string
|
|
spaceSlug?: string
|
|
city?: string
|
|
inviteCode?: string
|
|
}): Promise<{ data?: AuthData; error?: string }>
|
|
```
|
|
|
|
**Parameters**:
|
|
- `email` (string, required): User email
|
|
- `password` (string, required): Minimum 8 characters
|
|
- `fullName` (string, required): Display name
|
|
- `action` ('create' | 'join', required): Space action
|
|
- `spaceName` (string): Required if action='create'
|
|
- `spaceSlug` (string): Required if action='create'
|
|
- `city` (string, optional): Space location
|
|
- `inviteCode` (string): Required if action='join'
|
|
|
|
**Behavior**:
|
|
- Creates auth user with metadata
|
|
- DB trigger `handle_space_signup` creates space/member
|
|
- Returns session if email confirmation disabled
|
|
- Otherwise redirects to confirmation page
|
|
|
|
---
|
|
|
|
### `signOut()`
|
|
|
|
Signs out current user and redirects to login.
|
|
|
|
```typescript
|
|
async function signOut(): Promise<never>
|
|
```
|
|
|
|
**Behavior**:
|
|
- Calls `supabase.auth.signOut()`
|
|
- Redirects to `/login`
|
|
|
|
---
|
|
|
|
### `getUser()`
|
|
|
|
Gets current authenticated user.
|
|
|
|
```typescript
|
|
async function getUser(): Promise<User | null>
|
|
```
|
|
|
|
---
|
|
|
|
### `getCurrentMembership()`
|
|
|
|
Gets current user's space membership with space details.
|
|
|
|
```typescript
|
|
async function getCurrentMembership(): Promise<SpaceMember & { spaces: Space } | null>
|
|
```
|
|
|
|
---
|
|
|
|
### `createSpace(formData)`
|
|
|
|
Creates a new space for authenticated user.
|
|
|
|
```typescript
|
|
async function createSpace(formData: {
|
|
spaceName: string
|
|
spaceSlug: string
|
|
spaceCity?: string
|
|
displayName: string
|
|
}): Promise<{ success?: boolean; error?: string }>
|
|
```
|
|
|
|
**Role Required**: Authenticated (no existing space membership)
|
|
|
|
**Behavior**:
|
|
- Uses admin client (bypasses RLS)
|
|
- Creates space with auto-generated invite code
|
|
- Creates member as admin/plus/current
|
|
|
|
---
|
|
|
|
### `joinSpace(formData)`
|
|
|
|
Joins existing space via invite code.
|
|
|
|
```typescript
|
|
async function joinSpace(formData: {
|
|
inviteCode: string
|
|
displayName: string
|
|
}): Promise<{ success?: boolean; error?: string }>
|
|
```
|
|
|
|
**Role Required**: Authenticated
|
|
|
|
**Behavior**:
|
|
- Validates invite code
|
|
- Creates member with status based on `require_approval`
|
|
|
|
---
|
|
|
|
## 3. Task Actions
|
|
|
|
**File**: `lib/actions.ts`
|
|
|
|
### `createTask(formData)`
|
|
|
|
Creates a new task or chore.
|
|
|
|
```typescript
|
|
async function createTask(formData: {
|
|
title: string
|
|
description?: string
|
|
type: string // 'task' | 'chore'
|
|
area?: string
|
|
recurrence?: string // 'none' | 'daily' | 'weekly' | 'biweekly' | 'monthly'
|
|
due_date?: string // ISO date string
|
|
}): Promise<{ data?: Task; error?: string }>
|
|
```
|
|
|
|
**Role Required**: Member
|
|
|
|
**Behavior**:
|
|
- Sets `requested_by` to current user
|
|
- Sets `status` to 'open'
|
|
- Logs activity
|
|
- Revalidates `/tasks` and `/dashboard`
|
|
|
|
---
|
|
|
|
### `claimTask(taskId)`
|
|
|
|
Claims an open task.
|
|
|
|
```typescript
|
|
async function claimTask(taskId: string): Promise<{ success?: boolean; error?: string }>
|
|
```
|
|
|
|
**Role Required**: Member
|
|
|
|
**Behavior**:
|
|
- Sets `claimed_by` to current user
|
|
- Sets `status` to 'claimed'
|
|
- Logs activity
|
|
|
|
---
|
|
|
|
### `completeTask(taskId)`
|
|
|
|
Marks task as completed.
|
|
|
|
```typescript
|
|
async function completeTask(taskId: string): Promise<{ success?: boolean; error?: string }>
|
|
```
|
|
|
|
**Role Required**: Member (ideally claimant)
|
|
|
|
**Behavior**:
|
|
- Sets `status` to 'completed'
|
|
- Sets `completed_at` and `last_done_at`
|
|
- Logs activity
|
|
|
|
---
|
|
|
|
### `deleteTask(taskId)`
|
|
|
|
Deletes a task.
|
|
|
|
```typescript
|
|
async function deleteTask(taskId: string): Promise<{ success?: boolean; error?: string }>
|
|
```
|
|
|
|
**Role Required**: Member
|
|
|
|
---
|
|
|
|
## 4. Project Actions
|
|
|
|
### `createProject(formData)`
|
|
|
|
Creates a new project.
|
|
|
|
```typescript
|
|
async function createProject(formData: {
|
|
title: string
|
|
description?: string
|
|
area?: string
|
|
tags?: string[]
|
|
due_date?: string
|
|
}): Promise<{ data?: Project; error?: string }>
|
|
```
|
|
|
|
**Role Required**: Member
|
|
|
|
**Behavior**:
|
|
- Sets `status` to 'backlog'
|
|
- Logs activity
|
|
|
|
---
|
|
|
|
### `updateProjectStatus(projectId, status)`
|
|
|
|
Updates project kanban status.
|
|
|
|
```typescript
|
|
async function updateProjectStatus(
|
|
projectId: string,
|
|
status: string // 'backlog' | 'in_progress' | 'review' | 'done' | 'blocked'
|
|
): Promise<{ success?: boolean; error?: string }>
|
|
```
|
|
|
|
**Role Required**: Member
|
|
|
|
---
|
|
|
|
### `deleteProject(projectId)`
|
|
|
|
Deletes a project.
|
|
|
|
```typescript
|
|
async function deleteProject(projectId: string): Promise<{ success?: boolean; error?: string }>
|
|
```
|
|
|
|
**Role Required**: Member
|
|
|
|
---
|
|
|
|
## 5. Member Actions
|
|
|
|
### `addMember(formData)`
|
|
|
|
Manually adds a member to the space.
|
|
|
|
```typescript
|
|
async function addMember(formData: {
|
|
display_name: string
|
|
email: string
|
|
phone?: string
|
|
handle?: string
|
|
tier: string // 'plus' | 'basic' | 'associate'
|
|
role: string // 'admin' | 'board' | 'treasurer' | 'member' | 'associate'
|
|
joined_at?: string
|
|
has_card_access?: boolean
|
|
}): Promise<{ data?: SpaceMember; error?: string }>
|
|
```
|
|
|
|
**Role Required**: Admin or Board
|
|
|
|
**Note**: Creates member without user_id (offline member). User can claim via signup later.
|
|
|
|
---
|
|
|
|
### `updateMember(memberId, updates)`
|
|
|
|
Updates member details.
|
|
|
|
```typescript
|
|
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
|
|
}): Promise<{ success?: boolean; error?: string }>
|
|
```
|
|
|
|
**Role Required**: Admin or Board
|
|
|
|
---
|
|
|
|
### `approveMember(memberId)`
|
|
|
|
Approves a pending member.
|
|
|
|
```typescript
|
|
async function approveMember(memberId: string): Promise<{ success?: boolean; error?: string }>
|
|
```
|
|
|
|
**Role Required**: Admin or Board
|
|
|
|
**Behavior**:
|
|
- Sets `status` to 'current'
|
|
- Sets `approved` to true
|
|
- Logs activity
|
|
|
|
---
|
|
|
|
### `removeMember(memberId)`
|
|
|
|
Removes member from space.
|
|
|
|
```typescript
|
|
async function removeMember(memberId: string): Promise<{ success?: boolean; error?: string }>
|
|
```
|
|
|
|
**Role Required**: Admin only
|
|
|
|
---
|
|
|
|
### `importMembers(rows)`
|
|
|
|
Bulk import members from CSV data.
|
|
|
|
```typescript
|
|
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
|
|
}>): Promise<{ data?: SpaceMember[]; count?: number; error?: string }>
|
|
```
|
|
|
|
**Role Required**: Admin or Board
|
|
|
|
**Behavior**:
|
|
- Upserts on `(space_id, email)` to avoid duplicates
|
|
|
|
---
|
|
|
|
## 6. Payment Actions
|
|
|
|
### `logCashPayment(formData)`
|
|
|
|
Records a cash payment.
|
|
|
|
```typescript
|
|
async function logCashPayment(formData: {
|
|
amount: number
|
|
from_note: string
|
|
member_id?: string
|
|
transaction_date?: string
|
|
}): Promise<{ data?: Payment; error?: string }>
|
|
```
|
|
|
|
**Role Required**: Treasurer, Board, or Admin
|
|
|
|
**Behavior**:
|
|
- Sets `platform` to 'cash'
|
|
- If `member_id` provided, sets `link_status` to 'linked'
|
|
- Updates member's `last_paid_at` if linked
|
|
- Logs activity
|
|
|
|
---
|
|
|
|
### `linkPaymentToMember(paymentId, memberId)`
|
|
|
|
Links an unlinked payment to a member.
|
|
|
|
```typescript
|
|
async function linkPaymentToMember(
|
|
paymentId: string,
|
|
memberId: string
|
|
): Promise<{ success?: boolean; error?: string }>
|
|
```
|
|
|
|
**Role Required**: Treasurer, Board, or Admin
|
|
|
|
**Behavior**:
|
|
- Updates payment `member_id` and `link_status`
|
|
- Updates member `last_paid_at` and `payment_status`
|
|
|
|
---
|
|
|
|
### `importPaymentsCsv(rows)`
|
|
|
|
Bulk import payments.
|
|
|
|
```typescript
|
|
async function importPaymentsCsv(rows: Array<{
|
|
platform: string
|
|
amount: number
|
|
from_identifier: string
|
|
from_note?: string
|
|
transaction_date: string
|
|
}>): Promise<{ data?: Payment[]; count?: number; error?: string }>
|
|
```
|
|
|
|
**Role Required**: Treasurer, Board, or Admin
|
|
|
|
---
|
|
|
|
## 7. Knowledge Base Actions
|
|
|
|
### `createKbEntry(formData)`
|
|
|
|
Creates knowledge base entry.
|
|
|
|
```typescript
|
|
async function createKbEntry(formData: {
|
|
title: string
|
|
content: string
|
|
description?: string
|
|
area?: string
|
|
visibility?: string // 'all_members' | 'board' | 'admin_only'
|
|
is_pinned?: boolean
|
|
tags?: string[]
|
|
icon?: string
|
|
}): Promise<{ data?: KnowledgeBase; error?: string }>
|
|
```
|
|
|
|
**Role Required**: Member
|
|
|
|
---
|
|
|
|
### `updateKbEntry(entryId, updates)`
|
|
|
|
Updates knowledge base entry.
|
|
|
|
```typescript
|
|
async function updateKbEntry(entryId: string, updates: {
|
|
title?: string
|
|
content?: string
|
|
area?: string
|
|
visibility?: string
|
|
is_pinned?: boolean
|
|
tags?: string[]
|
|
}): Promise<{ success?: boolean; error?: string }>
|
|
```
|
|
|
|
**Role Required**: Member
|
|
|
|
---
|
|
|
|
### `deleteKbEntry(entryId)`
|
|
|
|
Deletes knowledge base entry.
|
|
|
|
```typescript
|
|
async function deleteKbEntry(entryId: string): Promise<{ success?: boolean; error?: string }>
|
|
```
|
|
|
|
**Role Required**: Board or Admin (via RLS)
|
|
|
|
---
|
|
|
|
### `createSecret(formData)`
|
|
|
|
Creates encrypted secret.
|
|
|
|
```typescript
|
|
async function createSecret(formData: {
|
|
title: string
|
|
value: string
|
|
description?: string
|
|
area?: string
|
|
icon?: string
|
|
}): Promise<{ data?: Secret; error?: string }>
|
|
```
|
|
|
|
**Role Required**: Admin or Board
|
|
|
|
---
|
|
|
|
### `deleteSecret(secretId)`
|
|
|
|
Deletes secret.
|
|
|
|
```typescript
|
|
async function deleteSecret(secretId: string): Promise<{ success?: boolean; error?: string }>
|
|
```
|
|
|
|
**Role Required**: Admin only
|
|
|
|
---
|
|
|
|
### `upsertAreaLead(formData)`
|
|
|
|
Creates or updates area lead assignment.
|
|
|
|
```typescript
|
|
async function upsertAreaLead(formData: {
|
|
area_code: string
|
|
area_name: string
|
|
lead_id?: string
|
|
lead_handle?: string
|
|
status?: string // 'active' | 'vacant' | 'handoff'
|
|
}): Promise<{ data?: AreaLead; error?: string }>
|
|
```
|
|
|
|
**Role Required**: Admin or Board
|
|
|
|
**Behavior**:
|
|
- Upserts on `(space_id, area_code)`
|
|
|
|
---
|
|
|
|
## 8. Contact Actions
|
|
|
|
### `createContact(formData)`
|
|
|
|
Creates a contact entry.
|
|
|
|
```typescript
|
|
async function createContact(formData: {
|
|
name: string
|
|
contact_type: string // 'vendor' | 'supplier' | 'partner' | 'landlord' | 'city'
|
|
email?: string
|
|
phone?: string
|
|
details?: string
|
|
note?: string
|
|
group_label?: string
|
|
tags?: string[]
|
|
}): Promise<{ data?: Contact; error?: string }>
|
|
```
|
|
|
|
**Role Required**: Member
|
|
|
|
**Behavior**:
|
|
- Auto-generates 3-letter code from name
|
|
|
|
---
|
|
|
|
### `updateContact(contactId, updates)`
|
|
|
|
Updates contact.
|
|
|
|
```typescript
|
|
async function updateContact(contactId: string, updates: {
|
|
name?: string
|
|
contact_type?: string
|
|
email?: string
|
|
phone?: string
|
|
details?: string
|
|
note?: string
|
|
group_label?: string
|
|
tags?: string[]
|
|
}): Promise<{ success?: boolean; error?: string }>
|
|
```
|
|
|
|
**Role Required**: Member
|
|
|
|
---
|
|
|
|
### `deleteContact(contactId)`
|
|
|
|
Deletes contact.
|
|
|
|
```typescript
|
|
async function deleteContact(contactId: string): Promise<{ success?: boolean; error?: string }>
|
|
```
|
|
|
|
**Role Required**: Member
|
|
|
|
---
|
|
|
|
## 9. Settings Actions
|
|
|
|
### `updateSpaceSettings(updates)`
|
|
|
|
Updates space configuration.
|
|
|
|
```typescript
|
|
async function updateSpaceSettings(updates: {
|
|
name?: string
|
|
slug?: string
|
|
city?: string
|
|
require_approval?: boolean
|
|
public_member_directory?: boolean
|
|
}): Promise<{ success?: boolean; error?: string }>
|
|
```
|
|
|
|
**Role Required**: Admin only
|
|
|
|
---
|
|
|
|
### `saveIntegration(platform, config)`
|
|
|
|
Saves integration configuration.
|
|
|
|
```typescript
|
|
async function saveIntegration(
|
|
platform: string,
|
|
config: Record<string, string>
|
|
): Promise<{ success?: boolean; error?: string }>
|
|
```
|
|
|
|
**Role Required**: Admin only
|
|
|
|
**Behavior**:
|
|
- Upserts on `(space_id, platform)`
|
|
- Stores config with `_set` indicators for each field
|
|
|
|
---
|
|
|
|
### `disconnectIntegration(platform)`
|
|
|
|
Disconnects/clears integration.
|
|
|
|
```typescript
|
|
async function disconnectIntegration(platform: string): Promise<{ success?: boolean; error?: string }>
|
|
```
|
|
|
|
**Role Required**: Admin only
|
|
|
|
---
|
|
|
|
### `rotateWebhookSecret()`
|
|
|
|
Generates new webhook secret.
|
|
|
|
```typescript
|
|
async function rotateWebhookSecret(): Promise<{ secret?: string; error?: string }>
|
|
```
|
|
|
|
**Role Required**: Admin only
|
|
|
|
**Returns**: New `whsec_` prefixed secret
|
|
|
|
---
|
|
|
|
## 10. Data Fetching Patterns
|
|
|
|
### Server Component Fetch
|
|
|
|
```typescript
|
|
// In page.tsx (Server Component)
|
|
import { createClient } from '@/lib/supabase/server'
|
|
|
|
export default async function TasksPage() {
|
|
const supabase = await createClient()
|
|
const { data: { user } } = await supabase.auth.getUser()
|
|
|
|
const { data: member } = await supabase
|
|
.from('space_members')
|
|
.select('space_id, display_name, role')
|
|
.eq('user_id', user!.id)
|
|
.eq('status', 'current')
|
|
.single()
|
|
|
|
const { data: tasks } = await supabase
|
|
.from('tasks')
|
|
.select('*')
|
|
.eq('space_id', member.space_id)
|
|
.order('created_at', { ascending: false })
|
|
|
|
return <TasksClient tasks={tasks ?? []} />
|
|
}
|
|
```
|
|
|
|
### Parallel Fetching
|
|
|
|
```typescript
|
|
const [
|
|
{ data: tasks },
|
|
{ data: projects },
|
|
{ count: memberCount }
|
|
] = await Promise.all([
|
|
supabase.from('tasks').select('*').eq('space_id', spaceId),
|
|
supabase.from('projects').select('*').eq('space_id', spaceId),
|
|
supabase.from('space_members').select('*', { count: 'exact', head: true }).eq('space_id', spaceId)
|
|
])
|
|
```
|
|
|
|
---
|
|
|
|
## 11. Real-time Subscriptions
|
|
|
|
### Comms Messages
|
|
|
|
```typescript
|
|
'use client'
|
|
import { createClient } from '@/lib/supabase/client'
|
|
import { useEffect, useState } from 'react'
|
|
|
|
function useChannelMessages(channelId: string) {
|
|
const [messages, setMessages] = useState([])
|
|
|
|
useEffect(() => {
|
|
const supabase = createClient()
|
|
|
|
// Initial fetch
|
|
supabase
|
|
.from('comms_messages')
|
|
.select('*')
|
|
.eq('channel_id', channelId)
|
|
.order('created_at')
|
|
.then(({ data }) => setMessages(data ?? []))
|
|
|
|
// Subscribe to new messages
|
|
const subscription = supabase
|
|
.channel(`messages:${channelId}`)
|
|
.on('postgres_changes', {
|
|
event: 'INSERT',
|
|
schema: 'public',
|
|
table: 'comms_messages',
|
|
filter: `channel_id=eq.${channelId}`
|
|
}, payload => {
|
|
setMessages(prev => [...prev, payload.new])
|
|
})
|
|
.subscribe()
|
|
|
|
return () => {
|
|
subscription.unsubscribe()
|
|
}
|
|
}, [channelId])
|
|
|
|
return messages
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 12. Error Handling
|
|
|
|
### Action Return Pattern
|
|
|
|
All actions return a consistent shape:
|
|
|
|
```typescript
|
|
// Success with data
|
|
{ data: T }
|
|
|
|
// Success without data
|
|
{ success: true }
|
|
|
|
// Error
|
|
{ error: string }
|
|
```
|
|
|
|
### Client-side Handling
|
|
|
|
```typescript
|
|
const [error, setError] = useState('')
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
async function handleSubmit() {
|
|
setLoading(true)
|
|
setError('')
|
|
|
|
const result = await createTask(formData)
|
|
|
|
if (result.error) {
|
|
setError(result.error)
|
|
} else {
|
|
// Success - close modal, refresh data, etc.
|
|
}
|
|
|
|
setLoading(false)
|
|
}
|
|
```
|
|
|
|
### Common Error Messages
|
|
|
|
| Error | Cause | Solution |
|
|
|-------|-------|----------|
|
|
| "Not authenticated" | No session | Redirect to login |
|
|
| "No active membership" | User has no space | Redirect to signup |
|
|
| "Admin access required" | Role check failed | Show permission error |
|
|
| "Treasurer access required" | Payment role check | Show permission error |
|
|
| "Invalid invite code" | Code not found | Ask user to verify |
|
|
| "A space with this URL slug already exists" | Duplicate slug | Suggest different slug |
|