98 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { createClient } from '@supabase/supabase-js';
import type { H3Event } from 'h3';
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.maxDevices,
});
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.maxDevices,
plan: profile?.plan ?? 'free',
devices,
},
});
}
throw err;
}
}