feat: Fleet Forge UEM - device management, agent, remote desktop (VNC), TLS reverse proxy
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import type { Request, Response, NextFunction } from "express";
|
||||
import jwt from "jsonwebtoken";
|
||||
import type { AuthenticatedUser, Role } from "@fleetforge/shared";
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || "dev-secret";
|
||||
|
||||
const ROLE_HIERARCHY: Record<Role, number> = {
|
||||
owner: 4,
|
||||
admin: 3,
|
||||
operator: 2,
|
||||
read_only: 1,
|
||||
};
|
||||
|
||||
export function generateToken(user: AuthenticatedUser): string {
|
||||
return jwt.sign(
|
||||
{
|
||||
userId: user.userId,
|
||||
organizationId: user.organizationId,
|
||||
role: user.role,
|
||||
email: user.email,
|
||||
},
|
||||
JWT_SECRET,
|
||||
{ expiresIn: (process.env.JWT_EXPIRES_IN || "24h") as any }
|
||||
);
|
||||
}
|
||||
|
||||
export function verifyToken(token: string): AuthenticatedUser {
|
||||
return jwt.verify(token, JWT_SECRET) as AuthenticatedUser;
|
||||
}
|
||||
|
||||
export function authenticate(req: Request, res: Response, next: NextFunction) {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader?.startsWith("Bearer ")) {
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
error: { code: "UNAUTHORIZED", message: "Missing or invalid token" },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const token = authHeader.substring(7);
|
||||
const user = verifyToken(token);
|
||||
(req as any).user = user;
|
||||
next();
|
||||
} catch {
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
error: { code: "UNAUTHORIZED", message: "Invalid or expired token" },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function authorize(...allowedRoles: Role[]) {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
const user = (req as any).user as AuthenticatedUser;
|
||||
if (!user) {
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
error: { code: "UNAUTHORIZED", message: "Not authenticated" },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (allowedRoles.length > 0) {
|
||||
const hasRole = allowedRoles.some(
|
||||
(role) => ROLE_HIERARCHY[user.role] >= ROLE_HIERARCHY[role]
|
||||
);
|
||||
if (!hasRole) {
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
error: { code: "FORBIDDEN", message: "Insufficient permissions" },
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { Request, Response, NextFunction } from "express";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
export function correlationMiddleware(
|
||||
req: Request,
|
||||
_res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const correlationId =
|
||||
(req.headers["x-correlation-id"] as string) || uuidv4();
|
||||
(req as any).correlationId = correlationId;
|
||||
next();
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export const corsConfig = {
|
||||
origin: process.env.CORS_ORIGINS
|
||||
? process.env.CORS_ORIGINS.split(",")
|
||||
: [
|
||||
"http://localhost:3000",
|
||||
"http://localhost:3001",
|
||||
"http://localhost:3002",
|
||||
"http://localhost:3003",
|
||||
"http://localhost:3004",
|
||||
"http://localhost:3005",
|
||||
"http://10.0.0.194:3000",
|
||||
"http://10.0.0.194:3001",
|
||||
"http://10.0.0.194:3002",
|
||||
"http://10.0.0.194:3003",
|
||||
"http://10.0.0.194:3004",
|
||||
"http://10.0.0.194:3005",
|
||||
],
|
||||
credentials: true,
|
||||
methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
|
||||
allowedHeaders: ["Content-Type", "Authorization", "X-Correlation-ID"],
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { Request, Response, NextFunction } from "express";
|
||||
|
||||
export function errorHandler(
|
||||
err: Error,
|
||||
_req: Request,
|
||||
res: Response,
|
||||
_next: NextFunction
|
||||
) {
|
||||
console.error("Unhandled error:", err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: { code: "INTERNAL_ERROR", message: "An internal error occurred" },
|
||||
});
|
||||
}
|
||||
|
||||
export function notFoundHandler(_req: Request, res: Response) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: { code: "NOT_FOUND", message: "Endpoint not found" },
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { RateLimiterMemory } from "rate-limiter-flexible";
|
||||
|
||||
const authLimiter = new RateLimiterMemory({
|
||||
points: 10,
|
||||
duration: 60,
|
||||
});
|
||||
|
||||
const apiLimiter = new RateLimiterMemory({
|
||||
points: 100,
|
||||
duration: 60,
|
||||
});
|
||||
|
||||
const enrollLimiter = new RateLimiterMemory({
|
||||
points: 5,
|
||||
duration: 300,
|
||||
});
|
||||
|
||||
export async function rateLimitAuth(req: any, res: any, next: any) {
|
||||
try {
|
||||
const ip = req.ip || req.socket?.remoteAddress || "unknown";
|
||||
await authLimiter.consume(`auth:${ip}`);
|
||||
next();
|
||||
} catch {
|
||||
res.status(429).json({
|
||||
success: false,
|
||||
error: { code: "RATE_LIMITED", message: "Too many authentication attempts. Please try again later." },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function rateLimitApi(req: any, res: any, next: any) {
|
||||
try {
|
||||
const ip = req.ip || req.socket?.remoteAddress || "unknown";
|
||||
const user = (req as any).user;
|
||||
const key = user ? `api:${user.userId}` : `api:${ip}`;
|
||||
await apiLimiter.consume(key);
|
||||
next();
|
||||
} catch {
|
||||
res.status(429).json({
|
||||
success: false,
|
||||
error: { code: "RATE_LIMITED", message: "Rate limit exceeded. Please slow down." },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function rateLimitEnroll(req: any, res: any, next: any) {
|
||||
try {
|
||||
const ip = req.ip || req.socket?.remoteAddress || "unknown";
|
||||
await enrollLimiter.consume(`enroll:${ip}`);
|
||||
next();
|
||||
} catch {
|
||||
res.status(429).json({
|
||||
success: false,
|
||||
error: { code: "RATE_LIMITED", message: "Too many enrollment attempts." },
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user