Files
rack-planner/server/services/moduleService.ts
jason bcb8a95fae Switch auth to plain-text password env var (remove bcrypt)
- Replace ADMIN_PASSWORD_HASH with ADMIN_PASSWORD in auth route and docker-compose
- Remove bcryptjs / @types/bcryptjs dependencies
- Delete scripts/hashPassword.ts (no longer needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 22:05:42 -05:00

184 lines
5.4 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.
import { prisma } from '../lib/prisma';
import { AppError } from '../types/index';
import { MODULE_PORT_DEFAULTS, MODULE_U_DEFAULTS, type ModuleType, type PortType } from '../lib/constants';
const moduleInclude = {
ports: {
orderBy: { portNumber: 'asc' as const },
include: {
vlans: { include: { vlan: true } },
},
},
};
/** Check whether a U-range is occupied in a rack, optionally excluding one module (for moves). */
async function hasCollision(
rackId: string,
uPosition: number,
uSize: number,
excludeModuleId?: string
): Promise<boolean> {
const modules = await prisma.module.findMany({
where: { rackId, ...(excludeModuleId ? { id: { not: excludeModuleId } } : {}) },
select: { uPosition: true, uSize: true },
});
const occupied = new Set<number>();
for (const m of modules) {
for (let u = m.uPosition; u < m.uPosition + m.uSize; u++) occupied.add(u);
}
for (let u = uPosition; u < uPosition + uSize; u++) {
if (occupied.has(u)) return true;
}
return false;
}
export async function createModule(
rackId: string,
data: {
name: string;
type: ModuleType;
uPosition: number;
uSize?: number;
manufacturer?: string;
model?: string;
ipAddress?: string;
notes?: string;
portCount?: number;
portType?: PortType;
}
) {
const rack = await prisma.rack.findUnique({ where: { id: rackId } });
if (!rack) throw new AppError('Rack not found', 404, 'NOT_FOUND');
const uSize = data.uSize ?? MODULE_U_DEFAULTS[data.type] ?? 1;
if (data.uPosition < 1 || data.uPosition + uSize - 1 > rack.totalU) {
throw new AppError(
`Module does not fit within rack (U1U${rack.totalU})`,
400,
'OUT_OF_BOUNDS'
);
}
if (await hasCollision(rackId, data.uPosition, uSize)) {
throw new AppError('U-slot collision: another module occupies that space', 409, 'COLLISION');
}
const portCount = data.portCount ?? MODULE_PORT_DEFAULTS[data.type] ?? 0;
const portType: PortType = data.portType ?? 'ETHERNET';
return prisma.module.create({
data: {
rackId,
name: data.name,
type: data.type,
uPosition: data.uPosition,
uSize,
manufacturer: data.manufacturer,
model: data.model,
ipAddress: data.ipAddress,
notes: data.notes,
ports: {
create: Array.from({ length: portCount }, (_, i) => ({
portNumber: i + 1,
portType,
})),
},
},
include: moduleInclude,
});
}
export async function updateModule(
id: string,
data: Partial<{
name: string;
uPosition: number;
uSize: number;
manufacturer: string;
model: string;
ipAddress: string;
notes: string;
}>
) {
const existing = await prisma.module.findUnique({ where: { id } });
if (!existing) throw new AppError('Module not found', 404, 'NOT_FOUND');
const newPosition = data.uPosition ?? existing.uPosition;
const newSize = data.uSize ?? existing.uSize;
if (data.uPosition !== undefined || data.uSize !== undefined) {
const rack = await prisma.rack.findUnique({ where: { id: existing.rackId } });
if (!rack) throw new AppError('Rack not found', 404, 'NOT_FOUND');
if (newPosition < 1 || newPosition + newSize - 1 > rack.totalU) {
throw new AppError(
`Module does not fit within rack (U1U${rack.totalU})`,
400,
'OUT_OF_BOUNDS'
);
}
if (await hasCollision(existing.rackId, newPosition, newSize, id)) {
throw new AppError('U-slot collision', 409, 'COLLISION');
}
}
return prisma.module.update({ where: { id }, data, include: moduleInclude });
}
export async function deleteModule(id: string) {
const existing = await prisma.module.findUnique({ where: { id } });
if (!existing) throw new AppError('Module not found', 404, 'NOT_FOUND');
return prisma.module.delete({ where: { id } });
}
/**
* Move a module to a new rack and/or U-position.
* Ports and VLAN assignments move with the module (they're linked by moduleId).
*/
export async function moveModule(
id: string,
targetRackId: string,
targetUPosition: number
) {
const existing = await prisma.module.findUnique({ where: { id } });
if (!existing) throw new AppError('Module not found', 404, 'NOT_FOUND');
const targetRack = await prisma.rack.findUnique({ where: { id: targetRackId } });
if (!targetRack) throw new AppError('Target rack not found', 404, 'NOT_FOUND');
if (targetUPosition < 1 || targetUPosition + existing.uSize - 1 > targetRack.totalU) {
throw new AppError(
`Module does not fit within target rack (U1U${targetRack.totalU})`,
400,
'OUT_OF_BOUNDS'
);
}
// Collision check in target rack, excluding self (handles same-rack moves)
const excludeInTarget = targetRackId === existing.rackId ? id : undefined;
if (await hasCollision(targetRackId, targetUPosition, existing.uSize, excludeInTarget)) {
throw new AppError('U-slot collision in target rack', 409, 'COLLISION');
}
return prisma.module.update({
where: { id },
data: { rackId: targetRackId, uPosition: targetUPosition },
include: moduleInclude,
});
}
export async function getModulePorts(id: string) {
const existing = await prisma.module.findUnique({ where: { id } });
if (!existing) throw new AppError('Module not found', 404, 'NOT_FOUND');
return prisma.port.findMany({
where: { moduleId: id },
orderBy: { portNumber: 'asc' },
include: { vlans: { include: { vlan: true } } },
});
}