import { createClient } from '@supabase/supabase-js'; import type { H3Event } from 'h3'; import { isAdminUser } from '../db/admin'; import { findUserDevice, registerDevice, touchDevice } from '../db/devices'; import { getProfile } from '../db/profile'; import { getPlanLimits } from './plan-features'; const TOUCH_THROTTLE_MS = 60_000; // touch lastSeenAt höchstens 1×/min pro Device export interface RequireUserOptions { /** Bootstrap-Endpoints (z.B. /api/devices/register) brauchen kein Device-Binding */ skipDeviceCheck?: boolean; } export async function requireUser( event: H3Event, opts: RequireUserOptions = {}, ) { const authHeader = getHeader(event, 'authorization'); let token = authHeader?.replace('Bearer ', ''); if (!token) { const query = getQuery(event); token = query.token as string; } if (!token) { throw createError({ statusCode: 401, message: 'Nicht eingeloggt' }); } const config = useRuntimeConfig(event); const supabaseCfg = (config as any).public?.supabase ?? (config as any).supabase; const client = createClient( supabaseCfg.url as string, supabaseCfg.key as string, { global: { headers: { Authorization: `Bearer ${token}` } } }, ); const { data: { user }, error, } = await client.auth.getUser(); if (error || !user) { throw createError({ statusCode: 401, message: 'Nicht eingeloggt' }); } if (opts.skipDeviceCheck) return user; // Device-Binding: nur enforced wenn Client einen x-device-id Header schickt. // Web-Clients ohne Header laufen weiter wie bisher. const deviceId = getHeader(event, 'x-device-id'); if (!deviceId) return user; const existing = await findUserDevice(user.id, deviceId); if (existing) { // Touch lastSeenAt, throttled auf 1×/min — fire-and-forget if (Date.now() - existing.lastSeenAt.getTime() > TOUCH_THROTTLE_MS) { touchDevice(user.id, deviceId).catch(() => {}); } return user; } // Device unbekannt — Auto-Register (ohne Frontend-explicit-call) // Plan-Limit holen const profile = await getProfile(user.id); const limits = getPlanLimits(profile?.plan ?? 'free'); const platform = getHeader(event, 'x-platform') ?? 'unknown'; try { await registerDevice({ userId: user.id, deviceId, platform, maxDevices: limits.maxAppDevices, }); return user; } catch (err: any) { if (err.code === 'DEVICE_LIMIT_REACHED') { // Devices-Liste mitschicken damit das Frontend-Modal die Geräte // anzeigen + Freigeben-Buttons rendern kann (auch wenn der 403 // nicht vom register-Endpoint sondern vom auth-Middleware kommt). const { listUserDevices } = await import('../db/devices'); const devices = await listUserDevices(user.id); throw createError({ statusCode: 403, statusMessage: 'device_limit_reached', data: { error: 'device_limit_reached', max: limits.maxAppDevices, plan: profile?.plan ?? 'free', devices, }, }); } throw err; } } /** * requireAdmin — wirft 401 wenn nicht eingeloggt, 403 wenn nicht in admin_users. * Wraps requireUser mit skipDeviceCheck=true (Admin-App hat kein Device-Binding). */ export async function requireAdmin(event: H3Event) { const user = await requireUser(event, { skipDeviceCheck: true }); const admin = await isAdminUser(user.id); if (!admin) { throw createError({ statusCode: 403, message: 'Kein Admin-Zugang' }); } return user; }