This commit is contained in:
Linrador 2025-11-15 10:01:44 +01:00
parent 3af854663e
commit d1f13fd77e
4 changed files with 208 additions and 32 deletions

View File

@ -566,7 +566,7 @@ export default function DevicesPage() {
/>
</div>
<div>
<div className='col-span-2'>
<p className="text-xs font-semibold uppercase tracking-wide text-gray-400">
MAC-Adresse
</p>

View File

@ -2,14 +2,19 @@
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import type { Prisma } from '@prisma/client';
import { getCurrentUserId } from '@/lib/auth';
type RouteParams = { id: string };
// ⬇️ Next liefert params als Promise
type RouteContext = { params: Promise<RouteParams> };
export async function GET(
_req: Request,
ctx: { params: Promise<RouteParams> }, // 👈 params ist ein Promise
ctx: RouteContext,
) {
const { id } = await ctx.params; // 👈 Promise auflösen
// ⬇️ Promise auflösen
const { id } = await ctx.params;
if (!id) {
return NextResponse.json({ error: 'MISSING_ID' }, { status: 400 });
@ -40,6 +45,7 @@ export async function GET(
ipv6Address: device.ipv6Address,
macAddress: device.macAddress,
username: device.username,
// passwordHash gebe ich hier absichtlich nicht zurück
group: device.group?.name ?? null,
location: device.location?.name ?? null,
createdAt: device.createdAt.toISOString(),
@ -53,14 +59,27 @@ export async function GET(
export async function PATCH(
req: Request,
ctx: { params: Promise<RouteParams> }, // 👈 hier auch
ctx: RouteContext,
) {
const { id } = await ctx.params; // 👈 Promise auflösen
// ⬇️ Promise auflösen
const { id } = await ctx.params;
const body = await req.json();
if (!id) {
return NextResponse.json({ error: 'MISSING_ID' }, { status: 400 });
}
try {
// User für updatedBy / History bestimmen
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,
},
});
if (!existing) {
@ -79,9 +98,17 @@ export async function PATCH(
ipv6Address: body.ipv6Address,
macAddress: body.macAddress,
username: body.username,
passwordHash: body.passwordHash,
};
// Gruppe (per Name) Name ist @unique in DeviceGroup
// 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: {
@ -105,6 +132,7 @@ export async function PATCH(
data.location = { disconnect: true };
}
// Update durchführen (für "after"-Snapshot)
const updated = await prisma.device.update({
where: { inventoryNumber: id },
data,
@ -114,33 +142,106 @@ export async function PATCH(
},
});
const snapshot: Prisma.JsonObject = {
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,
createdAt: updated.createdAt.toISOString(),
updatedAt: updated.updatedAt.toISOString(),
};
const trackedFields = [
'name',
'manufacturer',
'model',
'serialNumber',
'productNumber',
'comment',
'ipv4Address',
'ipv6Address',
'macAddress',
'username',
'passwordHash',
] as const;
await prisma.deviceHistory.create({
data: {
deviceId: updated.inventoryNumber,
changeType: 'UPDATED',
snapshot,
changedById: null,
},
});
type TrackedField = (typeof trackedFields)[number];
const changes: {
field: TrackedField | 'group' | 'location';
before: string | null;
after: string | null;
}[] = [];
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 });
}
}
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,
});
}
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,
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,
createdAt: updated.createdAt.toISOString(),
updatedAt: updated.updatedAt.toISOString(),
},
changes,
};
await prisma.deviceHistory.create({
data: {
deviceId: updated.inventoryNumber,
changeType: 'UPDATED',
snapshot,
changedById: userId ?? null,
},
});
}
return NextResponse.json({
inventoryNumber: updated.inventoryNumber,

75
lib/auth.ts Normal file
View File

@ -0,0 +1,75 @@
// lib/auth.ts
'use server';
import { headers } from 'next/headers';
import { prisma } from './prisma';
/**
* Liefert die aktuelle User-ID oder null,
* falls kein User ermittelt werden kann.
*
* Reihenfolge:
* 1. HTTP-Header: x-user-id
* 2. HTTP-Header: x-user-email
* 3. Fallback: DEFAULT_USER_EMAIL env oder "user@domain.local"
*/
export async function getCurrentUserId(): Promise<string | null> {
const h = await headers();
const headerUserId = h.get('x-user-id');
const headerUserEmail = h.get('x-user-email');
// 1) Direkt über ID (Header)
if (headerUserId) {
const userById = await prisma.user.findUnique({
where: { id: headerUserId },
select: { id: true },
});
if (userById) {
return userById.id;
}
}
// 2) Über Email (Header)
if (headerUserEmail) {
const userByEmail = await prisma.user.findUnique({
where: { email: headerUserEmail },
select: { id: true },
});
if (userByEmail) {
return userByEmail.id;
}
}
// 3) Fallback: Standard-User (z.B. dein Test-User)
const fallbackEmail =
process.env.DEFAULT_USER_EMAIL ?? 'user@domain.local';
const fallbackUser = await prisma.user.findUnique({
where: { email: fallbackEmail },
select: { id: true },
});
return fallbackUser?.id ?? null;
}
/**
* Optional: kompletten User holen (falls du später mehr brauchst)
*/
export async function getCurrentUser() {
const userId = await getCurrentUserId();
if (!userId) return null;
return prisma.user.findUnique({
where: { id: userId },
include: {
roles: {
include: {
role: true,
},
},
},
});
}

Binary file not shown.