mirror of
https://github.com/freedbygrace/DynamoDNS.git
synced 2026-08-20 06:32:17 +00:00
Implement initial application structure and UI.
Replit-Commit-Author: Agent Replit-Commit-Session-Id: 9111ef36-26c8-4085-84ca-a35dc1fec1b5 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7083d608-d6d3-4a6a-9a27-6286c5109627/54918520-d42c-44cd-b4f4-9e5a42eb5be1.jpg
This commit is contained in:
+205
@@ -0,0 +1,205 @@
|
||||
import passport from "passport";
|
||||
import { Strategy as LocalStrategy } from "passport-local";
|
||||
import { Express, Request } from "express";
|
||||
import session from "express-session";
|
||||
import { scrypt, randomBytes, timingSafeEqual } from "crypto";
|
||||
import { promisify } from "util";
|
||||
import { storage } from "./storage";
|
||||
import { User, InsertUser } from "@shared/schema";
|
||||
import { z } from "zod";
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface User extends User {}
|
||||
}
|
||||
}
|
||||
|
||||
const scryptAsync = promisify(scrypt);
|
||||
|
||||
async function hashPassword(password: string) {
|
||||
const salt = randomBytes(16).toString("hex");
|
||||
const buf = (await scryptAsync(password, salt, 64)) as Buffer;
|
||||
return `${buf.toString("hex")}.${salt}`;
|
||||
}
|
||||
|
||||
async function comparePasswords(supplied: string, stored: string) {
|
||||
const [hashed, salt] = stored.split(".");
|
||||
const hashedBuf = Buffer.from(hashed, "hex");
|
||||
const suppliedBuf = (await scryptAsync(supplied, salt, 64)) as Buffer;
|
||||
return timingSafeEqual(hashedBuf, suppliedBuf);
|
||||
}
|
||||
|
||||
// API Token Authentication middleware
|
||||
export function authenticateApiToken(req: Request, res: any, next: any) {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const token = authHeader.split(' ')[1];
|
||||
|
||||
storage.getApiTokenByToken(token).then(apiToken => {
|
||||
if (apiToken && apiToken.isActive) {
|
||||
req.apiToken = apiToken;
|
||||
}
|
||||
next();
|
||||
}).catch(err => {
|
||||
console.error('API token authentication error:', err);
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
// Role-based authorization middleware for API and UI
|
||||
export function requireRole(roles: string[]) {
|
||||
return (req: Request, res: any, next: any) => {
|
||||
// Check if authenticated via session
|
||||
if (req.isAuthenticated() && req.user && roles.includes(req.user.role)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Check if authenticated via API token
|
||||
if (req.apiToken) {
|
||||
const hasPermission = req.apiToken.permissions.some(p => roles.includes(p));
|
||||
if (hasPermission) {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(403).json({ message: "Insufficient permissions" });
|
||||
};
|
||||
}
|
||||
|
||||
export function setupAuth(app: Express) {
|
||||
const sessionSettings: session.SessionOptions = {
|
||||
secret: process.env.SESSION_SECRET || 'dynamidns-secret',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
store: storage.sessionStore,
|
||||
cookie: {
|
||||
maxAge: 1000 * 60 * 60 * 24, // 1 day
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax'
|
||||
}
|
||||
};
|
||||
|
||||
app.set("trust proxy", 1);
|
||||
app.use(session(sessionSettings));
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
// Apply API token authentication middleware before routes
|
||||
app.use(authenticateApiToken);
|
||||
|
||||
passport.use(
|
||||
new LocalStrategy(async (username, password, done) => {
|
||||
try {
|
||||
// Check if username is email format
|
||||
const isEmail = username.includes('@');
|
||||
|
||||
let user;
|
||||
if (isEmail) {
|
||||
user = await storage.getUserByEmail(username);
|
||||
} else {
|
||||
user = await storage.getUserByUsername(username);
|
||||
}
|
||||
|
||||
if (!user || !(await comparePasswords(password, user.password))) {
|
||||
return done(null, false);
|
||||
} else {
|
||||
return done(null, user);
|
||||
}
|
||||
} catch (error) {
|
||||
return done(error);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
passport.serializeUser((user, done) => done(null, user.id));
|
||||
passport.deserializeUser(async (id: number, done) => {
|
||||
try {
|
||||
const user = await storage.getUser(id);
|
||||
done(null, user);
|
||||
} catch (error) {
|
||||
done(error);
|
||||
}
|
||||
});
|
||||
|
||||
// User registration validation schema
|
||||
const registerSchema = z.object({
|
||||
username: z.string().min(3).max(50),
|
||||
password: z.string().min(8).max(100),
|
||||
email: z.string().email(),
|
||||
fullName: z.string().optional(),
|
||||
organizationId: z.number().optional()
|
||||
});
|
||||
|
||||
app.post("/api/register", async (req, res, next) => {
|
||||
try {
|
||||
// Validate input
|
||||
const validatedData = registerSchema.parse(req.body);
|
||||
|
||||
// Check if user exists
|
||||
const existingUser = await storage.getUserByUsername(validatedData.username);
|
||||
if (existingUser) {
|
||||
return res.status(400).json({ message: "Username already exists" });
|
||||
}
|
||||
|
||||
const existingEmail = await storage.getUserByEmail(validatedData.email);
|
||||
if (existingEmail) {
|
||||
return res.status(400).json({ message: "Email already exists" });
|
||||
}
|
||||
|
||||
// Create user
|
||||
const user = await storage.createUser({
|
||||
...validatedData,
|
||||
password: await hashPassword(validatedData.password),
|
||||
role: "user", // Default role
|
||||
});
|
||||
|
||||
// Remove password from response
|
||||
const { password, ...userWithoutPassword } = user;
|
||||
|
||||
req.login(user, (err) => {
|
||||
if (err) return next(err);
|
||||
res.status(201).json(userWithoutPassword);
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
res.status(400).json({ message: "Validation error", errors: error.errors });
|
||||
} else {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/login", (req, res, next) => {
|
||||
passport.authenticate("local", (err, user, info) => {
|
||||
if (err) return next(err);
|
||||
if (!user) return res.status(401).json({ message: "Invalid credentials" });
|
||||
|
||||
req.login(user, (err) => {
|
||||
if (err) return next(err);
|
||||
|
||||
// Remove password from response
|
||||
const { password, ...userWithoutPassword } = user;
|
||||
res.status(200).json(userWithoutPassword);
|
||||
});
|
||||
})(req, res, next);
|
||||
});
|
||||
|
||||
app.post("/api/logout", (req, res, next) => {
|
||||
req.logout((err) => {
|
||||
if (err) return next(err);
|
||||
res.sendStatus(200);
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/api/user", (req, res) => {
|
||||
if (!req.isAuthenticated()) return res.status(401).json({ message: "Not authenticated" });
|
||||
|
||||
// Remove password from response
|
||||
const { password, ...userWithoutPassword } = req.user;
|
||||
res.json(userWithoutPassword);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import express, { type Request, Response, NextFunction } from "express";
|
||||
import { registerRoutes } from "./routes";
|
||||
import { setupVite, serveStatic, log } from "./vite";
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
|
||||
app.use((req, res, next) => {
|
||||
const start = Date.now();
|
||||
const path = req.path;
|
||||
let capturedJsonResponse: Record<string, any> | undefined = undefined;
|
||||
|
||||
const originalResJson = res.json;
|
||||
res.json = function (bodyJson, ...args) {
|
||||
capturedJsonResponse = bodyJson;
|
||||
return originalResJson.apply(res, [bodyJson, ...args]);
|
||||
};
|
||||
|
||||
res.on("finish", () => {
|
||||
const duration = Date.now() - start;
|
||||
if (path.startsWith("/api")) {
|
||||
let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`;
|
||||
if (capturedJsonResponse) {
|
||||
logLine += ` :: ${JSON.stringify(capturedJsonResponse)}`;
|
||||
}
|
||||
|
||||
if (logLine.length > 80) {
|
||||
logLine = logLine.slice(0, 79) + "…";
|
||||
}
|
||||
|
||||
log(logLine);
|
||||
}
|
||||
});
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
(async () => {
|
||||
const server = await registerRoutes(app);
|
||||
|
||||
app.use((err: any, _req: Request, res: Response, _next: NextFunction) => {
|
||||
const status = err.status || err.statusCode || 500;
|
||||
const message = err.message || "Internal Server Error";
|
||||
|
||||
res.status(status).json({ message });
|
||||
throw err;
|
||||
});
|
||||
|
||||
// importantly only setup vite in development and after
|
||||
// setting up all the other routes so the catch-all route
|
||||
// doesn't interfere with the other routes
|
||||
if (app.get("env") === "development") {
|
||||
await setupVite(app, server);
|
||||
} else {
|
||||
serveStatic(app);
|
||||
}
|
||||
|
||||
// ALWAYS serve the app on port 5000
|
||||
// this serves both the API and the client.
|
||||
// It is the only port that is not firewalled.
|
||||
const port = 5000;
|
||||
server.listen({
|
||||
port,
|
||||
host: "0.0.0.0",
|
||||
reusePort: true,
|
||||
}, () => {
|
||||
log(`serving on port ${port}`);
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,672 @@
|
||||
import type { Express, Request, Response } from "express";
|
||||
import { createServer, type Server } from "http";
|
||||
import { storage } from "./storage";
|
||||
import { setupAuth, requireRole } from "./auth";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
insertDomainSchema,
|
||||
insertDnsRecordSchema,
|
||||
insertProviderSchema,
|
||||
insertApiTokenSchema,
|
||||
insertOrganizationSchema,
|
||||
recordTypes,
|
||||
providerTypes
|
||||
} from "@shared/schema";
|
||||
import { randomBytes } from "crypto";
|
||||
|
||||
export async function registerRoutes(app: Express): Promise<Server> {
|
||||
// Setup authentication routes
|
||||
setupAuth(app);
|
||||
|
||||
const httpServer = createServer(app);
|
||||
|
||||
// Organizations
|
||||
app.get("/api/organizations", requireRole(["admin", "manager", "user", "readonly"]), async (req, res) => {
|
||||
try {
|
||||
const organizations = await storage.getOrganizations();
|
||||
res.json(organizations);
|
||||
} catch (error) {
|
||||
console.error("Error fetching organizations:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/organizations/:id", requireRole(["admin", "manager", "user", "readonly"]), async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const organization = await storage.getOrganization(id);
|
||||
|
||||
if (!organization) {
|
||||
return res.status(404).json({ message: "Organization not found" });
|
||||
}
|
||||
|
||||
res.json(organization);
|
||||
} catch (error) {
|
||||
console.error("Error fetching organization:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/organizations", requireRole(["admin"]), async (req, res) => {
|
||||
try {
|
||||
const validatedData = insertOrganizationSchema.parse(req.body);
|
||||
const organization = await storage.createOrganization(validatedData);
|
||||
res.status(201).json(organization);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
res.status(400).json({ message: "Validation error", errors: error.errors });
|
||||
} else {
|
||||
console.error("Error creating organization:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.put("/api/organizations/:id", requireRole(["admin"]), async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const validatedData = insertOrganizationSchema.partial().parse(req.body);
|
||||
|
||||
const updatedOrganization = await storage.updateOrganization(id, validatedData);
|
||||
|
||||
if (!updatedOrganization) {
|
||||
return res.status(404).json({ message: "Organization not found" });
|
||||
}
|
||||
|
||||
res.json(updatedOrganization);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
res.status(400).json({ message: "Validation error", errors: error.errors });
|
||||
} else {
|
||||
console.error("Error updating organization:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.delete("/api/organizations/:id", requireRole(["admin"]), async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const deleted = await storage.deleteOrganization(id);
|
||||
|
||||
if (!deleted) {
|
||||
return res.status(404).json({ message: "Organization not found" });
|
||||
}
|
||||
|
||||
res.status(204).end();
|
||||
} catch (error) {
|
||||
console.error("Error deleting organization:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// Domains
|
||||
app.get("/api/domains", requireRole(["admin", "manager", "user", "readonly"]), async (req, res) => {
|
||||
try {
|
||||
let domains;
|
||||
const orgId = req.query.organizationId ? parseInt(req.query.organizationId as string) : undefined;
|
||||
|
||||
if (orgId) {
|
||||
domains = await storage.getDomainsByOrganization(orgId);
|
||||
} else {
|
||||
domains = await storage.getAllDomains();
|
||||
}
|
||||
|
||||
res.json(domains);
|
||||
} catch (error) {
|
||||
console.error("Error fetching domains:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/domains/:id", requireRole(["admin", "manager", "user", "readonly"]), async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const domain = await storage.getDomain(id);
|
||||
|
||||
if (!domain) {
|
||||
return res.status(404).json({ message: "Domain not found" });
|
||||
}
|
||||
|
||||
res.json(domain);
|
||||
} catch (error) {
|
||||
console.error("Error fetching domain:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/domains", requireRole(["admin", "manager"]), async (req, res) => {
|
||||
try {
|
||||
const validatedData = insertDomainSchema.parse(req.body);
|
||||
const domain = await storage.createDomain(validatedData);
|
||||
res.status(201).json(domain);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
res.status(400).json({ message: "Validation error", errors: error.errors });
|
||||
} else {
|
||||
console.error("Error creating domain:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.put("/api/domains/:id", requireRole(["admin", "manager"]), async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const validatedData = insertDomainSchema.partial().parse(req.body);
|
||||
|
||||
const updatedDomain = await storage.updateDomain(id, validatedData);
|
||||
|
||||
if (!updatedDomain) {
|
||||
return res.status(404).json({ message: "Domain not found" });
|
||||
}
|
||||
|
||||
res.json(updatedDomain);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
res.status(400).json({ message: "Validation error", errors: error.errors });
|
||||
} else {
|
||||
console.error("Error updating domain:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.delete("/api/domains/:id", requireRole(["admin", "manager"]), async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const deleted = await storage.deleteDomain(id);
|
||||
|
||||
if (!deleted) {
|
||||
return res.status(404).json({ message: "Domain not found" });
|
||||
}
|
||||
|
||||
res.status(204).end();
|
||||
} catch (error) {
|
||||
console.error("Error deleting domain:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// DNS Records
|
||||
app.get("/api/dns-records", requireRole(["admin", "manager", "user", "readonly"]), async (req, res) => {
|
||||
try {
|
||||
const domainId = req.query.domainId ? parseInt(req.query.domainId as string) : undefined;
|
||||
|
||||
if (!domainId) {
|
||||
return res.status(400).json({ message: "Domain ID is required" });
|
||||
}
|
||||
|
||||
const records = await storage.getDnsRecordsByDomain(domainId);
|
||||
res.json(records);
|
||||
} catch (error) {
|
||||
console.error("Error fetching DNS records:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/dns-records/:id", requireRole(["admin", "manager", "user", "readonly"]), async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const record = await storage.getDnsRecord(id);
|
||||
|
||||
if (!record) {
|
||||
return res.status(404).json({ message: "DNS record not found" });
|
||||
}
|
||||
|
||||
res.json(record);
|
||||
} catch (error) {
|
||||
console.error("Error fetching DNS record:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/dns-records", requireRole(["admin", "manager", "user"]), async (req, res) => {
|
||||
try {
|
||||
// Validate record type
|
||||
const recordTypeValidator = z.enum(recordTypes);
|
||||
|
||||
// Extend schema with validation
|
||||
const schema = insertDnsRecordSchema.extend({
|
||||
type: recordTypeValidator
|
||||
});
|
||||
|
||||
const validatedData = schema.parse(req.body);
|
||||
|
||||
// Get the previous record if it exists
|
||||
let previousRecord = null;
|
||||
const records = await storage.getDnsRecordsByDomain(validatedData.domainId);
|
||||
previousRecord = records.find(r => r.name === validatedData.name && r.type === validatedData.type);
|
||||
|
||||
// Create the new record
|
||||
const record = await storage.createDnsRecord(validatedData);
|
||||
|
||||
// Add to history
|
||||
await storage.addDnsHistory(
|
||||
record.id,
|
||||
"create",
|
||||
previousRecord ? JSON.stringify(previousRecord) : undefined,
|
||||
JSON.stringify(record),
|
||||
req.user?.id
|
||||
);
|
||||
|
||||
res.status(201).json(record);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
res.status(400).json({ message: "Validation error", errors: error.errors });
|
||||
} else {
|
||||
console.error("Error creating DNS record:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.put("/api/dns-records/:id", requireRole(["admin", "manager", "user"]), async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
|
||||
// Get the previous record
|
||||
const previousRecord = await storage.getDnsRecord(id);
|
||||
|
||||
if (!previousRecord) {
|
||||
return res.status(404).json({ message: "DNS record not found" });
|
||||
}
|
||||
|
||||
// Validate record type
|
||||
const recordTypeValidator = z.enum(recordTypes);
|
||||
|
||||
// Extend schema with validation
|
||||
const schema = insertDnsRecordSchema.partial().extend({
|
||||
type: recordTypeValidator.optional()
|
||||
});
|
||||
|
||||
const validatedData = schema.parse(req.body);
|
||||
|
||||
// Update the record
|
||||
const updatedRecord = await storage.updateDnsRecord(id, validatedData);
|
||||
|
||||
if (!updatedRecord) {
|
||||
return res.status(404).json({ message: "DNS record not found" });
|
||||
}
|
||||
|
||||
// Add to history
|
||||
await storage.addDnsHistory(
|
||||
id,
|
||||
"update",
|
||||
JSON.stringify(previousRecord),
|
||||
JSON.stringify(updatedRecord),
|
||||
req.user?.id
|
||||
);
|
||||
|
||||
res.json(updatedRecord);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
res.status(400).json({ message: "Validation error", errors: error.errors });
|
||||
} else {
|
||||
console.error("Error updating DNS record:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.delete("/api/dns-records/:id", requireRole(["admin", "manager"]), async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
|
||||
// Get the record before deletion
|
||||
const record = await storage.getDnsRecord(id);
|
||||
|
||||
if (!record) {
|
||||
return res.status(404).json({ message: "DNS record not found" });
|
||||
}
|
||||
|
||||
const deleted = await storage.deleteDnsRecord(id);
|
||||
|
||||
if (!deleted) {
|
||||
return res.status(404).json({ message: "DNS record not found" });
|
||||
}
|
||||
|
||||
// Add to history
|
||||
await storage.addDnsHistory(
|
||||
id,
|
||||
"delete",
|
||||
JSON.stringify(record),
|
||||
undefined,
|
||||
req.user?.id
|
||||
);
|
||||
|
||||
res.status(204).end();
|
||||
} catch (error) {
|
||||
console.error("Error deleting DNS record:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// Providers
|
||||
app.get("/api/providers", requireRole(["admin", "manager", "user", "readonly"]), async (req, res) => {
|
||||
try {
|
||||
const providers = await storage.getProviders();
|
||||
|
||||
// Mask credentials in response
|
||||
const maskedProviders = providers.map(provider => {
|
||||
if (provider.credentials) {
|
||||
return {
|
||||
...provider,
|
||||
credentials: { masked: true }
|
||||
};
|
||||
}
|
||||
return provider;
|
||||
});
|
||||
|
||||
res.json(maskedProviders);
|
||||
} catch (error) {
|
||||
console.error("Error fetching providers:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/providers/:id", requireRole(["admin", "manager"]), async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const provider = await storage.getProvider(id);
|
||||
|
||||
if (!provider) {
|
||||
return res.status(404).json({ message: "Provider not found" });
|
||||
}
|
||||
|
||||
// Mask credentials in response
|
||||
const maskedProvider = {
|
||||
...provider,
|
||||
credentials: provider.credentials ? { masked: true } : null
|
||||
};
|
||||
|
||||
res.json(maskedProvider);
|
||||
} catch (error) {
|
||||
console.error("Error fetching provider:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/providers", requireRole(["admin"]), async (req, res) => {
|
||||
try {
|
||||
// Validate provider type
|
||||
const providerTypeValidator = z.enum(providerTypes);
|
||||
|
||||
// Extend schema with validation
|
||||
const schema = insertProviderSchema.extend({
|
||||
type: providerTypeValidator
|
||||
});
|
||||
|
||||
const validatedData = schema.parse(req.body);
|
||||
const provider = await storage.createProvider(validatedData);
|
||||
|
||||
// Mask credentials in response
|
||||
const maskedProvider = {
|
||||
...provider,
|
||||
credentials: provider.credentials ? { masked: true } : null
|
||||
};
|
||||
|
||||
res.status(201).json(maskedProvider);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
res.status(400).json({ message: "Validation error", errors: error.errors });
|
||||
} else {
|
||||
console.error("Error creating provider:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.put("/api/providers/:id", requireRole(["admin"]), async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
|
||||
// Validate provider type
|
||||
const providerTypeValidator = z.enum(providerTypes);
|
||||
|
||||
// Extend schema with validation
|
||||
const schema = insertProviderSchema.partial().extend({
|
||||
type: providerTypeValidator.optional()
|
||||
});
|
||||
|
||||
const validatedData = schema.parse(req.body);
|
||||
|
||||
const updatedProvider = await storage.updateProvider(id, validatedData);
|
||||
|
||||
if (!updatedProvider) {
|
||||
return res.status(404).json({ message: "Provider not found" });
|
||||
}
|
||||
|
||||
// Mask credentials in response
|
||||
const maskedProvider = {
|
||||
...updatedProvider,
|
||||
credentials: updatedProvider.credentials ? { masked: true } : null
|
||||
};
|
||||
|
||||
res.json(maskedProvider);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
res.status(400).json({ message: "Validation error", errors: error.errors });
|
||||
} else {
|
||||
console.error("Error updating provider:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.delete("/api/providers/:id", requireRole(["admin"]), async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const deleted = await storage.deleteProvider(id);
|
||||
|
||||
if (!deleted) {
|
||||
return res.status(404).json({ message: "Provider not found" });
|
||||
}
|
||||
|
||||
res.status(204).end();
|
||||
} catch (error) {
|
||||
console.error("Error deleting provider:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// API Tokens
|
||||
app.get("/api/api-tokens", requireRole(["admin", "manager"]), async (req, res) => {
|
||||
try {
|
||||
let tokens;
|
||||
const orgId = req.query.organizationId ? parseInt(req.query.organizationId as string) : undefined;
|
||||
|
||||
if (orgId) {
|
||||
tokens = await storage.getApiTokensByOrganization(orgId);
|
||||
} else if (req.user?.role === "admin") {
|
||||
// Admins can see all tokens
|
||||
const allOrgs = await storage.getOrganizations();
|
||||
tokens = await Promise.all(
|
||||
allOrgs.map(org => storage.getApiTokensByOrganization(org.id))
|
||||
).then(results => results.flat());
|
||||
} else {
|
||||
// Others can only see tokens for their organization
|
||||
tokens = req.user?.organizationId
|
||||
? await storage.getApiTokensByOrganization(req.user.organizationId)
|
||||
: [];
|
||||
}
|
||||
|
||||
// Mask token values
|
||||
const maskedTokens = tokens.map(token => ({
|
||||
...token,
|
||||
token: token.token.substring(0, 8) + '...'
|
||||
}));
|
||||
|
||||
res.json(maskedTokens);
|
||||
} catch (error) {
|
||||
console.error("Error fetching API tokens:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/api-tokens", requireRole(["admin", "manager"]), async (req, res) => {
|
||||
try {
|
||||
// Generate a random token
|
||||
const tokenValue = randomBytes(32).toString('hex');
|
||||
|
||||
// Validate permissions
|
||||
const permissionsValidator = z.array(z.enum(["admin", "manager", "user", "readonly"]));
|
||||
|
||||
// Create schema with additional validation
|
||||
const schema = insertApiTokenSchema
|
||||
.omit({ token: true })
|
||||
.extend({
|
||||
permissions: permissionsValidator,
|
||||
name: z.string().min(1)
|
||||
});
|
||||
|
||||
const validatedData = schema.parse(req.body);
|
||||
|
||||
// Set token value and created by
|
||||
const tokenData = {
|
||||
...validatedData,
|
||||
token: tokenValue,
|
||||
createdBy: req.user?.id
|
||||
};
|
||||
|
||||
const token = await storage.createApiToken(tokenData);
|
||||
|
||||
// Return the full token only on creation
|
||||
res.status(201).json(token);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
res.status(400).json({ message: "Validation error", errors: error.errors });
|
||||
} else {
|
||||
console.error("Error creating API token:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.put("/api/api-tokens/:id", requireRole(["admin", "manager"]), async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
|
||||
// Get the token
|
||||
const token = await storage.getApiToken(id);
|
||||
|
||||
if (!token) {
|
||||
return res.status(404).json({ message: "API token not found" });
|
||||
}
|
||||
|
||||
// Verify permission: only admin can modify any token, managers can only modify their org's tokens
|
||||
if (req.user?.role !== "admin" && token.organizationId !== req.user?.organizationId) {
|
||||
return res.status(403).json({ message: "Insufficient permissions" });
|
||||
}
|
||||
|
||||
// Validate permissions
|
||||
const permissionsValidator = z.array(z.enum(["admin", "manager", "user", "readonly"])).optional();
|
||||
|
||||
// Create schema with additional validation
|
||||
const schema = insertApiTokenSchema
|
||||
.omit({ token: true })
|
||||
.partial()
|
||||
.extend({
|
||||
permissions: permissionsValidator,
|
||||
name: z.string().min(1).optional()
|
||||
});
|
||||
|
||||
const validatedData = schema.parse(req.body);
|
||||
|
||||
const updatedToken = await storage.updateApiToken(id, validatedData);
|
||||
|
||||
if (!updatedToken) {
|
||||
return res.status(404).json({ message: "API token not found" });
|
||||
}
|
||||
|
||||
// Mask token in response
|
||||
const maskedToken = {
|
||||
...updatedToken,
|
||||
token: updatedToken.token.substring(0, 8) + '...'
|
||||
};
|
||||
|
||||
res.json(maskedToken);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
res.status(400).json({ message: "Validation error", errors: error.errors });
|
||||
} else {
|
||||
console.error("Error updating API token:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.delete("/api/api-tokens/:id", requireRole(["admin", "manager"]), async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
|
||||
// Get the token
|
||||
const token = await storage.getApiToken(id);
|
||||
|
||||
if (!token) {
|
||||
return res.status(404).json({ message: "API token not found" });
|
||||
}
|
||||
|
||||
// Verify permission: only admin can delete any token, managers can only delete their org's tokens
|
||||
if (req.user?.role !== "admin" && token.organizationId !== req.user?.organizationId) {
|
||||
return res.status(403).json({ message: "Insufficient permissions" });
|
||||
}
|
||||
|
||||
const deleted = await storage.deleteApiToken(id);
|
||||
|
||||
if (!deleted) {
|
||||
return res.status(404).json({ message: "API token not found" });
|
||||
}
|
||||
|
||||
res.status(204).end();
|
||||
} catch (error) {
|
||||
console.error("Error deleting API token:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// DNS History
|
||||
app.get("/api/dns-history/record/:recordId", requireRole(["admin", "manager", "user", "readonly"]), async (req, res) => {
|
||||
try {
|
||||
const recordId = parseInt(req.params.recordId);
|
||||
const history = await storage.getDnsHistoryByRecord(recordId);
|
||||
res.json(history);
|
||||
} catch (error) {
|
||||
console.error("Error fetching DNS history:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/dns-history/domain/:domainId", requireRole(["admin", "manager", "user", "readonly"]), async (req, res) => {
|
||||
try {
|
||||
const domainId = parseInt(req.params.domainId);
|
||||
const history = await storage.getDnsHistoryByDomain(domainId);
|
||||
res.json(history);
|
||||
} catch (error) {
|
||||
console.error("Error fetching DNS history:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
// User Management (admin only)
|
||||
app.get("/api/users", requireRole(["admin"]), async (req, res) => {
|
||||
try {
|
||||
// This would use a real database query to get all users
|
||||
// For the in-memory storage, we'll return the currently stored users
|
||||
const users = Array.from(storage.usersMap ? storage.usersMap.values() : []);
|
||||
|
||||
// Remove passwords from response
|
||||
const sanitizedUsers = users.map(user => {
|
||||
const { password, ...userWithoutPassword } = user;
|
||||
return userWithoutPassword;
|
||||
});
|
||||
|
||||
res.json(sanitizedUsers);
|
||||
} catch (error) {
|
||||
console.error("Error fetching users:", error);
|
||||
res.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
return httpServer;
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
import { randomBytes } from "crypto";
|
||||
import {
|
||||
users, organizations, domains, dnsRecords,
|
||||
providers, dnsHistory, apiTokens,
|
||||
type User, type InsertUser,
|
||||
type Organization, type InsertOrganization,
|
||||
type Domain, type InsertDomain,
|
||||
type DnsRecord, type InsertDnsRecord,
|
||||
type Provider, type InsertProvider,
|
||||
type ApiToken, type InsertApiToken,
|
||||
type DnsHistory
|
||||
} from "@shared/schema";
|
||||
import session from "express-session";
|
||||
import createMemoryStore from "memorystore";
|
||||
|
||||
const MemoryStore = createMemoryStore(session);
|
||||
|
||||
export interface IStorage {
|
||||
// User management
|
||||
getUser(id: number): Promise<User | undefined>;
|
||||
getUserByUsername(username: string): Promise<User | undefined>;
|
||||
getUserByEmail(email: string): Promise<User | undefined>;
|
||||
createUser(user: InsertUser): Promise<User>;
|
||||
updateUser(id: number, user: Partial<InsertUser>): Promise<User | undefined>;
|
||||
deleteUser(id: number): Promise<boolean>;
|
||||
|
||||
// Organization management
|
||||
getOrganization(id: number): Promise<Organization | undefined>;
|
||||
getOrganizations(): Promise<Organization[]>;
|
||||
createOrganization(org: InsertOrganization): Promise<Organization>;
|
||||
updateOrganization(id: number, org: Partial<InsertOrganization>): Promise<Organization | undefined>;
|
||||
deleteOrganization(id: number): Promise<boolean>;
|
||||
|
||||
// Domain management
|
||||
getDomain(id: number): Promise<Domain | undefined>;
|
||||
getDomainsByOrganization(organizationId: number): Promise<Domain[]>;
|
||||
getAllDomains(): Promise<Domain[]>;
|
||||
createDomain(domain: InsertDomain): Promise<Domain>;
|
||||
updateDomain(id: number, domain: Partial<InsertDomain>): Promise<Domain | undefined>;
|
||||
deleteDomain(id: number): Promise<boolean>;
|
||||
|
||||
// DNS Record management
|
||||
getDnsRecord(id: number): Promise<DnsRecord | undefined>;
|
||||
getDnsRecordsByDomain(domainId: number): Promise<DnsRecord[]>;
|
||||
createDnsRecord(record: InsertDnsRecord): Promise<DnsRecord>;
|
||||
updateDnsRecord(id: number, record: Partial<InsertDnsRecord>): Promise<DnsRecord | undefined>;
|
||||
deleteDnsRecord(id: number): Promise<boolean>;
|
||||
|
||||
// Provider management
|
||||
getProvider(id: number): Promise<Provider | undefined>;
|
||||
getProviders(): Promise<Provider[]>;
|
||||
createProvider(provider: InsertProvider): Promise<Provider>;
|
||||
updateProvider(id: number, provider: Partial<InsertProvider>): Promise<Provider | undefined>;
|
||||
deleteProvider(id: number): Promise<boolean>;
|
||||
|
||||
// API Token management
|
||||
getApiToken(id: number): Promise<ApiToken | undefined>;
|
||||
getApiTokenByToken(token: string): Promise<ApiToken | undefined>;
|
||||
getApiTokensByOrganization(organizationId: number): Promise<ApiToken[]>;
|
||||
createApiToken(token: InsertApiToken): Promise<ApiToken>;
|
||||
updateApiToken(id: number, token: Partial<InsertApiToken>): Promise<ApiToken | undefined>;
|
||||
deleteApiToken(id: number): Promise<boolean>;
|
||||
|
||||
// DNS History
|
||||
addDnsHistory(recordId: number, action: string, previousValue?: string, newValue?: string, userId?: number): Promise<DnsHistory>;
|
||||
getDnsHistoryByRecord(recordId: number): Promise<DnsHistory[]>;
|
||||
getDnsHistoryByDomain(domainId: number): Promise<DnsHistory[]>;
|
||||
|
||||
// Session store
|
||||
sessionStore: session.SessionStore;
|
||||
}
|
||||
|
||||
export class MemStorage implements IStorage {
|
||||
private usersMap: Map<number, User>;
|
||||
private orgsMap: Map<number, Organization>;
|
||||
private domainsMap: Map<number, Domain>;
|
||||
private recordsMap: Map<number, DnsRecord>;
|
||||
private providersMap: Map<number, Provider>;
|
||||
private apiTokensMap: Map<number, ApiToken>;
|
||||
private historyMap: Map<number, DnsHistory>;
|
||||
|
||||
// Counters for IDs
|
||||
private userIdCounter: number;
|
||||
private orgIdCounter: number;
|
||||
private domainIdCounter: number;
|
||||
private recordIdCounter: number;
|
||||
private providerIdCounter: number;
|
||||
private apiTokenIdCounter: number;
|
||||
private historyIdCounter: number;
|
||||
|
||||
public sessionStore: session.SessionStore;
|
||||
|
||||
constructor() {
|
||||
this.usersMap = new Map();
|
||||
this.orgsMap = new Map();
|
||||
this.domainsMap = new Map();
|
||||
this.recordsMap = new Map();
|
||||
this.providersMap = new Map();
|
||||
this.apiTokensMap = new Map();
|
||||
this.historyMap = new Map();
|
||||
|
||||
this.userIdCounter = 1;
|
||||
this.orgIdCounter = 1;
|
||||
this.domainIdCounter = 1;
|
||||
this.recordIdCounter = 1;
|
||||
this.providerIdCounter = 1;
|
||||
this.apiTokenIdCounter = 1;
|
||||
this.historyIdCounter = 1;
|
||||
|
||||
this.sessionStore = new MemoryStore({
|
||||
checkPeriod: 86400000 // prune expired entries every 24h
|
||||
});
|
||||
|
||||
// Initialize sample data
|
||||
this.initSampleData();
|
||||
}
|
||||
|
||||
private initSampleData() {
|
||||
// Create a default provider for Cloudflare
|
||||
const cloudflareProvider: InsertProvider = {
|
||||
name: "Cloudflare",
|
||||
type: "cloudflare",
|
||||
credentials: { apiKey: process.env.CLOUDFLARE_API_KEY || "" },
|
||||
isActive: true
|
||||
};
|
||||
this.createProvider(cloudflareProvider);
|
||||
|
||||
// Create sample organization
|
||||
const defaultOrg: InsertOrganization = {
|
||||
name: "Default Organization",
|
||||
isActive: true
|
||||
};
|
||||
this.createOrganization(defaultOrg);
|
||||
}
|
||||
|
||||
// Users
|
||||
async getUser(id: number): Promise<User | undefined> {
|
||||
return this.usersMap.get(id);
|
||||
}
|
||||
|
||||
async getUserByUsername(username: string): Promise<User | undefined> {
|
||||
return Array.from(this.usersMap.values()).find(
|
||||
(user) => user.username === username,
|
||||
);
|
||||
}
|
||||
|
||||
async getUserByEmail(email: string): Promise<User | undefined> {
|
||||
return Array.from(this.usersMap.values()).find(
|
||||
(user) => user.email === email,
|
||||
);
|
||||
}
|
||||
|
||||
async createUser(user: InsertUser): Promise<User> {
|
||||
const id = this.userIdCounter++;
|
||||
const createdAt = new Date();
|
||||
const newUser: User = { id, ...user, createdAt };
|
||||
this.usersMap.set(id, newUser);
|
||||
return newUser;
|
||||
}
|
||||
|
||||
async updateUser(id: number, userData: Partial<InsertUser>): Promise<User | undefined> {
|
||||
const user = await this.getUser(id);
|
||||
if (!user) return undefined;
|
||||
|
||||
const updatedUser = { ...user, ...userData };
|
||||
this.usersMap.set(id, updatedUser);
|
||||
return updatedUser;
|
||||
}
|
||||
|
||||
async deleteUser(id: number): Promise<boolean> {
|
||||
return this.usersMap.delete(id);
|
||||
}
|
||||
|
||||
// Organizations
|
||||
async getOrganization(id: number): Promise<Organization | undefined> {
|
||||
return this.orgsMap.get(id);
|
||||
}
|
||||
|
||||
async getOrganizations(): Promise<Organization[]> {
|
||||
return Array.from(this.orgsMap.values());
|
||||
}
|
||||
|
||||
async createOrganization(org: InsertOrganization): Promise<Organization> {
|
||||
const id = this.orgIdCounter++;
|
||||
const createdAt = new Date();
|
||||
const newOrg: Organization = { id, ...org, createdAt };
|
||||
this.orgsMap.set(id, newOrg);
|
||||
return newOrg;
|
||||
}
|
||||
|
||||
async updateOrganization(id: number, orgData: Partial<InsertOrganization>): Promise<Organization | undefined> {
|
||||
const org = await this.getOrganization(id);
|
||||
if (!org) return undefined;
|
||||
|
||||
const updatedOrg = { ...org, ...orgData };
|
||||
this.orgsMap.set(id, updatedOrg);
|
||||
return updatedOrg;
|
||||
}
|
||||
|
||||
async deleteOrganization(id: number): Promise<boolean> {
|
||||
return this.orgsMap.delete(id);
|
||||
}
|
||||
|
||||
// Domains
|
||||
async getDomain(id: number): Promise<Domain | undefined> {
|
||||
return this.domainsMap.get(id);
|
||||
}
|
||||
|
||||
async getDomainsByOrganization(organizationId: number): Promise<Domain[]> {
|
||||
return Array.from(this.domainsMap.values())
|
||||
.filter(domain => domain.organizationId === organizationId);
|
||||
}
|
||||
|
||||
async getAllDomains(): Promise<Domain[]> {
|
||||
return Array.from(this.domainsMap.values());
|
||||
}
|
||||
|
||||
async createDomain(domain: InsertDomain): Promise<Domain> {
|
||||
const id = this.domainIdCounter++;
|
||||
const createdAt = new Date();
|
||||
const lastUpdated = new Date();
|
||||
const newDomain: Domain = { id, ...domain, lastUpdated, createdAt };
|
||||
this.domainsMap.set(id, newDomain);
|
||||
return newDomain;
|
||||
}
|
||||
|
||||
async updateDomain(id: number, domainData: Partial<InsertDomain>): Promise<Domain | undefined> {
|
||||
const domain = await this.getDomain(id);
|
||||
if (!domain) return undefined;
|
||||
|
||||
const updatedDomain = {
|
||||
...domain,
|
||||
...domainData,
|
||||
lastUpdated: new Date()
|
||||
};
|
||||
this.domainsMap.set(id, updatedDomain);
|
||||
return updatedDomain;
|
||||
}
|
||||
|
||||
async deleteDomain(id: number): Promise<boolean> {
|
||||
return this.domainsMap.delete(id);
|
||||
}
|
||||
|
||||
// DNS Records
|
||||
async getDnsRecord(id: number): Promise<DnsRecord | undefined> {
|
||||
return this.recordsMap.get(id);
|
||||
}
|
||||
|
||||
async getDnsRecordsByDomain(domainId: number): Promise<DnsRecord[]> {
|
||||
return Array.from(this.recordsMap.values())
|
||||
.filter(record => record.domainId === domainId);
|
||||
}
|
||||
|
||||
async createDnsRecord(record: InsertDnsRecord): Promise<DnsRecord> {
|
||||
const id = this.recordIdCounter++;
|
||||
const createdAt = new Date();
|
||||
const lastUpdated = new Date();
|
||||
const newRecord: DnsRecord = { id, ...record, lastUpdated, createdAt };
|
||||
this.recordsMap.set(id, newRecord);
|
||||
return newRecord;
|
||||
}
|
||||
|
||||
async updateDnsRecord(id: number, recordData: Partial<InsertDnsRecord>): Promise<DnsRecord | undefined> {
|
||||
const record = await this.getDnsRecord(id);
|
||||
if (!record) return undefined;
|
||||
|
||||
const updatedRecord = {
|
||||
...record,
|
||||
...recordData,
|
||||
lastUpdated: new Date()
|
||||
};
|
||||
this.recordsMap.set(id, updatedRecord);
|
||||
return updatedRecord;
|
||||
}
|
||||
|
||||
async deleteDnsRecord(id: number): Promise<boolean> {
|
||||
return this.recordsMap.delete(id);
|
||||
}
|
||||
|
||||
// Providers
|
||||
async getProvider(id: number): Promise<Provider | undefined> {
|
||||
return this.providersMap.get(id);
|
||||
}
|
||||
|
||||
async getProviders(): Promise<Provider[]> {
|
||||
return Array.from(this.providersMap.values());
|
||||
}
|
||||
|
||||
async createProvider(provider: InsertProvider): Promise<Provider> {
|
||||
const id = this.providerIdCounter++;
|
||||
const createdAt = new Date();
|
||||
const newProvider: Provider = { id, ...provider, createdAt };
|
||||
this.providersMap.set(id, newProvider);
|
||||
return newProvider;
|
||||
}
|
||||
|
||||
async updateProvider(id: number, providerData: Partial<InsertProvider>): Promise<Provider | undefined> {
|
||||
const provider = await this.getProvider(id);
|
||||
if (!provider) return undefined;
|
||||
|
||||
const updatedProvider = { ...provider, ...providerData };
|
||||
this.providersMap.set(id, updatedProvider);
|
||||
return updatedProvider;
|
||||
}
|
||||
|
||||
async deleteProvider(id: number): Promise<boolean> {
|
||||
return this.providersMap.delete(id);
|
||||
}
|
||||
|
||||
// API Tokens
|
||||
async getApiToken(id: number): Promise<ApiToken | undefined> {
|
||||
return this.apiTokensMap.get(id);
|
||||
}
|
||||
|
||||
async getApiTokenByToken(token: string): Promise<ApiToken | undefined> {
|
||||
return Array.from(this.apiTokensMap.values())
|
||||
.find(apiToken => apiToken.token === token);
|
||||
}
|
||||
|
||||
async getApiTokensByOrganization(organizationId: number): Promise<ApiToken[]> {
|
||||
return Array.from(this.apiTokensMap.values())
|
||||
.filter(token => token.organizationId === organizationId);
|
||||
}
|
||||
|
||||
async createApiToken(token: InsertApiToken): Promise<ApiToken> {
|
||||
const id = this.apiTokenIdCounter++;
|
||||
const createdAt = new Date();
|
||||
const newToken: ApiToken = { id, ...token, createdAt };
|
||||
this.apiTokensMap.set(id, newToken);
|
||||
return newToken;
|
||||
}
|
||||
|
||||
async updateApiToken(id: number, tokenData: Partial<InsertApiToken>): Promise<ApiToken | undefined> {
|
||||
const token = await this.getApiToken(id);
|
||||
if (!token) return undefined;
|
||||
|
||||
const updatedToken = { ...token, ...tokenData };
|
||||
this.apiTokensMap.set(id, updatedToken);
|
||||
return updatedToken;
|
||||
}
|
||||
|
||||
async deleteApiToken(id: number): Promise<boolean> {
|
||||
return this.apiTokensMap.delete(id);
|
||||
}
|
||||
|
||||
// DNS History
|
||||
async addDnsHistory(
|
||||
recordId: number,
|
||||
action: string,
|
||||
previousValue?: string,
|
||||
newValue?: string,
|
||||
userId?: number
|
||||
): Promise<DnsHistory> {
|
||||
const id = this.historyIdCounter++;
|
||||
const timestamp = new Date();
|
||||
|
||||
const historyEntry: DnsHistory = {
|
||||
id,
|
||||
recordId,
|
||||
action,
|
||||
previousValue,
|
||||
newValue,
|
||||
userId,
|
||||
timestamp
|
||||
};
|
||||
|
||||
this.historyMap.set(id, historyEntry);
|
||||
return historyEntry;
|
||||
}
|
||||
|
||||
async getDnsHistoryByRecord(recordId: number): Promise<DnsHistory[]> {
|
||||
return Array.from(this.historyMap.values())
|
||||
.filter(history => history.recordId === recordId)
|
||||
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
||||
}
|
||||
|
||||
async getDnsHistoryByDomain(domainId: number): Promise<DnsHistory[]> {
|
||||
// Get all records for domain
|
||||
const records = await this.getDnsRecordsByDomain(domainId);
|
||||
const recordIds = records.map(r => r.id);
|
||||
|
||||
// Get history entries for all records
|
||||
return Array.from(this.historyMap.values())
|
||||
.filter(history => recordIds.includes(history.recordId))
|
||||
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
||||
}
|
||||
}
|
||||
|
||||
export const storage = new MemStorage();
|
||||
@@ -0,0 +1,85 @@
|
||||
import express, { type Express } from "express";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { createServer as createViteServer, createLogger } from "vite";
|
||||
import { type Server } from "http";
|
||||
import viteConfig from "../vite.config";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
const viteLogger = createLogger();
|
||||
|
||||
export function log(message: string, source = "express") {
|
||||
const formattedTime = new Date().toLocaleTimeString("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: true,
|
||||
});
|
||||
|
||||
console.log(`${formattedTime} [${source}] ${message}`);
|
||||
}
|
||||
|
||||
export async function setupVite(app: Express, server: Server) {
|
||||
const serverOptions = {
|
||||
middlewareMode: true,
|
||||
hmr: { server },
|
||||
allowedHosts: true,
|
||||
};
|
||||
|
||||
const vite = await createViteServer({
|
||||
...viteConfig,
|
||||
configFile: false,
|
||||
customLogger: {
|
||||
...viteLogger,
|
||||
error: (msg, options) => {
|
||||
viteLogger.error(msg, options);
|
||||
process.exit(1);
|
||||
},
|
||||
},
|
||||
server: serverOptions,
|
||||
appType: "custom",
|
||||
});
|
||||
|
||||
app.use(vite.middlewares);
|
||||
app.use("*", async (req, res, next) => {
|
||||
const url = req.originalUrl;
|
||||
|
||||
try {
|
||||
const clientTemplate = path.resolve(
|
||||
import.meta.dirname,
|
||||
"..",
|
||||
"client",
|
||||
"index.html",
|
||||
);
|
||||
|
||||
// always reload the index.html file from disk incase it changes
|
||||
let template = await fs.promises.readFile(clientTemplate, "utf-8");
|
||||
template = template.replace(
|
||||
`src="/src/main.tsx"`,
|
||||
`src="/src/main.tsx?v=${nanoid()}"`,
|
||||
);
|
||||
const page = await vite.transformIndexHtml(url, template);
|
||||
res.status(200).set({ "Content-Type": "text/html" }).end(page);
|
||||
} catch (e) {
|
||||
vite.ssrFixStacktrace(e as Error);
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function serveStatic(app: Express) {
|
||||
const distPath = path.resolve(import.meta.dirname, "public");
|
||||
|
||||
if (!fs.existsSync(distPath)) {
|
||||
throw new Error(
|
||||
`Could not find the build directory: ${distPath}, make sure to build the client first`,
|
||||
);
|
||||
}
|
||||
|
||||
app.use(express.static(distPath));
|
||||
|
||||
// fall through to index.html if the file doesn't exist
|
||||
app.use("*", (_req, res) => {
|
||||
res.sendFile(path.resolve(distPath, "index.html"));
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user