nyk
feat(refactor): ready for manual QA after main sync (#274)
b6ecafa unverified
Raw
History Blame Contribute Delete
6.27 kB
import { NextRequest, NextResponse } from 'next/server'
import { requireRole } from '@/lib/auth'
import { getDatabase } from '@/lib/db'
import { logger } from '@/lib/logger'
import { nextIncompleteStepIndex, parseCompletedSteps, shouldShowOnboarding, markStepCompleted } from '@/lib/onboarding-state'
const ONBOARDING_STEPS = [
{ id: 'welcome', title: 'Welcome' },
{ id: 'interface-mode', title: 'Interface' },
{ id: 'gateway-link', title: 'Gateway' },
{ id: 'credentials', title: 'Credentials' },
] as const
const ONBOARDING_SETTING_KEYS = {
completed: 'onboarding.completed',
completedAt: 'onboarding.completed_at',
skipped: 'onboarding.skipped',
completedSteps: 'onboarding.completed_steps',
checklistDismissed: 'onboarding.checklist_dismissed',
} as const
type OnboardingSettingKey = typeof ONBOARDING_SETTING_KEYS[keyof typeof ONBOARDING_SETTING_KEYS]
function scopedOnboardingKey(key: OnboardingSettingKey, username: string): string {
return `user.${username}.${key}`
}
function getOnboardingSetting(key: string): string {
try {
const db = getDatabase()
const row = db.prepare('SELECT value FROM settings WHERE key = ?').get(key) as { value: string } | undefined
return row?.value ?? ''
} catch {
return ''
}
}
function setOnboardingSetting(key: string, value: string, actor: string) {
const db = getDatabase()
db.prepare(`
INSERT INTO settings (key, value, description, category, updated_by, updated_at)
VALUES (?, ?, ?, 'onboarding', ?, unixepoch())
ON CONFLICT(key) DO UPDATE SET
value = excluded.value,
updated_by = excluded.updated_by,
updated_at = unixepoch()
`).run(key, value, `Onboarding: ${key}`, actor)
}
function readUserOnboardingSetting(key: OnboardingSettingKey, username: string): string {
const scopedValue = getOnboardingSetting(scopedOnboardingKey(key, username))
if (scopedValue !== '') return scopedValue
return getOnboardingSetting(key)
}
function writeUserOnboardingSetting(key: OnboardingSettingKey, value: string, actor: string) {
setOnboardingSetting(scopedOnboardingKey(key, actor), value, actor)
}
export async function GET(request: NextRequest) {
const auth = requireRole(request, 'viewer')
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
try {
const completed = readUserOnboardingSetting(ONBOARDING_SETTING_KEYS.completed, auth.user.username) === 'true'
const skipped = readUserOnboardingSetting(ONBOARDING_SETTING_KEYS.skipped, auth.user.username) === 'true'
const checklistDismissed = readUserOnboardingSetting(ONBOARDING_SETTING_KEYS.checklistDismissed, auth.user.username) === 'true'
const completedStepsRaw = readUserOnboardingSetting(ONBOARDING_SETTING_KEYS.completedSteps, auth.user.username)
const completedSteps = parseCompletedSteps(completedStepsRaw, ONBOARDING_STEPS)
const isAdmin = auth.user.role === 'admin'
const showOnboarding = shouldShowOnboarding({ completed, skipped, isAdmin })
const steps = ONBOARDING_STEPS.map((s) => ({
...s,
completed: completedSteps.includes(s.id),
}))
const currentStep = nextIncompleteStepIndex(ONBOARDING_STEPS, completedSteps)
return NextResponse.json({
showOnboarding,
completed,
skipped,
checklistDismissed,
isAdmin,
currentStep: currentStep === -1 ? steps.length - 1 : currentStep,
steps,
})
} catch (error) {
logger.error({ err: error }, 'Onboarding GET error')
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
export async function POST(request: NextRequest) {
const auth = requireRole(request, 'admin')
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
try {
const body = await request.json()
const { action, step } = body as { action: string; step?: string }
switch (action) {
case 'complete_step': {
if (!step) return NextResponse.json({ error: 'step is required' }, { status: 400 })
const valid = ONBOARDING_STEPS.some(s => s.id === step)
if (!valid) return NextResponse.json({ error: 'Invalid step' }, { status: 400 })
const raw = readUserOnboardingSetting(ONBOARDING_SETTING_KEYS.completedSteps, auth.user.username)
const parsed = parseCompletedSteps(raw, ONBOARDING_STEPS)
const steps = markStepCompleted(parsed, step, ONBOARDING_STEPS)
writeUserOnboardingSetting(ONBOARDING_SETTING_KEYS.completedSteps, JSON.stringify(steps), auth.user.username)
return NextResponse.json({ ok: true, completedSteps: steps })
}
case 'complete': {
writeUserOnboardingSetting(ONBOARDING_SETTING_KEYS.completed, 'true', auth.user.username)
writeUserOnboardingSetting(ONBOARDING_SETTING_KEYS.completedAt, String(Date.now()), auth.user.username)
return NextResponse.json({ ok: true })
}
case 'skip': {
writeUserOnboardingSetting(ONBOARDING_SETTING_KEYS.skipped, 'true', auth.user.username)
writeUserOnboardingSetting(ONBOARDING_SETTING_KEYS.completedAt, String(Date.now()), auth.user.username)
return NextResponse.json({ ok: true })
}
case 'dismiss_checklist': {
writeUserOnboardingSetting(ONBOARDING_SETTING_KEYS.checklistDismissed, 'true', auth.user.username)
return NextResponse.json({ ok: true })
}
case 'reset': {
writeUserOnboardingSetting(ONBOARDING_SETTING_KEYS.completed, 'false', auth.user.username)
writeUserOnboardingSetting(ONBOARDING_SETTING_KEYS.completedAt, '', auth.user.username)
writeUserOnboardingSetting(ONBOARDING_SETTING_KEYS.skipped, 'false', auth.user.username)
writeUserOnboardingSetting(ONBOARDING_SETTING_KEYS.completedSteps, '[]', auth.user.username)
writeUserOnboardingSetting(ONBOARDING_SETTING_KEYS.checklistDismissed, 'false', auth.user.username)
return NextResponse.json({ ok: true })
}
default:
return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
}
} catch (error) {
logger.error({ err: error }, 'Onboarding POST error')
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}