fix(backend): backfill device name/model/osVersion on touch + auto-register
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 <noreply@anthropic.com>
This commit is contained in:
parent
e1ba0ebeaf
commit
60f608d891
@ -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<void> {
|
||||
/** 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<void> {
|
||||
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 */
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user