Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
884 lines
25 KiB
TypeScript
884 lines
25 KiB
TypeScript
import http from "http";
|
|
import url from "url";
|
|
|
|
// In-memory state (reset on server start)
|
|
let websites: any[] = [];
|
|
let domains: any[] = [];
|
|
let dnsRecords: Record<string, any[]> = {};
|
|
let activeSessions: Record<string, { id: string; name: string; email: string; role: string }> =
|
|
{};
|
|
let rateLimitTracker: Record<string, number[]> = {};
|
|
|
|
function initMockDb() {
|
|
websites = [
|
|
{
|
|
id: "site-1",
|
|
name: "Main Production",
|
|
domain: "example.com",
|
|
runtime: "PHP",
|
|
status: "healthy",
|
|
ssl: "healthy",
|
|
environment: "production",
|
|
cpuLimit: "1 vCPU",
|
|
memoryLimit: "1 GB",
|
|
lastDeployment: new Date().toISOString(),
|
|
},
|
|
{
|
|
id: "site-2",
|
|
name: "Staging Area",
|
|
domain: "staging.example.com",
|
|
runtime: "Node.js",
|
|
status: "healthy",
|
|
ssl: "warning",
|
|
environment: "staging",
|
|
cpuLimit: "0.5 vCPU",
|
|
memoryLimit: "512 MB",
|
|
lastDeployment: new Date().toISOString(),
|
|
},
|
|
];
|
|
|
|
domains = [
|
|
{
|
|
id: "dom-1",
|
|
name: "example.com",
|
|
type: "primary",
|
|
documentRoot: "/home/coaxys/public_html",
|
|
sslStatus: "healthy",
|
|
dnssec: true,
|
|
propagation: 100,
|
|
},
|
|
{
|
|
id: "dom-2",
|
|
name: "staging.example.com",
|
|
type: "subdomain",
|
|
documentRoot: "/home/coaxys/staging",
|
|
sslStatus: "warning",
|
|
dnssec: false,
|
|
propagation: 90,
|
|
},
|
|
];
|
|
|
|
dnsRecords = {
|
|
"dom-1": [
|
|
{ id: "dns-1", name: "@", ttl: 3600, class: "IN", type: "A", record: "192.0.2.1" },
|
|
{
|
|
id: "dns-2",
|
|
name: "www",
|
|
ttl: 3600,
|
|
class: "IN",
|
|
type: "CNAME",
|
|
record: "example.com",
|
|
},
|
|
],
|
|
"dom-2": [
|
|
{ id: "dns-3", name: "@", ttl: 3600, class: "IN", type: "A", record: "192.0.2.2" },
|
|
],
|
|
};
|
|
|
|
activeSessions = {};
|
|
rateLimitTracker = {};
|
|
}
|
|
|
|
function getRequestBody(req: http.IncomingMessage): Promise<string> {
|
|
return new Promise((resolve) => {
|
|
let body = "";
|
|
req.on("data", (chunk) => {
|
|
body += chunk.toString();
|
|
});
|
|
req.on("end", () => {
|
|
resolve(body);
|
|
});
|
|
});
|
|
}
|
|
|
|
function parseCookies(cookieHeader?: string): Record<string, string> {
|
|
const list: Record<string, string> = {};
|
|
if (!cookieHeader) return list;
|
|
cookieHeader.split(";").forEach((cookie) => {
|
|
const parts = cookie.split("=");
|
|
const key = parts.shift()?.trim();
|
|
if (key) {
|
|
list[key] = decodeURIComponent(parts.join("=").trim());
|
|
}
|
|
});
|
|
return list;
|
|
}
|
|
|
|
function sendEnvelope(
|
|
res: http.ServerResponse,
|
|
status: number,
|
|
data: any,
|
|
metaExtra: any = {},
|
|
error: any = null,
|
|
headers: Record<string, string> = {},
|
|
) {
|
|
const responseObj = {
|
|
data,
|
|
meta: {
|
|
requestId: `req_${Math.random().toString(16).slice(2, 10)}`,
|
|
...metaExtra,
|
|
},
|
|
error,
|
|
};
|
|
res.writeHead(status, {
|
|
"Content-Type": "application/json",
|
|
...headers,
|
|
});
|
|
res.end(JSON.stringify(responseObj));
|
|
}
|
|
|
|
// Simple IP/Session rate limiter: max 5 requests per 100ms for testing rate limits
|
|
function _isRateLimited(key: string): boolean {
|
|
const now = Date.now();
|
|
if (!rateLimitTracker[key]) {
|
|
rateLimitTracker[key] = [];
|
|
}
|
|
rateLimitTracker[key] = rateLimitTracker[key].filter((t) => now - t < 100);
|
|
if (rateLimitTracker[key].length >= 5) {
|
|
return true;
|
|
}
|
|
rateLimitTracker[key].push(now);
|
|
return false;
|
|
}
|
|
|
|
export function startMockServer(port: number): http.Server {
|
|
initMockDb();
|
|
|
|
const server = http.createServer(async (req, res) => {
|
|
const parsedUrl = url.parse(req.url || "", true);
|
|
const pathname = parsedUrl.pathname || "";
|
|
const method = req.method || "GET";
|
|
const cookies = parseCookies(req.headers.cookie);
|
|
const _clientIp =
|
|
req.headers["x-forwarded-for"]?.toString() || req.socket.remoteAddress || "unknown";
|
|
|
|
// Rate limiting trigger (explicit trigger by path/header)
|
|
if (pathname === "/api/rate-limited" || req.headers["x-test-rate-limit"] === "true") {
|
|
return sendEnvelope(
|
|
res,
|
|
429,
|
|
null,
|
|
{
|
|
rateLimit: {
|
|
limit: 5,
|
|
remaining: 0,
|
|
reset: 100,
|
|
},
|
|
},
|
|
{
|
|
code: "RATE_LIMIT_ERROR",
|
|
message: "Too many requests. Please try again later.",
|
|
},
|
|
);
|
|
}
|
|
|
|
// 500 error simulation
|
|
if (pathname === "/api/simulate-500" || req.headers["x-simulate-500"] === "true") {
|
|
return sendEnvelope(
|
|
res,
|
|
500,
|
|
null,
|
|
{},
|
|
{
|
|
code: "NETWORK_ERROR",
|
|
message: "Internal server error simulated successfully.",
|
|
},
|
|
);
|
|
}
|
|
|
|
// Authentication session extraction
|
|
const session = cookies.session_id ? activeSessions[cookies.session_id] : null;
|
|
|
|
// Helper to enforce auth
|
|
const requireAuth = () => {
|
|
if (!session) {
|
|
sendEnvelope(
|
|
res,
|
|
401,
|
|
null,
|
|
{},
|
|
{
|
|
code: "PERMISSION_DENIED",
|
|
message: "Authentication required to access this resource.",
|
|
},
|
|
);
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
// Helper to enforce permission checks (for role-based scope comparison tests)
|
|
const requireRole = (roles: string[]) => {
|
|
if (!requireAuth() || !session) return false;
|
|
if (!roles.includes(session.role)) {
|
|
sendEnvelope(
|
|
res,
|
|
403,
|
|
null,
|
|
{},
|
|
{
|
|
code: "PERMISSION_DENIED",
|
|
message: `Role ${session.role} is not authorized for this operation.`,
|
|
},
|
|
);
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
// Routing Logic
|
|
try {
|
|
// 1. AUTHENTICATION ENDPOINTS
|
|
if (pathname === "/api/auth/login" && method === "POST") {
|
|
const bodyStr = await getRequestBody(req);
|
|
let body: any = {};
|
|
try {
|
|
body = JSON.parse(bodyStr);
|
|
} catch {
|
|
return sendEnvelope(
|
|
res,
|
|
400,
|
|
null,
|
|
{},
|
|
{ code: "VALIDATION_ERROR", message: "Invalid JSON body." },
|
|
);
|
|
}
|
|
|
|
const { email, password, code } = body;
|
|
if (!email) {
|
|
return sendEnvelope(
|
|
res,
|
|
400,
|
|
null,
|
|
{},
|
|
{
|
|
code: "VALIDATION_ERROR",
|
|
message: "Email is required.",
|
|
fields: { email: "Email is required." },
|
|
},
|
|
);
|
|
}
|
|
if (!password) {
|
|
return sendEnvelope(
|
|
res,
|
|
400,
|
|
null,
|
|
{},
|
|
{
|
|
code: "VALIDATION_ERROR",
|
|
message: "Password is required.",
|
|
fields: { password: "Password is required." },
|
|
},
|
|
);
|
|
}
|
|
|
|
// Simulating invalid password
|
|
if (password === "wrong-password") {
|
|
return sendEnvelope(
|
|
res,
|
|
401,
|
|
null,
|
|
{},
|
|
{
|
|
code: "PERMISSION_DENIED",
|
|
message: "Invalid credentials.",
|
|
},
|
|
);
|
|
}
|
|
|
|
// Two-factor simulation
|
|
if ((email.includes("2fa") || password === "2fa-password") && !code) {
|
|
return sendEnvelope(res, 200, {
|
|
user: null,
|
|
challenge: "totp",
|
|
sessionExpiresAt: null,
|
|
});
|
|
}
|
|
|
|
if (
|
|
(email.includes("2fa") || password === "2fa-password") &&
|
|
code &&
|
|
code !== "123456"
|
|
) {
|
|
return sendEnvelope(
|
|
res,
|
|
401,
|
|
null,
|
|
{},
|
|
{
|
|
code: "PERMISSION_DENIED",
|
|
message: "Invalid 2FA code.",
|
|
fields: { code: "Invalid 2FA code." },
|
|
},
|
|
);
|
|
}
|
|
|
|
// Determine role
|
|
let role = "owner";
|
|
if (email.includes("operator")) role = "operator";
|
|
if (email.includes("developer")) role = "developer";
|
|
if (email.includes("auditor")) role = "auditor";
|
|
|
|
const sessionId = `sess_${Math.random().toString(36).substring(2, 15)}`;
|
|
const user = {
|
|
id: `usr-${Math.random().toString(36).substring(2, 9)}`,
|
|
name: email.split("@")[0],
|
|
email,
|
|
role,
|
|
permissions: [],
|
|
lastLogin: new Date().toISOString(),
|
|
avatarInitials: email.substring(0, 2).toUpperCase(),
|
|
twoFactorEnabled: email.includes("2fa"),
|
|
};
|
|
|
|
activeSessions[sessionId] = user;
|
|
|
|
return sendEnvelope(
|
|
res,
|
|
200,
|
|
{
|
|
user,
|
|
challenge: null,
|
|
sessionExpiresAt: new Date(Date.now() + 45 * 60 * 1000).toISOString(),
|
|
},
|
|
{},
|
|
null,
|
|
{
|
|
"Set-Cookie": `session_id=${sessionId}; Path=/; HttpOnly`,
|
|
},
|
|
);
|
|
}
|
|
|
|
if (pathname === "/api/auth/logout" && method === "POST") {
|
|
if (cookies.session_id) {
|
|
delete activeSessions[cookies.session_id];
|
|
}
|
|
return sendEnvelope(res, 200, { ok: true }, {}, null, {
|
|
"Set-Cookie": "session_id=; Path=/; HttpOnly; Max-Age=0",
|
|
});
|
|
}
|
|
|
|
if (pathname === "/api/auth/me" && method === "GET") {
|
|
if (!requireAuth()) return;
|
|
return sendEnvelope(res, 200, session);
|
|
}
|
|
|
|
// 2. WEBSITES ENDPOINTS
|
|
if (pathname === "/api/websites" && method === "GET") {
|
|
if (!requireAuth()) return;
|
|
|
|
// SQL injection check: if search query contains common SQL Injection patterns,
|
|
// we safely return 200 with an empty list, verifying SQL injection safety.
|
|
const search = parsedUrl.query.search as string;
|
|
if (
|
|
search &&
|
|
(search.includes("'") ||
|
|
search.includes("OR 1=") ||
|
|
search.includes("UNION") ||
|
|
search.includes("--"))
|
|
) {
|
|
return sendEnvelope(res, 200, [], {
|
|
pagination: { page: 1, pageSize: 10, total: 0, pageCount: 0 },
|
|
});
|
|
}
|
|
|
|
let filtered = [...websites];
|
|
if (search) {
|
|
filtered = filtered.filter(
|
|
(w) =>
|
|
w.name.toLowerCase().includes(search.toLowerCase()) ||
|
|
w.domain.toLowerCase().includes(search.toLowerCase()),
|
|
);
|
|
}
|
|
|
|
// Pagination metadata
|
|
const page = parseInt(parsedUrl.query.page as string) || 1;
|
|
const pageSize = parseInt(parsedUrl.query.pageSize as string) || 10;
|
|
const total = filtered.length;
|
|
const pageCount = Math.ceil(total / pageSize);
|
|
const paginated = filtered.slice((page - 1) * pageSize, page * pageSize);
|
|
|
|
return sendEnvelope(res, 200, paginated, {
|
|
pagination: { page, pageSize, total, pageCount },
|
|
});
|
|
}
|
|
|
|
if (pathname === "/api/websites" && method === "POST") {
|
|
if (!requireRole(["owner", "operator", "developer"])) return;
|
|
|
|
const bodyStr = await getRequestBody(req);
|
|
let body: any = {};
|
|
try {
|
|
body = JSON.parse(bodyStr);
|
|
} catch {
|
|
return sendEnvelope(
|
|
res,
|
|
400,
|
|
null,
|
|
{},
|
|
{ code: "VALIDATION_ERROR", message: "Invalid JSON body." },
|
|
);
|
|
}
|
|
|
|
const { name, domain, runtime, environment } = body;
|
|
|
|
// Input validation
|
|
const fields: Record<string, string> = {};
|
|
if (!name || name.length < 3) {
|
|
fields.name = "Use at least three characters.";
|
|
}
|
|
if (!domain || !domain.includes(".")) {
|
|
fields.domain = "Invalid domain syntax.";
|
|
}
|
|
if (Object.keys(fields).length > 0) {
|
|
return sendEnvelope(
|
|
res,
|
|
400,
|
|
null,
|
|
{},
|
|
{
|
|
code: "VALIDATION_ERROR",
|
|
message: "Validation failed.",
|
|
fields,
|
|
},
|
|
);
|
|
}
|
|
|
|
// Conflict check
|
|
if (websites.some((w) => w.name.toLowerCase() === name.toLowerCase())) {
|
|
return sendEnvelope(
|
|
res,
|
|
409,
|
|
null,
|
|
{},
|
|
{
|
|
code: "VALIDATION_ERROR",
|
|
message: "Website name already exists.",
|
|
fields: { name: "Website name already exists." },
|
|
},
|
|
);
|
|
}
|
|
|
|
// Domain mapping to website CRUD check (for tier3 cross-feature logic)
|
|
// Ensure domain exists if it's not a new mapping configuration
|
|
if (domain && !domains.some((d) => d.name.toLowerCase() === domain.toLowerCase())) {
|
|
// In actual system, you might warning or allow it, but we can enforce mapping checks
|
|
// let's just make it healthy
|
|
}
|
|
|
|
const newSite = {
|
|
id: `site-${Math.random().toString(36).substring(2, 9)}`,
|
|
name,
|
|
domain,
|
|
runtime: runtime || "PHP",
|
|
status: "healthy",
|
|
ssl: "healthy",
|
|
environment: environment || "production",
|
|
cpuLimit: "1 vCPU",
|
|
memoryLimit: "1 GB",
|
|
lastDeployment: new Date().toISOString(),
|
|
};
|
|
|
|
websites.push(newSite);
|
|
|
|
return sendEnvelope(res, 201, newSite);
|
|
}
|
|
|
|
// Website detail endpoints: /api/websites/:id
|
|
if (pathname.startsWith("/api/websites/") && !pathname.endsWith("/dns")) {
|
|
const id = pathname.substring("/api/websites/".length);
|
|
const index = websites.findIndex((w) => w.id === id);
|
|
|
|
if (index === -1) {
|
|
return sendEnvelope(
|
|
res,
|
|
404,
|
|
null,
|
|
{},
|
|
{
|
|
code: "NOT_FOUND",
|
|
message: `Website with ID ${id} not found.`,
|
|
},
|
|
);
|
|
}
|
|
|
|
if (method === "GET") {
|
|
if (!requireAuth()) return;
|
|
return sendEnvelope(res, 200, websites[index]);
|
|
}
|
|
|
|
if (method === "PUT") {
|
|
if (!requireRole(["owner", "operator", "developer"])) return;
|
|
const bodyStr = await getRequestBody(req);
|
|
let body: any = {};
|
|
try {
|
|
body = JSON.parse(bodyStr);
|
|
} catch {
|
|
return sendEnvelope(
|
|
res,
|
|
400,
|
|
null,
|
|
{},
|
|
{ code: "VALIDATION_ERROR", message: "Invalid JSON body." },
|
|
);
|
|
}
|
|
|
|
websites[index] = {
|
|
...websites[index],
|
|
...body,
|
|
};
|
|
return sendEnvelope(res, 200, websites[index]);
|
|
}
|
|
|
|
if (method === "DELETE") {
|
|
if (!requireRole(["owner", "operator"])) return;
|
|
const deleted = websites.splice(index, 1)[0];
|
|
return sendEnvelope(res, 200, { deleted: deleted.id });
|
|
}
|
|
}
|
|
|
|
// 3. DOMAINS ENDPOINTS
|
|
if (pathname === "/api/domains" && method === "GET") {
|
|
if (!requireAuth()) return;
|
|
return sendEnvelope(res, 200, domains);
|
|
}
|
|
|
|
if (pathname === "/api/domains" && method === "POST") {
|
|
if (!requireRole(["owner", "operator"])) return;
|
|
|
|
const bodyStr = await getRequestBody(req);
|
|
let body: any = {};
|
|
try {
|
|
body = JSON.parse(bodyStr);
|
|
} catch {
|
|
return sendEnvelope(
|
|
res,
|
|
400,
|
|
null,
|
|
{},
|
|
{ code: "VALIDATION_ERROR", message: "Invalid JSON body." },
|
|
);
|
|
}
|
|
|
|
const { name, type, documentRoot } = body;
|
|
const fields: Record<string, string> = {};
|
|
if (!name || !name.includes(".")) {
|
|
fields.name = "Invalid domain syntax.";
|
|
}
|
|
if (!type) {
|
|
fields.type = "Type is required.";
|
|
}
|
|
if (Object.keys(fields).length > 0) {
|
|
return sendEnvelope(
|
|
res,
|
|
400,
|
|
null,
|
|
{},
|
|
{
|
|
code: "VALIDATION_ERROR",
|
|
message: "Validation failed.",
|
|
fields,
|
|
},
|
|
);
|
|
}
|
|
|
|
// Conflict check
|
|
if (domains.some((d) => d.name.toLowerCase() === name.toLowerCase())) {
|
|
return sendEnvelope(
|
|
res,
|
|
409,
|
|
null,
|
|
{},
|
|
{
|
|
code: "VALIDATION_ERROR",
|
|
message: "Domain already exists.",
|
|
fields: { name: "Domain already exists." },
|
|
},
|
|
);
|
|
}
|
|
|
|
const newDom = {
|
|
id: `dom-${Math.random().toString(36).substring(2, 9)}`,
|
|
name,
|
|
type,
|
|
documentRoot: documentRoot || `/home/coaxys/${name}`,
|
|
sslStatus: "healthy",
|
|
dnssec: false,
|
|
propagation: 0,
|
|
};
|
|
|
|
domains.push(newDom);
|
|
dnsRecords[newDom.id] = [
|
|
{
|
|
id: `dns-${Math.random().toString(36).substring(2, 9)}`,
|
|
name: "@",
|
|
ttl: 14400,
|
|
class: "IN",
|
|
type: "NS",
|
|
record: "ns1.coaxys.com.",
|
|
},
|
|
];
|
|
|
|
return sendEnvelope(res, 201, newDom);
|
|
}
|
|
|
|
// Domain detail / DNS endpoints
|
|
if (pathname.startsWith("/api/domains/")) {
|
|
// Parse paths like:
|
|
// /api/domains/:id
|
|
// /api/domains/:id/dns
|
|
// /api/domains/:id/dns/:recordId
|
|
const remainingPath = pathname.substring("/api/domains/".length);
|
|
const parts = remainingPath.split("/");
|
|
const id = parts[0];
|
|
const domainIndex = domains.findIndex((d) => d.id === id);
|
|
|
|
if (domainIndex === -1) {
|
|
return sendEnvelope(
|
|
res,
|
|
404,
|
|
null,
|
|
{},
|
|
{
|
|
code: "NOT_FOUND",
|
|
message: `Domain with ID ${id} not found.`,
|
|
},
|
|
);
|
|
}
|
|
|
|
const domainObj = domains[domainIndex];
|
|
|
|
// Case: /api/domains/:id
|
|
if (parts.length === 1) {
|
|
if (method === "GET") {
|
|
if (!requireAuth()) return;
|
|
return sendEnvelope(res, 200, domainObj);
|
|
}
|
|
|
|
if (method === "PUT") {
|
|
if (!requireRole(["owner", "operator"])) return;
|
|
const bodyStr = await getRequestBody(req);
|
|
let body: any = {};
|
|
try {
|
|
body = JSON.parse(bodyStr);
|
|
} catch {
|
|
return sendEnvelope(
|
|
res,
|
|
400,
|
|
null,
|
|
{},
|
|
{ code: "VALIDATION_ERROR", message: "Invalid JSON body." },
|
|
);
|
|
}
|
|
|
|
domains[domainIndex] = {
|
|
...domainObj,
|
|
...body,
|
|
};
|
|
return sendEnvelope(res, 200, domains[domainIndex]);
|
|
}
|
|
|
|
if (method === "DELETE") {
|
|
if (!requireRole(["owner", "operator"])) return;
|
|
const deleted = domains.splice(domainIndex, 1)[0];
|
|
delete dnsRecords[id];
|
|
|
|
// Clean up dependent websites domains if matching mapping
|
|
websites.forEach((w) => {
|
|
if (w.domain === deleted.name) {
|
|
w.status = "warning"; // domain mapping severed
|
|
}
|
|
});
|
|
|
|
return sendEnvelope(res, 200, { deleted: deleted.id });
|
|
}
|
|
}
|
|
|
|
// Case: /api/domains/:id/dns
|
|
if (parts[1] === "dns") {
|
|
const recordsList = dnsRecords[id] || [];
|
|
|
|
if (parts.length === 2) {
|
|
if (method === "GET") {
|
|
if (!requireAuth()) return;
|
|
return sendEnvelope(res, 200, recordsList);
|
|
}
|
|
|
|
if (method === "POST") {
|
|
if (!requireRole(["owner", "operator", "developer"])) return;
|
|
const bodyStr = await getRequestBody(req);
|
|
let body: any = {};
|
|
try {
|
|
body = JSON.parse(bodyStr);
|
|
} catch {
|
|
return sendEnvelope(
|
|
res,
|
|
400,
|
|
null,
|
|
{},
|
|
{ code: "VALIDATION_ERROR", message: "Invalid JSON body." },
|
|
);
|
|
}
|
|
|
|
const { name: recName, ttl, type: recType, record, priority } = body;
|
|
const fields: Record<string, string> = {};
|
|
if (!recName) fields.name = "Record name is required.";
|
|
if (!ttl || ttl < 60) fields.ttl = "TTL must be at least 60 seconds.";
|
|
if (
|
|
!recType ||
|
|
!["A", "AAAA", "CNAME", "MX", "TXT", "SRV", "CAA", "NS", "PTR"].includes(
|
|
recType,
|
|
)
|
|
) {
|
|
fields.type = "Invalid DNS record type.";
|
|
}
|
|
if (!record) fields.record = "Record value is required.";
|
|
|
|
if (Object.keys(fields).length > 0) {
|
|
return sendEnvelope(
|
|
res,
|
|
400,
|
|
null,
|
|
{},
|
|
{
|
|
code: "VALIDATION_ERROR",
|
|
message: "Validation failed.",
|
|
fields,
|
|
},
|
|
);
|
|
}
|
|
|
|
// MX exclusivity check
|
|
if (recType === "CNAME" && recordsList.some((r) => r.name === recName)) {
|
|
return sendEnvelope(
|
|
res,
|
|
409,
|
|
null,
|
|
{},
|
|
{
|
|
code: "VALIDATION_ERROR",
|
|
message:
|
|
"CNAME conflict: A CNAME record cannot coexist with other records for the same name.",
|
|
fields: { name: "CNAME record conflict." },
|
|
},
|
|
);
|
|
}
|
|
|
|
const newRecord = {
|
|
id: `dns-${Math.random().toString(36).substring(2, 9)}`,
|
|
name: recName,
|
|
ttl,
|
|
class: "IN",
|
|
type: recType,
|
|
record,
|
|
priority,
|
|
};
|
|
|
|
recordsList.push(newRecord);
|
|
dnsRecords[id] = recordsList;
|
|
|
|
return sendEnvelope(res, 201, newRecord);
|
|
}
|
|
}
|
|
|
|
// Case: /api/domains/:id/dns/:recordId
|
|
if (parts.length === 3) {
|
|
const recordId = parts[2];
|
|
const recordIndex = recordsList.findIndex((r) => r.id === recordId);
|
|
|
|
if (recordIndex === -1) {
|
|
return sendEnvelope(
|
|
res,
|
|
404,
|
|
null,
|
|
{},
|
|
{
|
|
code: "NOT_FOUND",
|
|
message: `DNS Record with ID ${recordId} not found.`,
|
|
},
|
|
);
|
|
}
|
|
|
|
if (method === "PUT") {
|
|
if (!requireRole(["owner", "operator", "developer"])) return;
|
|
const bodyStr = await getRequestBody(req);
|
|
let body: any = {};
|
|
try {
|
|
body = JSON.parse(bodyStr);
|
|
} catch {
|
|
return sendEnvelope(
|
|
res,
|
|
400,
|
|
null,
|
|
{},
|
|
{ code: "VALIDATION_ERROR", message: "Invalid JSON body." },
|
|
);
|
|
}
|
|
|
|
recordsList[recordIndex] = {
|
|
...recordsList[recordIndex],
|
|
...body,
|
|
};
|
|
dnsRecords[id] = recordsList;
|
|
|
|
return sendEnvelope(res, 200, recordsList[recordIndex]);
|
|
}
|
|
|
|
if (method === "DELETE") {
|
|
if (!requireRole(["owner", "operator", "developer"])) return;
|
|
const deleted = recordsList.splice(recordIndex, 1)[0];
|
|
dnsRecords[id] = recordsList;
|
|
|
|
return sendEnvelope(res, 200, { deleted: deleted.id });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 4. METHOD NOT ALLOWED FALLBACK
|
|
// If a route is recognized but method is invalid
|
|
if (
|
|
(pathname === "/api/auth/login" || pathname === "/api/auth/logout") &&
|
|
method !== "POST"
|
|
) {
|
|
res.writeHead(405, { "Content-Type": "application/json" });
|
|
return res.end(
|
|
JSON.stringify({
|
|
data: null,
|
|
meta: { requestId: "req_method_not_allowed" },
|
|
error: {
|
|
code: "METHOD_NOT_ALLOWED",
|
|
message: `Method ${method} not allowed on this path.`,
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
|
|
// Default Not Found
|
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
res.end(
|
|
JSON.stringify({
|
|
data: null,
|
|
meta: { requestId: "req_not_found" },
|
|
error: { code: "NOT_FOUND", message: "Path not found." },
|
|
}),
|
|
);
|
|
} catch (err: any) {
|
|
// 500 fallback
|
|
return sendEnvelope(
|
|
res,
|
|
500,
|
|
null,
|
|
{},
|
|
{
|
|
code: "NETWORK_ERROR",
|
|
message: err.message || "An unexpected server error occurred.",
|
|
},
|
|
);
|
|
}
|
|
});
|
|
|
|
server.listen(port);
|
|
return server;
|
|
}
|