refactor: remove extension toggle system — compiled-in extensions are always active (#59)

The runtime toggle system (extension_toggles table, API routes, hooks, UI components)
added unnecessary complexity. Extensions controlled via extensions.config.json at build
time are now always active for all users. This removes ~835 lines of toggle-related code
including API routes, DB queries, the ExtensionToggleButton component, useEnabledExtensions
and useExtensionToggle hooks, and the toggle-check module. AI consent gating remains
unchanged.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-03-20 09:31:29 +01:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 38b658205d
commit cf77adaa0a
30 changed files with 115 additions and 833 deletions
@@ -1,7 +1,6 @@
import { createClient } from '@/lib/supabase/server'
import { redirect, notFound } from 'next/navigation'
import { getExtensionDefinition } from '@/lib/extensions/sectors'
import { LEGACY_GENERAL_EXTENSIONS } from '@/lib/extensions/toggle-check'
import ExtensionWorkspaceLoader from '@/components/extensions/ExtensionWorkspaceLoader'
export default async function ExtensionWorkspacePage({
@@ -18,21 +17,6 @@ export default async function ExtensionWorkspacePage({
const definition = getExtensionDefinition(sector, slug)
if (!definition) notFound()
// Check toggle — fall back to legacy list when no toggle row exists
const { data: toggle } = await supabase
.from('extension_toggles')
.select('enabled')
.eq('user_id', user.id)
.eq('sector_slug', sector)
.eq('extension_slug', slug)
.single()
const isEnabled = toggle
? toggle.enabled
: sector === 'general' && LEGACY_GENERAL_EXTENSIONS.includes(slug)
if (!isEnabled) redirect('/extensions')
return (
<ExtensionWorkspaceLoader
sector={sector}
@@ -3,7 +3,8 @@ import { getExtensionDefinition, getSector } from '@/lib/extensions/sectors'
import { resolveIcon } from '@/lib/extensions/icon-resolver'
import type { SectorSlug } from '@/lib/extensions/types'
import CategoryBadge from '@/components/extensions/CategoryBadge'
import ExtensionToggleButton from '@/components/extensions/ExtensionToggleButton'
import { WORKSPACES } from '@/lib/extensions/_generated/workspace-map'
import { Button } from '@/components/ui/button'
import Link from 'next/link'
export default async function ExtensionDetailPage({
@@ -17,9 +18,11 @@ export default async function ExtensionDetailPage({
if (!definition) notFound()
const sector = getSector(sectorSlug as SectorSlug)
const Icon = resolveIcon(definition.icon)
const hasWorkspace = `${sectorSlug}/${extensionSlug}` in WORKSPACES
const dataPatternLabels: Record<string, string> = {
core: 'Använder bokföringsdata',
manual: 'Manuell inmatning',
@@ -44,7 +47,7 @@ export default async function ExtensionDetailPage({
<span className="text-foreground">{definition.name}</span>
</nav>
{/* Header with toggle */}
{/* Header */}
<div className="flex items-start justify-between gap-4 mb-8">
<div className="flex items-start gap-4">
<div className="flex h-14 w-14 items-center justify-center rounded-xl bg-primary/10 flex-shrink-0">
@@ -58,11 +61,13 @@ export default async function ExtensionDetailPage({
</div>
</div>
</div>
<ExtensionToggleButton
sectorSlug={sectorSlug}
extensionSlug={extensionSlug}
subscriptionNotice={definition.subscriptionNotice}
/>
{hasWorkspace && (
<Button asChild>
<Link href={`/e/${sectorSlug}/${extensionSlug}`}>
Öppna
</Link>
</Button>
)}
</div>
{/* Details */}
+4 -20
View File
@@ -773,26 +773,10 @@ export default function ImportPage() {
}
}
}, [])
// If extension isn't compiled in, we know synchronously it's unavailable
const bankingCompiledIn = ENABLED_EXTENSION_IDS.has('enable-banking')
const [hasBankingExtension, setHasBankingExtension] = useState<boolean | null>(
bankingCompiledIn ? null : false
)
// Migration extension: show card when compiled in (no DB toggle needed,
// since the extensions marketplace is not exposed in the UI)
// Extensions are active if compiled in — no runtime toggle check needed
const hasBankingExtension = ENABLED_EXTENSION_IDS.has('enable-banking')
const hasMigrationExtension = ENABLED_EXTENSION_IDS.has('arcim-migration')
useEffect(() => {
if (!bankingCompiledIn) return
fetch('/api/extensions/toggles/general/enable-banking')
.then(res => res.ok ? res.json() : null)
.then(json => {
setHasBankingExtension(json?.data?.enabled || false)
})
.catch(() => setHasBankingExtension(false))
}, [bankingCompiledIn])
return (
<div className="space-y-6">
{/* Header */}
@@ -803,9 +787,9 @@ export default function ImportPage() {
</p>
</div>
{mode === null && hasBankingExtension !== null && (
{mode === null && (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{hasBankingExtension === true && (
{hasBankingExtension && (
<Card
role="button"
tabIndex={0}
+2 -13
View File
@@ -5,7 +5,6 @@ import { RecaptIdentify } from '@/components/RecaptIdentify'
import { SentryIdentify } from '@/components/SentryIdentify'
import { SandboxBanner } from '@/components/dashboard/SandboxBanner'
import { getExtensionNavItems } from '@/lib/extensions/sectors'
import { LEGACY_GENERAL_EXTENSIONS } from '@/lib/extensions/toggle-check'
import type { EntityType } from '@/types'
export default async function DashboardLayout({
@@ -21,7 +20,7 @@ export default async function DashboardLayout({
redirect('/login')
}
const [{ data: settings }, { count: uncategorizedCount }, { data: enabledToggles }] = await Promise.all([
const [{ data: settings }, { count: uncategorizedCount }] = await Promise.all([
supabase
.from('company_settings')
.select('company_name, onboarding_complete, entity_type, is_sandbox')
@@ -32,11 +31,6 @@ export default async function DashboardLayout({
.select('*', { count: 'exact', head: true })
.eq('user_id', user.id)
.is('is_business', null),
supabase
.from('extension_toggles')
.select('sector_slug, extension_slug')
.eq('user_id', user.id)
.eq('enabled', true),
])
if (!settings?.onboarding_complete) {
@@ -62,12 +56,7 @@ export default async function DashboardLayout({
entityType={entityType}
uncategorizedTransactionCount={uncategorizedCount ?? 0}
isSandbox={isSandbox}
extensionNavItems={getExtensionNavItems([
...(enabledToggles || []),
...LEGACY_GENERAL_EXTENSIONS
.filter(slug => !(enabledToggles || []).some(t => t.sector_slug === 'general' && t.extension_slug === slug))
.map(slug => ({ sector_slug: 'general', extension_slug: slug })),
])}
extensionNavItems={getExtensionNavItems()}
/>
<main id="main-content" className="safe-area-main-padding md:!pb-0 md:pl-[232px]" role="main">
<div className="max-w-5xl mx-auto px-5 py-8 md:px-8 md:py-10">
-9
View File
@@ -1,7 +1,6 @@
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
import DashboardContent from '@/components/dashboard/DashboardContent'
import { LEGACY_GENERAL_EXTENSIONS } from '@/lib/extensions/toggle-check'
import type { Deadline, ReceiptQueueSummary, OnboardingProgress } from '@/types'
export const dynamic = 'force-dynamic'
@@ -51,7 +50,6 @@ export default async function DashboardPage() {
{ count: postedEntriesCount },
{ data: entriesWithDocs },
{ data: recentReceiptActivity },
{ data: enabledToggles },
{ count: sieImportCount },
{ count: staleUncategorizedCount },
] = await Promise.all([
@@ -76,7 +74,6 @@ export default async function DashboardPage() {
supabase.from('journal_entries').select('*', { count: 'exact', head: true }).eq('user_id', user.id).eq('status', 'posted').in('source_type', needsDocSourceTypes),
supabase.from('document_attachments').select('journal_entry_id').eq('user_id', user.id).eq('is_current_version', true).not('journal_entry_id', 'is', null),
supabase.from('receipts').select('created_at').eq('user_id', user.id).eq('status', 'confirmed').order('created_at', { ascending: false }).limit(30),
supabase.from('extension_toggles').select('sector_slug, extension_slug').eq('user_id', user.id).eq('enabled', true),
supabase.from('sie_imports').select('*', { count: 'exact', head: true }).eq('user_id', user.id).eq('status', 'completed'),
supabase.from('transactions').select('*', { count: 'exact', head: true }).eq('user_id', user.id).is('journal_entry_id', null).not('is_business', 'eq', false).lt('date', new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]),
])
@@ -224,12 +221,6 @@ export default async function DashboardPage() {
staleUncategorizedCount: staleUncategorizedCount || 0,
}}
onboardingProgress={onboardingProgress}
enabledExtensions={[
...(enabledToggles || []),
...LEGACY_GENERAL_EXTENSIONS
.filter(slug => !(enabledToggles || []).some(t => t.sector_slug === 'general' && t.extension_slug === slug))
.map(slug => ({ sector_slug: 'general', extension_slug: slug })),
]}
/>
)
}
+4 -12
View File
@@ -1,7 +1,7 @@
'use client'
import dynamic from 'next/dynamic'
import { useExtensionToggle } from '@/lib/extensions/hooks'
import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Receipt, Loader2 } from 'lucide-react'
@@ -12,18 +12,10 @@ const ReceiptsPageOCR = dynamic(
{ loading: () => <div className="flex items-center justify-center h-64"><Loader2 className="h-8 w-8 animate-spin text-primary" /></div> }
)
const ocrEnabled = ENABLED_EXTENSION_IDS.has('receipt-ocr')
export default function ReceiptsPage() {
const { enabled, isLoading } = useExtensionToggle('general', 'receipt-ocr')
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
)
}
if (enabled) {
if (ocrEnabled) {
return <ReceiptsPageOCR />
}
+8 -11
View File
@@ -1,9 +1,9 @@
'use client'
import dynamic from 'next/dynamic'
import { useExtensionToggle } from '@/lib/extensions/hooks'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions'
import { Loader2 } from 'lucide-react'
const ScanReceiptPageOCR = dynamic(
@@ -11,22 +11,19 @@ const ScanReceiptPageOCR = dynamic(
{ loading: () => <div className="flex items-center justify-center h-64"><Loader2 className="h-8 w-8 animate-spin text-primary" /></div> }
)
const ocrEnabled = ENABLED_EXTENSION_IDS.has('receipt-ocr')
export default function ScanReceiptPage() {
const { enabled, isLoading } = useExtensionToggle('general', 'receipt-ocr')
const router = useRouter()
useEffect(() => {
if (!isLoading && !enabled) {
if (!ocrEnabled) {
router.replace('/receipts')
}
}, [isLoading, enabled, router])
}, [router])
if (isLoading || !enabled) {
return (
<div className="flex items-center justify-center h-64">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
)
if (!ocrEnabled) {
return null
}
return <ScanReceiptPageOCR />
+4 -35
View File
@@ -39,7 +39,6 @@ import { SecuritySettings } from '@/components/settings/SecuritySettings'
import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions'
const BankingPanel = getSettingsPanel('enable-banking')
const bankingCompiledIn = ENABLED_EXTENSION_IDS.has('enable-banking')
export default function SettingsPage() {
const router = useRouter()
@@ -50,10 +49,8 @@ export default function SettingsPage() {
const [isLoading, setIsLoading] = useState(true)
const [isSaving, setIsSaving] = useState(false)
const [settings, setSettings] = useState<CompanySettings | null>(null)
const [hasBankingExtension, setHasBankingExtension] = useState<boolean | null>(
bankingCompiledIn ? null : false
)
const [hasCalendarExtension, setHasCalendarExtension] = useState(false)
const hasBankingExtension = ENABLED_EXTENSION_IDS.has('enable-banking')
const hasCalendarExtension = ENABLED_EXTENSION_IDS.has('calendar')
const [bankConnectionError, setBankConnectionError] = useState<string | null>(null)
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
const [deleteConfirmText, setDeleteConfirmText] = useState('')
@@ -86,34 +83,10 @@ export default function SettingsPage() {
return
}
// Fetch settings and extension toggles in parallel
const [settingsRes, bankingToggleRes, calendarToggleRes] = await Promise.all([
supabase.from('company_settings').select('*').eq('user_id', user.id).single(),
fetch('/api/extensions/toggles/general/enable-banking').catch(() => null),
fetch('/api/extensions/toggles/general/calendar').catch(() => null),
])
const settingsRes = await supabase.from('company_settings').select('*').eq('user_id', user.id).single()
setSettings(settingsRes.data)
if (bankingToggleRes?.ok) {
const { data } = await bankingToggleRes.json()
setHasBankingExtension(data?.enabled || false)
} else {
// Fallback: check for existing bank connections
const { data: connections } = await supabase
.from('bank_connections')
.select('id')
.eq('user_id', user.id)
.eq('status', 'active')
.limit(1)
setHasBankingExtension((connections && connections.length > 0) || false)
}
if (calendarToggleRes?.ok) {
const { data } = await calendarToggleRes.json()
setHasCalendarExtension(data?.enabled || false)
}
setIsLoading(false)
}
@@ -570,11 +543,7 @@ export default function SettingsPage() {
</button>
</div>
)}
{hasBankingExtension === null ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : hasBankingExtension && BankingPanel ? (
{hasBankingExtension && BankingPanel ? (
<BankingPanel />
) : (
<Card>
+2 -2
View File
@@ -9,7 +9,7 @@ import { useToast } from '@/components/ui/use-toast'
import { Loader2 } from 'lucide-react'
import { cn } from '@/lib/utils'
import { validatePeriodDuration } from '@/lib/bookkeeping/validate-period-duration'
import { useExtensionToggle } from '@/lib/extensions/hooks'
import { ENABLED_EXTENSION_IDS } from '@/lib/extensions/_generated/enabled-extensions'
import type { CompanyLookupResult } from '@/lib/company-lookup/types'
import type { CompanySettings, EntityType, MomsPeriod } from '@/types'
@@ -76,7 +76,7 @@ function OnboardingPageContent() {
const [isSaving, setIsSaving] = useState(false)
const [currentStep, setCurrentStep] = useState(1)
const [settings, setSettings] = useState<Partial<CompanySettings>>({})
const { enabled: ticEnabled } = useExtensionToggle('general', 'tic')
const ticEnabled = ENABLED_EXTENSION_IDS.has('tic')
const [ticLookup, setTicLookup] = useState<CompanyLookupResult | null>(null)
const totalSteps = 5
@@ -15,10 +15,6 @@ vi.mock('@/lib/init', () => ({
ensureInitialized: vi.fn(),
}))
vi.mock('@/lib/extensions/toggle-check', () => ({
isExtensionEnabled: vi.fn(),
}))
vi.mock('@/lib/extensions/context-factory', () => ({
createExtensionContext: vi.fn().mockReturnValue({
userId: 'user-1',
@@ -28,11 +24,9 @@ vi.mock('@/lib/extensions/context-factory', () => ({
import { createClient } from '@/lib/supabase/server'
import { extensionRegistry } from '@/lib/extensions/registry'
import { isExtensionEnabled } from '@/lib/extensions/toggle-check'
import { GET, POST } from '../route'
const mockCreateClient = vi.mocked(createClient)
const mockIsEnabled = vi.mocked(isExtensionEnabled)
function createPathParams(path: string[]) {
return { params: Promise.resolve({ path }) }
@@ -89,29 +83,6 @@ describe('Extension Catch-All Route', () => {
expect(status).toBe(401)
})
it('returns 403 when extension is disabled', async () => {
extensionRegistry.register({
id: 'test-ext',
name: 'Test',
version: '1.0.0',
apiRoutes: [{ method: 'GET', path: '/data', handler: vi.fn() }],
})
const { supabase } = createQueuedMockSupabase()
supabase.auth.getUser.mockResolvedValue({
data: { user: { id: 'user-1' } },
error: null,
})
mockCreateClient.mockResolvedValue(supabase as never)
mockIsEnabled.mockResolvedValue(false)
const request = createMockRequest('/api/extensions/ext/test-ext/data')
const response = await GET(request, createPathParams(['test-ext', 'data']))
const { status } = await parseJsonResponse(response)
expect(status).toBe(403)
})
it('returns 404 for unmatched method/path', async () => {
extensionRegistry.register({
id: 'test-ext',
@@ -126,7 +97,6 @@ describe('Extension Catch-All Route', () => {
error: null,
})
mockCreateClient.mockResolvedValue(supabase as never)
mockIsEnabled.mockResolvedValue(true)
// GET doesn't match POST /data
const request = createMockRequest('/api/extensions/ext/test-ext/data')
@@ -154,7 +124,6 @@ describe('Extension Catch-All Route', () => {
error: null,
})
mockCreateClient.mockResolvedValue(supabase as never)
mockIsEnabled.mockResolvedValue(true)
const request = createMockRequest('/api/extensions/ext/enable-banking/banks')
const response = await GET(request, createPathParams(['enable-banking', 'banks']))
@@ -185,7 +154,6 @@ describe('Extension Catch-All Route', () => {
error: null,
})
mockCreateClient.mockResolvedValue(supabase as never)
mockIsEnabled.mockResolvedValue(true)
const request = createMockRequest('/api/extensions/ext/test-ext/connect', {
method: 'POST',
@@ -3,7 +3,6 @@ import { NextResponse } from 'next/server'
import { ensureInitialized } from '@/lib/init'
import { extensionRegistry } from '@/lib/extensions/registry'
import { createExtensionContext } from '@/lib/extensions/context-factory'
import { isExtensionEnabled } from '@/lib/extensions/toggle-check'
import { hasAiConsent, isAiExtension } from '@/lib/extensions/ai-consent'
import type { ApiRouteDefinition } from '@/lib/extensions/types'
@@ -118,13 +117,6 @@ async function handleRequest(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
// Toggle check — use extension's declared sector, fallback to 'general'
const sector = extension.sector || 'general'
const enabled = await isExtensionEnabled(user.id, sector, extensionId)
if (!enabled) {
return NextResponse.json({ error: 'Extension is disabled' }, { status: 403 })
}
// AI consent check
if (isAiExtension(extensionId)) {
const consented = await hasAiConsent(supabase, user.id, extensionId)
@@ -1,59 +0,0 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { LEGACY_GENERAL_EXTENSIONS } from '@/lib/extensions/toggle-check'
export async function GET(
_request: Request,
{ params }: { params: Promise<{ sector: string; slug: string }> }
) {
const { sector, slug } = await params
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { data } = await supabase
.from('extension_toggles')
.select('*')
.eq('user_id', user.id)
.eq('sector_slug', sector)
.eq('extension_slug', slug)
.single()
if (data) {
return NextResponse.json({ data })
}
// No toggle row: legacy general extensions default to enabled
const defaultEnabled =
sector === 'general' && LEGACY_GENERAL_EXTENSIONS.includes(slug)
return NextResponse.json({ data: { enabled: defaultEnabled } })
}
export async function DELETE(
_request: Request,
{ params }: { params: Promise<{ sector: string; slug: string }> }
) {
const { sector, slug } = await params
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { error } = await supabase
.from('extension_toggles')
.delete()
.eq('user_id', user.id)
.eq('sector_slug', sector)
.eq('extension_slug', slug)
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 })
}
return NextResponse.json({ success: true })
}
@@ -1,105 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import {
createMockRequest,
parseJsonResponse,
createQueuedMockSupabase,
makeExtensionToggle,
} from '@/tests/helpers'
const { supabase: mockSupabase, enqueue, reset } = createQueuedMockSupabase()
vi.mock('@/lib/supabase/server', () => ({
createClient: () => Promise.resolve(mockSupabase),
}))
import { GET, POST } from '../route'
describe('GET /api/extensions/toggles', () => {
const mockUser = { id: 'user-1', email: 'test@test.se' }
beforeEach(() => {
vi.clearAllMocks()
reset()
mockSupabase.auth.getUser.mockResolvedValue({ data: { user: mockUser } })
})
it('returns 401 when not authenticated', async () => {
mockSupabase.auth.getUser.mockResolvedValue({ data: { user: null } })
const response = await GET()
const { status, body } = await parseJsonResponse(response)
expect(status).toBe(401)
expect(body).toEqual({ error: 'Unauthorized' })
})
it('returns enabled toggles for user', async () => {
const toggles = [
makeExtensionToggle({ extension_slug: 'receipt-ocr' }),
makeExtensionToggle({ extension_slug: 'ai-chat' }),
]
enqueue({ data: toggles, error: null })
const response = await GET()
const { status, body } = await parseJsonResponse<{ data: unknown[] }>(response)
expect(status).toBe(200)
expect(body.data).toEqual(toggles)
expect(mockSupabase.from).toHaveBeenCalledWith('extension_toggles')
})
})
describe('POST /api/extensions/toggles', () => {
const mockUser = { id: 'user-1', email: 'test@test.se' }
beforeEach(() => {
vi.clearAllMocks()
reset()
mockSupabase.auth.getUser.mockResolvedValue({ data: { user: mockUser } })
})
it('returns 401 when not authenticated', async () => {
mockSupabase.auth.getUser.mockResolvedValue({ data: { user: null } })
const request = createMockRequest('/api/extensions/toggles', {
method: 'POST',
body: { sector_slug: 'general', extension_slug: 'receipt-ocr', enabled: true },
})
const response = await POST(request)
const { status, body } = await parseJsonResponse(response)
expect(status).toBe(401)
expect(body).toEqual({ error: 'Unauthorized' })
})
it('returns 400 when missing fields', async () => {
const request = createMockRequest('/api/extensions/toggles', {
method: 'POST',
body: { sector_slug: 'general' },
})
const response = await POST(request)
const { status, body } = await parseJsonResponse<{ error: string }>(response)
expect(status).toBe(400)
expect(body.error).toBe('sector_slug, extension_slug, and enabled are required')
})
it('upserts toggle and returns data', async () => {
const toggle = makeExtensionToggle({
sector_slug: 'general',
extension_slug: 'receipt-ocr',
enabled: true,
})
enqueue({ data: toggle, error: null })
const request = createMockRequest('/api/extensions/toggles', {
method: 'POST',
body: { sector_slug: 'general', extension_slug: 'receipt-ocr', enabled: true },
})
const response = await POST(request)
const { status, body } = await parseJsonResponse<{ data: unknown }>(response)
expect(status).toBe(200)
expect(body.data).toEqual(toggle)
expect(mockSupabase.from).toHaveBeenCalledWith('extension_toggles')
})
})
-62
View File
@@ -1,62 +0,0 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
export async function GET() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { data, error } = await supabase
.from('extension_toggles')
.select('*')
.eq('user_id', user.id)
.eq('enabled', true)
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 })
}
return NextResponse.json({ data })
}
export async function POST(request: Request) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const { sector_slug, extension_slug, enabled } = body
if (!sector_slug || !extension_slug || typeof enabled !== 'boolean') {
return NextResponse.json(
{ error: 'sector_slug, extension_slug, and enabled are required' },
{ status: 400 }
)
}
const { data, error } = await supabase
.from('extension_toggles')
.upsert(
{
user_id: user.id,
sector_slug,
extension_slug,
enabled,
},
{ onConflict: 'user_id,sector_slug,extension_slug' }
)
.select()
.single()
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 })
}
return NextResponse.json({ data })
}