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:
alphaeusmote
2025-04-09 21:34:26 +00:00
parent e1340a29cb
commit cba930e4d3
99 changed files with 21903 additions and 0 deletions
+205
View File
@@ -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);
});
}
+70
View File
@@ -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}`);
});
})();
+672
View File
@@ -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;
}
+389
View File
@@ -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();
+85
View File
@@ -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"));
});
}