2025-11-17 15:26:43 +01:00

387 lines
11 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.

// app/api/devices/[id]/route.ts
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import type { Prisma } from '@prisma/client';
import { getCurrentUserId } from '@/lib/auth';
import type { Server as IOServer } from 'socket.io';
type RouteParams = { id: string };
// in Next 15+ ist params ein Promise
type RouteContext = { params: Promise<RouteParams> };
export async function GET(
_req: Request,
ctx: RouteContext,
) {
// params-Promise auflösen
const { id } = await ctx.params;
if (!id) {
return NextResponse.json({ error: 'MISSING_ID' }, { status: 400 });
}
try {
const device = await prisma.device.findUnique({
where: { inventoryNumber: id },
include: {
group: true,
location: true,
tags: true, // 🔹 NEU
},
});
if (!device) {
return NextResponse.json({ error: 'NOT_FOUND' }, { status: 404 });
}
return NextResponse.json({
inventoryNumber: device.inventoryNumber,
name: device.name,
manufacturer: device.manufacturer,
model: device.model,
serialNumber: device.serialNumber,
productNumber: device.productNumber,
comment: device.comment,
ipv4Address: device.ipv4Address,
ipv6Address: device.ipv6Address,
macAddress: device.macAddress,
username: device.username,
// passwordHash bewusst weggelassen
group: device.group?.name ?? null,
location: device.location?.name ?? null,
tags: device.tags.map((t) => t.name), // 🔹
createdAt: device.createdAt.toISOString(),
updatedAt: device.updatedAt.toISOString(),
});
} catch (err) {
console.error('[GET /api/devices/[id]]', err);
return NextResponse.json({ error: 'INTERNAL_ERROR' }, { status: 500 });
}
}
export async function PATCH(
req: Request,
ctx: RouteContext,
) {
const { id } = await ctx.params;
const body = await req.json();
if (!id) {
return NextResponse.json({ error: 'MISSING_ID' }, { status: 400 });
}
try {
// ⬇️ hier jetzt die Request-Header durchreichen
const userId = await getCurrentUserId();
// aktuelles Gerät inkl. Relations laden (für "before"-Snapshot)
const existing = await prisma.device.findUnique({
where: { inventoryNumber: id },
include: {
group: true,
location: true,
tags: true, // 🔹 NEU
},
});
if (!existing) {
return NextResponse.json({ error: 'NOT_FOUND' }, { status: 404 });
}
// Basis-Update-Daten
const data: Prisma.DeviceUpdateInput = {
name: body.name,
manufacturer: body.manufacturer,
model: body.model,
serialNumber: body.serialNumber,
productNumber: body.productNumber,
comment: body.comment,
ipv4Address: body.ipv4Address,
ipv6Address: body.ipv6Address,
macAddress: body.macAddress,
username: body.username,
passwordHash: body.passwordHash,
};
// Tags aus dem Body bereinigen
const incomingTags: string[] = Array.isArray(body.tags)
? body.tags.map((t: unknown) => String(t).trim()).filter(Boolean)
: [];
// existierende Tag-Namen
const existingTagNames = existing.tags.map((t) => t.name);
// welche sollen entfernt werden?
const tagsToRemove = existingTagNames.filter(
(name) => !incomingTags.includes(name),
);
// welche sind neu hinzuzufügen?
const tagsToAdd = incomingTags.filter(
(name) => !existingTagNames.includes(name),
);
if (!data.tags) {
data.tags = {};
}
// Tags, die nicht mehr vorkommen → disconnect über name (weil @unique)
if (tagsToRemove.length > 0) {
(data.tags as any).disconnect = tagsToRemove.map((name) => ({ name }));
}
// neue Tags → connectOrCreate (Tag wird bei Bedarf angelegt)
if (tagsToAdd.length > 0) {
(data.tags as any).connectOrCreate = tagsToAdd.map((name) => ({
where: { name },
create: { name },
}));
}
// updatedBy setzen, wenn User da
if (userId) {
data.updatedBy = {
connect: { id: userId },
};
}
// Gruppe (per Name) DeviceGroup.name ist @unique
if (body.group != null && body.group !== '') {
data.group = {
connectOrCreate: {
where: { name: body.group },
create: { name: body.group },
},
};
} else {
data.group = { disconnect: true };
}
// Standort (per Name) Location.name ist @unique
if (body.location != null && body.location !== '') {
data.location = {
connectOrCreate: {
where: { name: body.location },
create: { name: body.location },
},
};
} else {
data.location = { disconnect: true };
}
// Tags (Many-to-Many via Tag.name @unique)
if (Array.isArray(body.tags)) {
const tagNames = (body.tags as string[])
.map((t) => String(t).trim())
.filter(Boolean);
const beforeNames = existing.tags.map((t) => t.name.toLowerCase());
const normalized = tagNames.map((n) => n.toLowerCase());
const toConnect = tagNames.filter(
(n) => !beforeNames.includes(n.toLowerCase()),
);
const toDisconnect = existing.tags
.map((t) => t.name)
.filter((n) => !normalized.includes(n.toLowerCase()));
data.tags = {
// neue / fehlende Tags verknüpfen (und ggf. anlegen)
connectOrCreate: toConnect.map((name) => ({
where: { name },
create: { name },
})),
// nicht mehr vorhandene Tags trennen
disconnect: toDisconnect.map((name) => ({ name })),
};
}
// Update durchführen (für "after"-Snapshot)
const updated = await prisma.device.update({
where: { inventoryNumber: id },
data,
include: {
group: true,
location: true,
tags: true,
},
});
// Felder, die wir tracken wollen
const trackedFields = [
'name',
'manufacturer',
'model',
'serialNumber',
'productNumber',
'comment',
'ipv4Address',
'ipv6Address',
'macAddress',
'username',
'passwordHash',
] as const;
type TrackedField = (typeof trackedFields)[number];
// explizit JSON-kompatible Types (string | null)
const changes: {
field: TrackedField | 'group' | 'location' | 'tags';
before: string | null;
after: string | null;
}[] = [];
// einfache Feld-Diffs
for (const field of trackedFields) {
const before = (existing as any)[field] as string | null;
const after = (updated as any)[field] as string | null;
if (before !== after) {
changes.push({ field, before, after });
}
}
// group / location per Name vergleichen
const beforeGroup = (existing.group?.name ?? null) as string | null;
const afterGroup = (updated.group?.name ?? null) as string | null;
if (beforeGroup !== afterGroup) {
changes.push({
field: 'group',
before: beforeGroup,
after: afterGroup,
});
}
const beforeLocation = (existing.location?.name ?? null) as string | null;
const afterLocation = (updated.location?.name ?? null) as string | null;
if (beforeLocation !== afterLocation) {
changes.push({
field: 'location',
before: beforeLocation,
after: afterLocation,
});
}
// Tags vergleichen (als kommagetrennte Liste)
const beforeTagsList = existing.tags.map((t) => t.name).sort();
const afterTagsList = updated.tags.map((t) => t.name).sort();
const beforeTags =
beforeTagsList.length > 0 ? beforeTagsList.join(', ') : null;
const afterTags =
afterTagsList.length > 0 ? afterTagsList.join(', ') : null;
if (beforeTags !== afterTags) {
changes.push({
field: 'tags',
before: beforeTags,
after: afterTags,
});
}
// Falls sich *gar nichts* geändert hat, kein History-Eintrag
if (changes.length > 0) {
const snapshot: Prisma.JsonObject = {
before: {
inventoryNumber: existing.inventoryNumber,
name: existing.name,
manufacturer: existing.manufacturer,
model: existing.model,
serialNumber: existing.serialNumber,
productNumber: existing.productNumber,
comment: existing.comment,
ipv4Address: existing.ipv4Address,
ipv6Address: existing.ipv6Address,
macAddress: existing.macAddress,
username: existing.username,
passwordHash: existing.passwordHash,
group: existing.group?.name ?? null,
location: existing.location?.name ?? null,
tags: existing.tags.map((t) => t.name),
createdAt: existing.createdAt.toISOString(),
updatedAt: existing.updatedAt.toISOString(),
},
after: {
inventoryNumber: updated.inventoryNumber,
name: updated.name,
manufacturer: updated.manufacturer,
model: updated.model,
serialNumber: updated.serialNumber,
productNumber: updated.productNumber,
comment: updated.comment,
ipv4Address: updated.ipv4Address,
ipv6Address: updated.ipv6Address,
macAddress: updated.macAddress,
username: updated.username,
passwordHash: updated.passwordHash,
group: updated.group?.name ?? null,
location: updated.location?.name ?? null,
tags: updated.tags.map((t) => t.name),
createdAt: updated.createdAt.toISOString(),
updatedAt: updated.updatedAt.toISOString(),
},
changes: changes.map((c) => ({
field: c.field,
before: c.before,
after: c.after,
})),
};
await prisma.deviceHistory.create({
data: {
deviceId: updated.inventoryNumber,
changeType: 'UPDATED',
snapshot,
changedById: userId ?? null,
},
});
}
// 🔊 Socket.IO-Broadcast für echte Live-Updates
const io = (global as any).devicesIo as IOServer | undefined;
if (io) {
io.emit('device:updated', {
inventoryNumber: updated.inventoryNumber,
name: updated.name,
manufacturer: updated.manufacturer,
model: updated.model,
serialNumber: updated.serialNumber,
productNumber: updated.productNumber,
comment: updated.comment,
ipv4Address: updated.ipv4Address,
ipv6Address: updated.ipv6Address,
macAddress: updated.macAddress,
username: updated.username,
group: updated.group?.name ?? null,
location: updated.location?.name ?? null,
updatedAt: updated.updatedAt.toISOString(),
});
}
// Antwort an den Client (flattened)
return NextResponse.json({
inventoryNumber: updated.inventoryNumber,
name: updated.name,
manufacturer: updated.manufacturer,
model: updated.model,
serialNumber: updated.serialNumber,
productNumber: updated.productNumber,
comment: updated.comment,
ipv4Address: updated.ipv4Address,
ipv6Address: updated.ipv6Address,
macAddress: updated.macAddress,
username: updated.username,
group: updated.group?.name ?? null,
location: updated.location?.name ?? null,
createdAt: updated.createdAt.toISOString(),
updatedAt: updated.updatedAt.toISOString(),
});
} catch (err) {
console.error('[PATCH /api/devices/[id]]', err);
return NextResponse.json({ error: 'INTERNAL_ERROR' }, { status: 500 });
}
}