585 lines
17 KiB
TypeScript
585 lines
17 KiB
TypeScript
// 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 POST(req: Request) {
|
||
try {
|
||
const body = await req.json();
|
||
|
||
const {
|
||
inventoryNumber,
|
||
name,
|
||
manufacturer,
|
||
model,
|
||
serialNumber,
|
||
productNumber,
|
||
comment,
|
||
group, // Name der Gruppe (string)
|
||
location, // Name des Standorts (string)
|
||
ipv4Address,
|
||
ipv6Address,
|
||
macAddress,
|
||
username,
|
||
passwordHash,
|
||
tags, // string[]
|
||
} = body;
|
||
|
||
if (!inventoryNumber || !name) {
|
||
return NextResponse.json(
|
||
{ error: 'inventoryNumber und name sind Pflichtfelder.' },
|
||
{ status: 400 },
|
||
);
|
||
}
|
||
|
||
// prüfen, ob Inventar-Nr. schon existiert
|
||
const existing = await prisma.device.findUnique({
|
||
where: { inventoryNumber },
|
||
});
|
||
if (existing) {
|
||
return NextResponse.json(
|
||
{ error: 'Gerät mit dieser Inventar-Nr. existiert bereits.' },
|
||
{ status: 409 },
|
||
);
|
||
}
|
||
|
||
// 🔹 aktuell eingeloggten User ermitteln
|
||
const userId = await getCurrentUserId();
|
||
|
||
// 🔹 nur verknüpfen, wenn der User wirklich in der DB existiert
|
||
let canConnectUser = false;
|
||
if (userId) {
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: userId },
|
||
select: { id: true },
|
||
});
|
||
if (user) {
|
||
canConnectUser = true;
|
||
} else {
|
||
console.warn(
|
||
`[POST /api/devices] User mit id=${userId} nicht gefunden – createdBy/changedBy werden nicht gesetzt.`,
|
||
);
|
||
}
|
||
}
|
||
|
||
// ✅ Gruppe nach Name auflösen / anlegen
|
||
let groupId: string | null = null;
|
||
if (group && typeof group === 'string' && group.trim() !== '') {
|
||
const grp = await prisma.deviceGroup.upsert({
|
||
where: { name: group.trim() },
|
||
update: {},
|
||
create: { name: group.trim() },
|
||
});
|
||
groupId = grp.id;
|
||
}
|
||
|
||
// ✅ Location nach Name auflösen / anlegen
|
||
let locationId: string | null = null;
|
||
if (location && typeof location === 'string' && location.trim() !== '') {
|
||
const loc = await prisma.location.upsert({
|
||
where: { name: location.trim() },
|
||
update: {},
|
||
create: { name: location.trim() },
|
||
});
|
||
locationId = loc.id;
|
||
}
|
||
|
||
// ✅ Tag-Namen vorbereiten
|
||
const tagNames: string[] = Array.isArray(tags)
|
||
? tags
|
||
.map((t: unknown) => String(t).trim())
|
||
.filter((t) => t.length > 0)
|
||
: [];
|
||
|
||
// ✅ Device anlegen (mit createdBy & Tags)
|
||
const created = await prisma.device.create({
|
||
data: {
|
||
inventoryNumber,
|
||
name,
|
||
manufacturer,
|
||
model,
|
||
serialNumber: serialNumber ?? null,
|
||
productNumber: productNumber ?? null,
|
||
comment: comment ?? null,
|
||
ipv4Address: ipv4Address ?? null,
|
||
ipv6Address: ipv6Address ?? null,
|
||
macAddress: macAddress ?? null,
|
||
username: username ?? null,
|
||
passwordHash: passwordHash ?? null,
|
||
groupId,
|
||
locationId,
|
||
// User, der das Gerät angelegt hat
|
||
...(canConnectUser && userId
|
||
? {
|
||
createdBy: {
|
||
connect: { id: userId },
|
||
},
|
||
}
|
||
: {}),
|
||
// Tags Many-to-Many
|
||
...(tagNames.length
|
||
? {
|
||
tags: {
|
||
connectOrCreate: tagNames.map((name) => ({
|
||
where: { name },
|
||
create: { name },
|
||
})),
|
||
},
|
||
}
|
||
: {}),
|
||
},
|
||
include: {
|
||
group: true,
|
||
location: true,
|
||
tags: true,
|
||
createdBy: true,
|
||
},
|
||
});
|
||
|
||
// 🔹 History-Eintrag "CREATED" mit changedById
|
||
const snapshot: Prisma.JsonObject = {
|
||
before: null,
|
||
after: {
|
||
inventoryNumber: created.inventoryNumber,
|
||
name: created.name,
|
||
manufacturer: created.manufacturer,
|
||
model: created.model,
|
||
serialNumber: created.serialNumber,
|
||
productNumber: created.productNumber,
|
||
comment: created.comment,
|
||
ipv4Address: created.ipv4Address,
|
||
ipv6Address: created.ipv6Address,
|
||
macAddress: created.macAddress,
|
||
username: created.username,
|
||
passwordHash: created.passwordHash,
|
||
group: created.group?.name ?? null,
|
||
location: created.location?.name ?? null,
|
||
tags: created.tags.map((t) => t.name),
|
||
createdAt: created.createdAt.toISOString(),
|
||
updatedAt: created.updatedAt.toISOString(),
|
||
},
|
||
changes: [],
|
||
};
|
||
|
||
await prisma.deviceHistory.create({
|
||
data: {
|
||
deviceId: created.inventoryNumber,
|
||
changeType: 'CREATED',
|
||
snapshot,
|
||
changedById: canConnectUser && userId ? userId : null,
|
||
},
|
||
});
|
||
|
||
// 🔊 optional: Socket.IO-Broadcast für Live-Listen
|
||
const io = (global as any).devicesIo as IOServer | undefined;
|
||
if (io) {
|
||
io.emit('device:created', {
|
||
inventoryNumber: created.inventoryNumber,
|
||
name: created.name,
|
||
manufacturer: created.manufacturer,
|
||
model: created.model,
|
||
serialNumber: created.serialNumber,
|
||
productNumber: created.productNumber,
|
||
comment: created.comment,
|
||
ipv4Address: created.ipv4Address,
|
||
ipv6Address: created.ipv6Address,
|
||
macAddress: created.macAddress,
|
||
username: created.username,
|
||
group: created.group?.name ?? null,
|
||
location: created.location?.name ?? null,
|
||
tags: created.tags.map((t) => t.name),
|
||
updatedAt: created.updatedAt.toISOString(),
|
||
});
|
||
}
|
||
|
||
// Antwort wie bei GET /api/devices
|
||
return NextResponse.json(
|
||
{
|
||
inventoryNumber: created.inventoryNumber,
|
||
name: created.name,
|
||
manufacturer: created.manufacturer,
|
||
model: created.model,
|
||
serialNumber: created.serialNumber,
|
||
productNumber: created.productNumber,
|
||
comment: created.comment,
|
||
ipv4Address: created.ipv4Address,
|
||
ipv6Address: created.ipv6Address,
|
||
macAddress: created.macAddress,
|
||
username: created.username,
|
||
passwordHash: created.passwordHash,
|
||
group: created.group?.name ?? null,
|
||
location: created.location?.name ?? null,
|
||
tags: created.tags.map((t) => t.name),
|
||
updatedAt: created.updatedAt.toISOString(),
|
||
},
|
||
{ status: 201 },
|
||
);
|
||
} catch (err) {
|
||
console.error('Error in POST /api/devices', err);
|
||
return NextResponse.json(
|
||
{ error: 'Interner Serverfehler beim Anlegen des Geräts.' },
|
||
{ status: 500 },
|
||
);
|
||
}
|
||
}
|
||
|
||
export async function PATCH(
|
||
req: Request,
|
||
ctx: RouteContext,
|
||
) {
|
||
const { id } = await ctx.params;
|
||
|
||
if (!id) {
|
||
return NextResponse.json({ error: 'MISSING_ID' }, { status: 400 });
|
||
}
|
||
|
||
try {
|
||
const body = await req.json();
|
||
|
||
// aktuell eingeloggten User ermitteln
|
||
const userId = await getCurrentUserId();
|
||
|
||
// nur verbinden, wenn der User in der DB existiert
|
||
let canConnectUpdatedBy = false;
|
||
if (userId) {
|
||
const user = await prisma.user.findUnique({
|
||
where: { id: userId },
|
||
select: { id: true },
|
||
});
|
||
if (user) {
|
||
canConnectUpdatedBy = true;
|
||
} else {
|
||
console.warn(
|
||
`[PATCH /api/devices/${id}] User mit id=${userId} nicht gefunden – updatedBy wird nicht gesetzt.`,
|
||
);
|
||
}
|
||
}
|
||
|
||
// 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,
|
||
},
|
||
});
|
||
|
||
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 ?? null,
|
||
productNumber: body.productNumber ?? null,
|
||
comment: body.comment ?? null,
|
||
ipv4Address: body.ipv4Address ?? null,
|
||
ipv6Address: body.ipv6Address ?? null,
|
||
macAddress: body.macAddress ?? null,
|
||
username: body.username ?? null,
|
||
passwordHash: body.passwordHash ?? null,
|
||
};
|
||
|
||
// updatedBy nur setzen, wenn User existiert
|
||
if (canConnectUpdatedBy && 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 = {
|
||
connectOrCreate: toConnect.map((name) => ({
|
||
where: { name },
|
||
create: { name },
|
||
})),
|
||
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];
|
||
|
||
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,
|
||
})),
|
||
};
|
||
|
||
// 🔴 WICHTIG: changedById nur setzen, wenn der User wirklich existiert
|
||
await prisma.deviceHistory.create({
|
||
data: {
|
||
deviceId: updated.inventoryNumber,
|
||
changeType: 'UPDATED',
|
||
snapshot,
|
||
changedById: canConnectUpdatedBy && userId ? userId : null,
|
||
},
|
||
});
|
||
}
|
||
|
||
// 🔊 Socket.IO-Broadcast für 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
|
||
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,
|
||
tags: updated.tags.map((t) => t.name),
|
||
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 });
|
||
}
|
||
} |