From 60f608d8914e173fc2a6cbdc4c9a83d8860a07de Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Fri, 15 May 2026 21:27:58 +0200 Subject: [PATCH] fix(backend): backfill device name/model/osVersion on touch + auto-register MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: existing devices registered before Frontend started sending x-device-name/ x-device-model/x-device-os headers stayed with NULL fields forever — DeviceLimit sheet shows only platform label ("iPhone" without iOS version, no name). Fix: - touchDevice() now accepts optional { name, model, osVersion } and updates these fields when headers are provided (existing-row backfill). - requireUser auth middleware reads URL-encoded x-device-* headers + passes them to both touchDevice() (existing) and registerDevice() (auto-register). After deploy: next authenticated request from updated client backfills the device record automatically (throttled per TOUCH_THROTTLE_MS = 1×/min). Co-Authored-By: Claude Opus 4.7 --- backend/server/db/devices.ts | 18 +++++++++++++++--- backend/server/utils/auth.ts | 22 ++++++++++++++++++++-- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/backend/server/db/devices.ts b/backend/server/db/devices.ts index 3678c76..960f4a0 100644 --- a/backend/server/db/devices.ts +++ b/backend/server/db/devices.ts @@ -161,13 +161,25 @@ export async function registerDevice(opts: { return { device: created, created: true }; } -/** Touch lastSeenAt — wird in der Auth-Middleware bei jedem Request aufgerufen. */ -export async function touchDevice(userId: string, deviceId: string): Promise { +/** Touch lastSeenAt — wird in der Auth-Middleware bei jedem Request aufgerufen. + * Optional: backfillt name/model/osVersion wenn Headers da sind (für Devices + * die unter altem Build registriert wurden ohne diese Info). */ +export async function touchDevice( + userId: string, + deviceId: string, + info?: { name?: string | null; model?: string | null; osVersion?: string | null }, +): Promise { const db = usePrisma(); + const data: { lastSeenAt: Date; name?: string; model?: string; osVersion?: string } = { + lastSeenAt: new Date(), + }; + if (info?.name) data.name = info.name; + if (info?.model) data.model = info.model; + if (info?.osVersion) data.osVersion = info.osVersion; await db.userDevice .updateMany({ where: { userId, deviceId }, - data: { lastSeenAt: new Date() }, + data, }) .catch(() => { /* race-safe: wenn Device gerade gelöscht wurde */ diff --git a/backend/server/utils/auth.ts b/backend/server/utils/auth.ts index 39e5d9e..9f6d3c3 100644 --- a/backend/server/utils/auth.ts +++ b/backend/server/utils/auth.ts @@ -53,11 +53,26 @@ export async function requireUser( const deviceId = getHeader(event, 'x-device-id'); if (!deviceId) return user; + // Frontend sendet name/model/osVersion als x-device-* Headers (URL-encoded) + function readEncoded(name: string): string | null { + const raw = getHeader(event, name); + if (!raw) return null; + try { return decodeURIComponent(raw); } catch { return raw; } + } + const deviceName = readEncoded('x-device-name'); + const deviceModel = readEncoded('x-device-model'); + const deviceOs = readEncoded('x-device-os'); + const existing = await findUserDevice(user.id, deviceId); if (existing) { - // Touch lastSeenAt, throttled auf 1×/min — fire-and-forget + // Touch lastSeenAt + Backfill name/model/osVersion falls noch nicht gesetzt, + // throttled auf 1×/min — fire-and-forget if (Date.now() - existing.lastSeenAt.getTime() > TOUCH_THROTTLE_MS) { - touchDevice(user.id, deviceId).catch(() => {}); + touchDevice(user.id, deviceId, { + name: deviceName, + model: deviceModel, + osVersion: deviceOs, + }).catch(() => {}); } return user; } @@ -73,6 +88,9 @@ export async function requireUser( userId: user.id, deviceId, platform, + name: deviceName, + model: deviceModel, + osVersion: deviceOs, maxDevices: limits.maxAppDevices, }); return user;