Files
accounted/app/api/connector/sync/route.ts
T
Mattsson ee22c9c7b7 feat(connect): connector status + Synka nu row in Settings -> Abonnemang (PR6b-3) (#2104)
* feat(connect): connector status + Synka nu row in Settings -> Abonnemang (PR6b-3)

Self-host only: shows per-upstream connector mode, key prefix, and the
active company's granted capabilities from GET /api/connector/status,
plus a manual run of the entitlement sync via the new authed
POST /api/connector/sync (requireWrite, 60s cooldown) instead of
waiting for the hourly cron. Hidden on hosted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KAUKbGUGtAmRhpDKfn84F5

* fix(connect): handle sync fetch rejection, count capabilities not rows, not_configured toast

Skeptic findings on the Synka nu flow: a rejected fetch (instance
restarting) was a silent no-op with an unhandled rejection; the success
toast printed grant rows (companies x scopes) as capabilities; a
not_configured outcome claimed the hosted service was unreachable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KAUKbGUGtAmRhpDKfn84F5

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 11:44:33 +02:00

56 lines
2.1 KiB
TypeScript

import { NextResponse } from 'next/server'
import { withRouteContext } from '@/lib/api/with-route-context'
import { isSelfHosted } from '@/lib/env/public-flags'
import { createServiceClient } from '@/lib/supabase/server'
import { getConnectorConfig } from '@/lib/connect/instance/config'
import { syncConnectorEntitlements } from '@/lib/connect/instance/sync'
import {
endManualSync,
tryBeginManualSync,
} from '@/lib/connect/instance/manual-sync-throttle'
/**
* POST /api/connector/sync: operator-triggered run of the same entitlement
* sync the hourly self-hosted cron performs (/api/connector/sync/cron),
* for the "Synka nu" row in Settings -> Abonnemang. Runs with the service
* client because grants are instance-wide (every company on the instance),
* so the caller must hold a non-viewer role (requireWrite) and runs are
* cooldown-gated: each one reports to the hosted entitlements endpoint.
*
* Hosted (or an instance without a key) answers 200 not_configured rather
* than an error: the settings row is hidden there and this is the backstop.
*/
export const maxDuration = 60
export const POST = withRouteContext(
'connector.sync',
async (_request, { log }) => {
if (!isSelfHosted() || !getConnectorConfig()) {
return NextResponse.json({ data: { outcome: 'not_configured' } })
}
if (!tryBeginManualSync()) {
return NextResponse.json(
{
error: {
code: 'CONNECTOR_SYNC_COOLDOWN',
message: 'En synkronisering kördes nyss. Vänta en minut och försök igen.',
message_en: 'A sync just ran. Wait a minute and try again.',
},
},
{ status: 429, headers: { 'Retry-After': '60' } },
)
}
try {
const result = await syncConnectorEntitlements(createServiceClient(), {
instanceUrl: process.env.NEXT_PUBLIC_APP_URL?.trim() || null,
appVersion: process.env.npm_package_version ?? null,
})
log.info('manual connector sync run', { ...result })
return NextResponse.json({ data: result })
} finally {
endManualSync()
}
},
{ requireWrite: true },
)