diff --git a/app/(app)/devices/page.tsx b/app/(app)/devices/page.tsx index 810a7cb..030d29a 100644 --- a/app/(app)/devices/page.tsx +++ b/app/(app)/devices/page.tsx @@ -566,7 +566,7 @@ export default function DevicesPage() { /> -
+

MAC-Adresse

diff --git a/app/api/devices/[id]/route.ts b/app/api/devices/[id]/route.ts index e4a8173..409f6b4 100644 --- a/app/api/devices/[id]/route.ts +++ b/app/api/devices/[id]/route.ts @@ -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 }; + export async function GET( _req: Request, - ctx: { params: Promise }, // 👈 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 }, // 👈 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, diff --git a/lib/auth.ts b/lib/auth.ts new file mode 100644 index 0000000..5b7a55d --- /dev/null +++ b/lib/auth.ts @@ -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 { + 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, + }, + }, + }, + }); +} diff --git a/prisma/dev.db b/prisma/dev.db index 94528b7..17a57ea 100644 Binary files a/prisma/dev.db and b/prisma/dev.db differ