2025-11-24 08:59:14 +01:00

691 lines
20 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 '@/generated/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) {
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,
},
});
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),
// Verleih
loanedTo: device.loanedTo,
loanedFrom: device.loanedFrom ? device.loanedFrom.toISOString() : null,
loanedUntil: device.loanedUntil ? device.loanedUntil.toISOString() : null,
loanComment: device.loanComment,
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,
location,
ipv4Address,
ipv6Address,
macAddress,
username,
passwordHash,
tags,
// Verleih-Felder
loanedTo,
loanedFrom,
loanedUntil,
loanComment,
} = body;
if (!inventoryNumber || !name) {
return NextResponse.json(
{ error: 'inventoryNumber und name sind Pflichtfelder.' },
{ status: 400 },
);
}
const existing = await prisma.device.findUnique({
where: { inventoryNumber },
});
if (existing) {
return NextResponse.json(
{ error: 'Gerät mit dieser Inventar-Nr. existiert bereits.' },
{ status: 409 },
);
}
const userId = await getCurrentUserId();
let canConnectUser = false;
if (userId) {
const user = await prisma.user.findUnique({
where: { nwkennung: userId },
select: { nwkennung: true },
});
if (user) {
canConnectUser = true;
} else {
console.warn(
`[POST /api/devices] User mit nwkennung=${userId} nicht gefunden createdBy/changedBy werden nicht gesetzt.`,
);
}
}
// Gruppe
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;
}
// Standort
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;
}
const tagNames: string[] = Array.isArray(tags)
? tags
.map((t: unknown) => String(t).trim())
.filter((t) => t.length > 0)
: [];
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,
// Verleih-Felder
loanedTo: loanedTo ?? null,
loanedFrom: loanedFrom ? new Date(loanedFrom) : null,
loanedUntil: loanedUntil ? new Date(loanedUntil) : null,
loanComment: loanComment ?? null,
groupId,
locationId,
// ⬇️ statt createdBy.connect -> einfach FK setzen
createdById: canConnectUser && userId ? userId : null,
...(tagNames.length
? {
tags: {
connectOrCreate: tagNames.map((name) => ({
where: { name },
create: { name },
})),
},
}
: {}),
},
include: {
group: true,
location: true,
tags: true,
createdBy: true, // darf trotzdem included werden, Prisma nutzt createdById
},
});
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),
loanedTo: created.loanedTo,
loanedFrom: created.loanedFrom
? created.loanedFrom.toISOString()
: null,
loanedUntil: created.loanedUntil
? created.loanedUntil.toISOString()
: null,
loanComment: created.loanComment,
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,
},
});
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),
loanedTo: created.loanedTo,
loanedFrom: created.loanedFrom
? created.loanedFrom.toISOString()
: null,
loanedUntil: created.loanedUntil
? created.loanedUntil.toISOString()
: null,
loanComment: created.loanComment,
updatedAt: created.updatedAt.toISOString(),
});
}
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),
loanedTo: created.loanedTo,
loanedFrom: created.loanedFrom
? created.loanedFrom.toISOString()
: null,
loanedUntil: created.loanedUntil
? created.loanedUntil.toISOString()
: null,
loanComment: created.loanComment,
updatedAt: created.updatedAt.toISOString(),
},
{ status: 201 },
);
} catch (err) {
console.error('Error in POST /api/devices/[id]', 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();
const userId = await getCurrentUserId();
let canConnectUpdatedBy = false;
if (userId) {
const user = await prisma.user.findUnique({
where: { nwkennung: userId }, // ✅ User.nwkennung ist @id
select: { nwkennung: true },
});
if (user) {
canConnectUpdatedBy = true;
} else {
console.warn(
`[PATCH /api/devices/${id}] User mit nwkennung=${userId} nicht gefunden updatedBy wird nicht gesetzt.`,
);
}
}
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 });
}
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,
// Verleih
loanedTo: body.loanedTo ?? null,
loanedFrom: body.loanedFrom ? new Date(body.loanedFrom) : null,
loanedUntil: body.loanedUntil ? new Date(body.loanedUntil) : null,
loanComment: body.loanComment ?? null,
};
if (canConnectUpdatedBy && userId) {
data.updatedBy = {
connect: { nwkennung: userId },
};
}
// Gruppe
if (body.group != null && body.group !== '') {
data.group = {
connectOrCreate: {
where: { name: body.group },
create: { name: body.group },
},
};
} else {
data.group = { disconnect: true };
}
// Standort
if (body.location != null && body.location !== '') {
data.location = {
connectOrCreate: {
where: { name: body.location },
create: { name: body.location },
},
};
} else {
data.location = { disconnect: true };
}
// Tags
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 })),
};
}
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'
| 'loanedTo'
| 'loanedFrom'
| 'loanedUntil'
| 'loanComment';
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
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
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,
});
}
// Verleih-Diffs
const beforeLoanedTo = existing.loanedTo ?? null;
const afterLoanedTo = updated.loanedTo ?? null;
if (beforeLoanedTo !== afterLoanedTo) {
changes.push({
field: 'loanedTo',
before: beforeLoanedTo,
after: afterLoanedTo,
});
}
const beforeLoanedFrom = existing.loanedFrom
? existing.loanedFrom.toISOString()
: null;
const afterLoanedFrom = updated.loanedFrom
? updated.loanedFrom.toISOString()
: null;
if (beforeLoanedFrom !== afterLoanedFrom) {
changes.push({
field: 'loanedFrom',
before: beforeLoanedFrom,
after: afterLoanedFrom,
});
}
const beforeLoanedUntil = existing.loanedUntil
? existing.loanedUntil.toISOString()
: null;
const afterLoanedUntil = updated.loanedUntil
? updated.loanedUntil.toISOString()
: null;
if (beforeLoanedUntil !== afterLoanedUntil) {
changes.push({
field: 'loanedUntil',
before: beforeLoanedUntil,
after: afterLoanedUntil,
});
}
const beforeLoanComment = existing.loanComment ?? null;
const afterLoanComment = updated.loanComment ?? null;
if (beforeLoanComment !== afterLoanComment) {
changes.push({
field: 'loanComment',
before: beforeLoanComment,
after: afterLoanComment,
});
}
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),
loanedTo: existing.loanedTo,
loanedFrom: existing.loanedFrom
? existing.loanedFrom.toISOString()
: null,
loanedUntil: existing.loanedUntil
? existing.loanedUntil.toISOString()
: null,
loanComment: existing.loanComment,
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),
loanedTo: updated.loanedTo,
loanedFrom: updated.loanedFrom
? updated.loanedFrom.toISOString()
: null,
loanedUntil: updated.loanedUntil
? updated.loanedUntil.toISOString()
: null,
loanComment: updated.loanComment,
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: canConnectUpdatedBy && userId ? userId : null,
},
});
}
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,
tags: updated.tags.map((t) => t.name),
loanedTo: updated.loanedTo,
loanedFrom: updated.loanedFrom
? updated.loanedFrom.toISOString()
: null,
loanedUntil: updated.loanedUntil
? updated.loanedUntil.toISOString()
: null,
loanComment: updated.loanComment,
updatedAt: updated.updatedAt.toISOString(),
});
}
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),
loanedTo: updated.loanedTo,
loanedFrom: updated.loanedFrom
? updated.loanedFrom.toISOString()
: null,
loanedUntil: updated.loanedUntil
? updated.loanedUntil.toISOString()
: null,
loanComment: updated.loanComment,
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 });
}
}