Initial scaffold: full-stack RackMapper application
Complete project scaffold with working auth, REST API, Prisma/SQLite schema, Docker config, and React frontend for both Rack Planner and Service Mapper modules. Both server and client pass TypeScript strict mode with zero errors. Initial migration applied. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
60
server/routes/auth.ts
Normal file
60
server/routes/auth.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { AppError, ok } from '../types/index';
|
||||
import { authMiddleware } from '../middleware/authMiddleware';
|
||||
|
||||
export const authRouter = Router();
|
||||
|
||||
const COOKIE_OPTS = {
|
||||
httpOnly: true,
|
||||
sameSite: 'strict' as const,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
path: '/',
|
||||
};
|
||||
|
||||
authRouter.post('/login', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { username, password } = req.body as { username?: string; password?: string };
|
||||
|
||||
if (!username || !password) {
|
||||
throw new AppError('Username and password are required', 400, 'MISSING_FIELDS');
|
||||
}
|
||||
|
||||
const adminUsername = process.env.ADMIN_USERNAME;
|
||||
const adminHash = process.env.ADMIN_PASSWORD_HASH;
|
||||
|
||||
if (!adminUsername || !adminHash) {
|
||||
throw new AppError('Server not configured: admin credentials missing', 500, 'CONFIG_ERROR');
|
||||
}
|
||||
|
||||
const usernameMatch = username === adminUsername;
|
||||
// Always run bcrypt to prevent timing attacks even if username is wrong
|
||||
const passwordMatch = await bcrypt.compare(password, adminHash);
|
||||
|
||||
if (!usernameMatch || !passwordMatch) {
|
||||
throw new AppError('Invalid username or password', 401, 'INVALID_CREDENTIALS');
|
||||
}
|
||||
|
||||
const secret = process.env.JWT_SECRET;
|
||||
if (!secret) throw new AppError('Server not configured: JWT_SECRET missing', 500, 'CONFIG_ERROR');
|
||||
|
||||
const token = jwt.sign({ sub: 'admin' }, secret, {
|
||||
expiresIn: (process.env.JWT_EXPIRY ?? '8h') as jwt.SignOptions['expiresIn'],
|
||||
});
|
||||
|
||||
res.cookie('token', token, COOKIE_OPTS);
|
||||
res.json(ok({ success: true }));
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
authRouter.post('/logout', (_req: Request, res: Response) => {
|
||||
res.clearCookie('token', COOKIE_OPTS);
|
||||
res.json(ok({ success: true }));
|
||||
});
|
||||
|
||||
authRouter.get('/me', authMiddleware, (_req: Request, res: Response) => {
|
||||
res.json(ok({ authenticated: true }));
|
||||
});
|
||||
28
server/routes/edges.ts
Normal file
28
server/routes/edges.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import * as mapService from '../services/mapService';
|
||||
import { ok } from '../types/index';
|
||||
|
||||
export const edgesRouter = Router();
|
||||
|
||||
edgesRouter.put('/:id', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { label, edgeType, animated, metadata } = req.body as {
|
||||
label?: string;
|
||||
edgeType?: string;
|
||||
animated?: boolean;
|
||||
metadata?: string;
|
||||
};
|
||||
res.json(ok(await mapService.updateEdge(req.params.id, { label, edgeType, animated, metadata })));
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
edgesRouter.delete('/:id', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await mapService.deleteEdge(req.params.id);
|
||||
res.json(ok(null));
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
51
server/routes/modules.ts
Normal file
51
server/routes/modules.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import * as moduleService from '../services/moduleService';
|
||||
import { ok } from '../types/index';
|
||||
|
||||
export const modulesRouter = Router();
|
||||
|
||||
modulesRouter.put('/:id', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { name, uPosition, uSize, manufacturer, model, ipAddress, notes } = req.body as {
|
||||
name?: string;
|
||||
uPosition?: number;
|
||||
uSize?: number;
|
||||
manufacturer?: string;
|
||||
model?: string;
|
||||
ipAddress?: string;
|
||||
notes?: string;
|
||||
};
|
||||
res.json(
|
||||
ok(
|
||||
await moduleService.updateModule(req.params.id, {
|
||||
name,
|
||||
uPosition,
|
||||
uSize,
|
||||
manufacturer,
|
||||
model,
|
||||
ipAddress,
|
||||
notes,
|
||||
})
|
||||
)
|
||||
);
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
modulesRouter.delete('/:id', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await moduleService.deleteModule(req.params.id);
|
||||
res.json(ok(null));
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
modulesRouter.get('/:id/ports', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
res.json(ok(await moduleService.getModulePorts(req.params.id)));
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
33
server/routes/nodes.ts
Normal file
33
server/routes/nodes.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import * as mapService from '../services/mapService';
|
||||
import { ok } from '../types/index';
|
||||
|
||||
export const nodesRouter = Router();
|
||||
|
||||
nodesRouter.put('/:id', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { label, positionX, positionY, metadata, color, icon, moduleId } = req.body as {
|
||||
label?: string;
|
||||
positionX?: number;
|
||||
positionY?: number;
|
||||
metadata?: string;
|
||||
color?: string;
|
||||
icon?: string;
|
||||
moduleId?: string | null;
|
||||
};
|
||||
res.json(
|
||||
ok(await mapService.updateNode(req.params.id, { label, positionX, positionY, metadata, color, icon, moduleId }))
|
||||
);
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
nodesRouter.delete('/:id', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await mapService.deleteNode(req.params.id);
|
||||
res.json(ok(null));
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
21
server/routes/ports.ts
Normal file
21
server/routes/ports.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import * as portService from '../services/portService';
|
||||
import { ok } from '../types/index';
|
||||
import type { VlanMode } from '../lib/constants';
|
||||
|
||||
export const portsRouter = Router();
|
||||
|
||||
portsRouter.put('/:id', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { label, mode, nativeVlan, notes, vlans } = req.body as {
|
||||
label?: string;
|
||||
mode?: VlanMode;
|
||||
nativeVlan?: number | null;
|
||||
notes?: string;
|
||||
vlans?: Array<{ vlanId: string; tagged: boolean }>;
|
||||
};
|
||||
res.json(ok(await portService.updatePort(req.params.id, { label, mode, nativeVlan, notes, vlans })));
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
83
server/routes/racks.ts
Normal file
83
server/routes/racks.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import * as rackService from '../services/rackService';
|
||||
import * as moduleService from '../services/moduleService';
|
||||
import { ok } from '../types/index';
|
||||
import type { ModuleType, PortType } from '../lib/constants';
|
||||
|
||||
export const racksRouter = Router();
|
||||
|
||||
racksRouter.get('/', async (_req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
res.json(ok(await rackService.listRacks()));
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
racksRouter.post('/', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { name, totalU, location, displayOrder } = req.body as {
|
||||
name: string;
|
||||
totalU?: number;
|
||||
location?: string;
|
||||
displayOrder?: number;
|
||||
};
|
||||
res.status(201).json(ok(await rackService.createRack({ name, totalU, location, displayOrder })));
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
racksRouter.get('/:id', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
res.json(ok(await rackService.getRack(req.params.id)));
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
racksRouter.put('/:id', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { name, totalU, location, displayOrder } = req.body as {
|
||||
name?: string;
|
||||
totalU?: number;
|
||||
location?: string;
|
||||
displayOrder?: number;
|
||||
};
|
||||
res.json(ok(await rackService.updateRack(req.params.id, { name, totalU, location, displayOrder })));
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
racksRouter.delete('/:id', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await rackService.deleteRack(req.params.id);
|
||||
res.json(ok(null));
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
racksRouter.post('/:id/modules', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { name, type, uPosition, uSize, manufacturer, model, ipAddress, notes, portCount, portType } =
|
||||
req.body as {
|
||||
name: string;
|
||||
type: ModuleType;
|
||||
uPosition: number;
|
||||
uSize?: number;
|
||||
manufacturer?: string;
|
||||
model?: string;
|
||||
ipAddress?: string;
|
||||
notes?: string;
|
||||
portCount?: number;
|
||||
portType?: PortType;
|
||||
};
|
||||
res.status(201).json(
|
||||
ok(await moduleService.createModule(req.params.id, { name, type, uPosition, uSize, manufacturer, model, ipAddress, notes, portCount, portType }))
|
||||
);
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
97
server/routes/serviceMap.ts
Normal file
97
server/routes/serviceMap.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import * as mapService from '../services/mapService';
|
||||
import { ok } from '../types/index';
|
||||
import type { NodeType } from '../lib/constants';
|
||||
|
||||
export const serviceMapRouter = Router();
|
||||
|
||||
serviceMapRouter.get('/', async (_req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
res.json(ok(await mapService.listMaps()));
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
serviceMapRouter.post('/', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { name, description } = req.body as { name: string; description?: string };
|
||||
res.status(201).json(ok(await mapService.createMap({ name, description })));
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
serviceMapRouter.get('/:id', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
res.json(ok(await mapService.getMap(req.params.id)));
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
serviceMapRouter.put('/:id', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { name, description } = req.body as { name?: string; description?: string };
|
||||
res.json(ok(await mapService.updateMap(req.params.id, { name, description })));
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
serviceMapRouter.delete('/:id', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await mapService.deleteMap(req.params.id);
|
||||
res.json(ok(null));
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
serviceMapRouter.post('/:id/nodes', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { label, nodeType, positionX, positionY, metadata, color, icon, moduleId } = req.body as {
|
||||
label: string;
|
||||
nodeType: NodeType;
|
||||
positionX: number;
|
||||
positionY: number;
|
||||
metadata?: string;
|
||||
color?: string;
|
||||
icon?: string;
|
||||
moduleId?: string;
|
||||
};
|
||||
res.status(201).json(
|
||||
ok(await mapService.addNode(req.params.id, { label, nodeType, positionX, positionY, metadata, color, icon, moduleId }))
|
||||
);
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
serviceMapRouter.post('/:id/populate', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
res.json(ok(await mapService.populateFromRack(req.params.id)));
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
serviceMapRouter.post('/:id/edges', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { sourceId, targetId, label, edgeType, animated, metadata } = req.body as {
|
||||
sourceId: string;
|
||||
targetId: string;
|
||||
label?: string;
|
||||
edgeType?: string;
|
||||
animated?: boolean;
|
||||
metadata?: string;
|
||||
};
|
||||
res.status(201).json(
|
||||
ok(await mapService.addEdge(req.params.id, { sourceId, targetId, label, edgeType, animated, metadata }))
|
||||
);
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
export { mapService };
|
||||
49
server/routes/vlans.ts
Normal file
49
server/routes/vlans.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import * as vlanService from '../services/vlanService';
|
||||
import { ok } from '../types/index';
|
||||
|
||||
export const vlansRouter = Router();
|
||||
|
||||
vlansRouter.get('/', async (_req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
res.json(ok(await vlanService.listVlans()));
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
vlansRouter.post('/', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { vlanId, name, description, color } = req.body as {
|
||||
vlanId: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
color?: string;
|
||||
};
|
||||
res.status(201).json(ok(await vlanService.createVlan({ vlanId, name, description, color })));
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
vlansRouter.put('/:id', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const { name, description, color } = req.body as {
|
||||
name?: string;
|
||||
description?: string;
|
||||
color?: string;
|
||||
};
|
||||
res.json(ok(await vlanService.updateVlan(req.params.id, { name, description, color })));
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
vlansRouter.delete('/:id', async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await vlanService.deleteVlan(req.params.id);
|
||||
res.json(ok(null));
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user