mirror of
https://github.com/freedbygrace/DynamoDNS.git
synced 2026-07-27 20:08:56 +00:00
Migrate storage from in-memory to PostgreSQL database.
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/37883948-9924-4429-8878-f2530cf6fe2b.jpg
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
{pkgs}: {
|
||||
deps = [
|
||||
pkgs.postgresql
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
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 { db } from "./db";
|
||||
import { eq, and, desc, or, inArray, sql } from "drizzle-orm";
|
||||
import { IStorage } from "./storage";
|
||||
import session from "express-session";
|
||||
import connectPg from "connect-pg-simple";
|
||||
import { pool } from "./db";
|
||||
|
||||
const PostgresSessionStore = connectPg(session);
|
||||
|
||||
export class DatabaseStorage implements IStorage {
|
||||
public sessionStore: any;
|
||||
|
||||
constructor() {
|
||||
this.sessionStore = new PostgresSessionStore({
|
||||
pool,
|
||||
createTableIfMissing: true
|
||||
});
|
||||
}
|
||||
|
||||
// User management
|
||||
async getUser(id: number): Promise<User | undefined> {
|
||||
const [user] = await db.select().from(users).where(eq(users.id, id));
|
||||
return user;
|
||||
}
|
||||
|
||||
async getUserByUsername(username: string): Promise<User | undefined> {
|
||||
const [user] = await db.select().from(users).where(eq(users.username, username));
|
||||
return user;
|
||||
}
|
||||
|
||||
async getUserByEmail(email: string): Promise<User | undefined> {
|
||||
const [user] = await db.select().from(users).where(eq(users.email, email));
|
||||
return user;
|
||||
}
|
||||
|
||||
async createUser(user: InsertUser): Promise<User> {
|
||||
const [newUser] = await db.insert(users).values(user).returning();
|
||||
return newUser;
|
||||
}
|
||||
|
||||
async updateUser(id: number, userData: Partial<InsertUser>): Promise<User | undefined> {
|
||||
const [updatedUser] = await db.update(users)
|
||||
.set(userData)
|
||||
.where(eq(users.id, id))
|
||||
.returning();
|
||||
return updatedUser;
|
||||
}
|
||||
|
||||
async deleteUser(id: number): Promise<boolean> {
|
||||
const result = await db.delete(users).where(eq(users.id, id)).returning();
|
||||
return result.length > 0;
|
||||
}
|
||||
|
||||
// Organization management
|
||||
async getOrganization(id: number): Promise<Organization | undefined> {
|
||||
const [org] = await db.select().from(organizations).where(eq(organizations.id, id));
|
||||
return org;
|
||||
}
|
||||
|
||||
async getOrganizations(): Promise<Organization[]> {
|
||||
return await db.select().from(organizations);
|
||||
}
|
||||
|
||||
async createOrganization(org: InsertOrganization): Promise<Organization> {
|
||||
const [newOrg] = await db.insert(organizations).values(org).returning();
|
||||
return newOrg;
|
||||
}
|
||||
|
||||
async updateOrganization(id: number, orgData: Partial<InsertOrganization>): Promise<Organization | undefined> {
|
||||
const [updatedOrg] = await db.update(organizations)
|
||||
.set(orgData)
|
||||
.where(eq(organizations.id, id))
|
||||
.returning();
|
||||
return updatedOrg;
|
||||
}
|
||||
|
||||
async deleteOrganization(id: number): Promise<boolean> {
|
||||
const result = await db.delete(organizations).where(eq(organizations.id, id)).returning();
|
||||
return result.length > 0;
|
||||
}
|
||||
|
||||
// Domain management
|
||||
async getDomain(id: number): Promise<Domain | undefined> {
|
||||
const [domain] = await db.select().from(domains).where(eq(domains.id, id));
|
||||
return domain;
|
||||
}
|
||||
|
||||
async getDomainsByOrganization(organizationId: number): Promise<Domain[]> {
|
||||
return await db.select()
|
||||
.from(domains)
|
||||
.where(eq(domains.organizationId, organizationId));
|
||||
}
|
||||
|
||||
async getAllDomains(): Promise<Domain[]> {
|
||||
return await db.select().from(domains);
|
||||
}
|
||||
|
||||
async createDomain(domain: InsertDomain): Promise<Domain> {
|
||||
const [newDomain] = await db.insert(domains).values(domain).returning();
|
||||
return newDomain;
|
||||
}
|
||||
|
||||
async updateDomain(id: number, domainData: Partial<InsertDomain>): Promise<Domain | undefined> {
|
||||
const [updatedDomain] = await db.update(domains)
|
||||
.set({ ...domainData, lastUpdated: new Date() })
|
||||
.where(eq(domains.id, id))
|
||||
.returning();
|
||||
return updatedDomain;
|
||||
}
|
||||
|
||||
async deleteDomain(id: number): Promise<boolean> {
|
||||
const result = await db.delete(domains).where(eq(domains.id, id)).returning();
|
||||
return result.length > 0;
|
||||
}
|
||||
|
||||
// DNS Record management
|
||||
async getDnsRecord(id: number): Promise<DnsRecord | undefined> {
|
||||
const [record] = await db.select().from(dnsRecords).where(eq(dnsRecords.id, id));
|
||||
return record;
|
||||
}
|
||||
|
||||
async getDnsRecordsByDomain(domainId: number): Promise<DnsRecord[]> {
|
||||
return await db.select()
|
||||
.from(dnsRecords)
|
||||
.where(eq(dnsRecords.domainId, domainId));
|
||||
}
|
||||
|
||||
async createDnsRecord(record: InsertDnsRecord): Promise<DnsRecord> {
|
||||
const [newRecord] = await db.insert(dnsRecords).values(record).returning();
|
||||
return newRecord;
|
||||
}
|
||||
|
||||
async updateDnsRecord(id: number, recordData: Partial<InsertDnsRecord>): Promise<DnsRecord | undefined> {
|
||||
const [updatedRecord] = await db.update(dnsRecords)
|
||||
.set({ ...recordData, lastUpdated: new Date() })
|
||||
.where(eq(dnsRecords.id, id))
|
||||
.returning();
|
||||
return updatedRecord;
|
||||
}
|
||||
|
||||
async deleteDnsRecord(id: number): Promise<boolean> {
|
||||
const result = await db.delete(dnsRecords).where(eq(dnsRecords.id, id)).returning();
|
||||
return result.length > 0;
|
||||
}
|
||||
|
||||
// Provider management
|
||||
async getProvider(id: number): Promise<Provider | undefined> {
|
||||
const [provider] = await db.select().from(providers).where(eq(providers.id, id));
|
||||
return provider;
|
||||
}
|
||||
|
||||
async getProviders(): Promise<Provider[]> {
|
||||
return await db.select().from(providers);
|
||||
}
|
||||
|
||||
async createProvider(provider: InsertProvider): Promise<Provider> {
|
||||
const [newProvider] = await db.insert(providers).values(provider).returning();
|
||||
return newProvider;
|
||||
}
|
||||
|
||||
async updateProvider(id: number, providerData: Partial<InsertProvider>): Promise<Provider | undefined> {
|
||||
const [updatedProvider] = await db.update(providers)
|
||||
.set(providerData)
|
||||
.where(eq(providers.id, id))
|
||||
.returning();
|
||||
return updatedProvider;
|
||||
}
|
||||
|
||||
async deleteProvider(id: number): Promise<boolean> {
|
||||
const result = await db.delete(providers).where(eq(providers.id, id)).returning();
|
||||
return result.length > 0;
|
||||
}
|
||||
|
||||
// API Token management
|
||||
async getApiToken(id: number): Promise<ApiToken | undefined> {
|
||||
const [token] = await db.select().from(apiTokens).where(eq(apiTokens.id, id));
|
||||
return token;
|
||||
}
|
||||
|
||||
async getApiTokenByToken(token: string): Promise<ApiToken | undefined> {
|
||||
const [apiToken] = await db.select().from(apiTokens).where(eq(apiTokens.token, token));
|
||||
return apiToken;
|
||||
}
|
||||
|
||||
async getApiTokensByOrganization(organizationId: number): Promise<ApiToken[]> {
|
||||
return await db.select()
|
||||
.from(apiTokens)
|
||||
.where(eq(apiTokens.organizationId, organizationId));
|
||||
}
|
||||
|
||||
async createApiToken(token: InsertApiToken): Promise<ApiToken> {
|
||||
const [newToken] = await db.insert(apiTokens).values(token).returning();
|
||||
return newToken;
|
||||
}
|
||||
|
||||
async updateApiToken(id: number, tokenData: Partial<InsertApiToken>): Promise<ApiToken | undefined> {
|
||||
const [updatedToken] = await db.update(apiTokens)
|
||||
.set(tokenData)
|
||||
.where(eq(apiTokens.id, id))
|
||||
.returning();
|
||||
return updatedToken;
|
||||
}
|
||||
|
||||
async deleteApiToken(id: number): Promise<boolean> {
|
||||
const result = await db.delete(apiTokens).where(eq(apiTokens.id, id)).returning();
|
||||
return result.length > 0;
|
||||
}
|
||||
|
||||
// DNS History
|
||||
async addDnsHistory(
|
||||
recordId: number,
|
||||
action: string,
|
||||
previousValue?: string,
|
||||
newValue?: string,
|
||||
userId?: number
|
||||
): Promise<DnsHistory> {
|
||||
const [historyEntry] = await db.insert(dnsHistory).values({
|
||||
recordId,
|
||||
action,
|
||||
previousValue,
|
||||
newValue,
|
||||
userId,
|
||||
}).returning();
|
||||
|
||||
return historyEntry;
|
||||
}
|
||||
|
||||
async getDnsHistoryByRecord(recordId: number): Promise<DnsHistory[]> {
|
||||
return await db.select()
|
||||
.from(dnsHistory)
|
||||
.where(eq(dnsHistory.recordId, recordId))
|
||||
.orderBy(desc(dnsHistory.timestamp));
|
||||
}
|
||||
|
||||
async getDnsHistoryByDomain(domainId: number): Promise<DnsHistory[]> {
|
||||
// We need to get all records for the domain first
|
||||
const records = await this.getDnsRecordsByDomain(domainId);
|
||||
const recordIds = records.map(r => r.id);
|
||||
|
||||
if (recordIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Use SQL for the IN condition
|
||||
return await db.select()
|
||||
.from(dnsHistory)
|
||||
.where(
|
||||
sql`${dnsHistory.recordId} IN (${sql.join(recordIds, sql`, `)})`
|
||||
)
|
||||
.orderBy(desc(dnsHistory.timestamp));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Pool, neonConfig } from '@neondatabase/serverless';
|
||||
import { drizzle } from 'drizzle-orm/neon-serverless';
|
||||
import ws from "ws";
|
||||
import * as schema from "@shared/schema";
|
||||
|
||||
neonConfig.webSocketConstructor = ws;
|
||||
|
||||
if (!process.env.DATABASE_URL) {
|
||||
throw new Error(
|
||||
"DATABASE_URL must be set. Did you forget to provision a database?",
|
||||
);
|
||||
}
|
||||
|
||||
export const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
||||
export const db = drizzle(pool, { schema });
|
||||
+6
-12
@@ -1,7 +1,4 @@
|
||||
import { randomBytes } from "crypto";
|
||||
import {
|
||||
users, organizations, domains, dnsRecords,
|
||||
providers, dnsHistory, apiTokens,
|
||||
type User, type InsertUser,
|
||||
type Organization, type InsertOrganization,
|
||||
type Domain, type InsertDomain,
|
||||
@@ -11,9 +8,7 @@ import {
|
||||
type DnsHistory
|
||||
} from "@shared/schema";
|
||||
import session from "express-session";
|
||||
import createMemoryStore from "memorystore";
|
||||
|
||||
const MemoryStore = createMemoryStore(session);
|
||||
import { DatabaseStorage } from "./database-storage";
|
||||
|
||||
export interface IStorage {
|
||||
// User management
|
||||
@@ -67,7 +62,7 @@ export interface IStorage {
|
||||
getDnsHistoryByDomain(domainId: number): Promise<DnsHistory[]>;
|
||||
|
||||
// Session store
|
||||
sessionStore: session.SessionStore;
|
||||
sessionStore: any;
|
||||
}
|
||||
|
||||
export class MemStorage implements IStorage {
|
||||
@@ -88,7 +83,7 @@ export class MemStorage implements IStorage {
|
||||
private apiTokenIdCounter: number;
|
||||
private historyIdCounter: number;
|
||||
|
||||
public sessionStore: session.SessionStore;
|
||||
public sessionStore: any;
|
||||
|
||||
constructor() {
|
||||
this.usersMap = new Map();
|
||||
@@ -107,9 +102,8 @@ export class MemStorage implements IStorage {
|
||||
this.apiTokenIdCounter = 1;
|
||||
this.historyIdCounter = 1;
|
||||
|
||||
this.sessionStore = new MemoryStore({
|
||||
checkPeriod: 86400000 // prune expired entries every 24h
|
||||
});
|
||||
// Session store is created in the DatabaseStorage class
|
||||
this.sessionStore = null;
|
||||
|
||||
// Initialize sample data
|
||||
this.initSampleData();
|
||||
@@ -386,4 +380,4 @@ export class MemStorage implements IStorage {
|
||||
}
|
||||
}
|
||||
|
||||
export const storage = new MemStorage();
|
||||
export const storage = new DatabaseStorage();
|
||||
|
||||
+91
-28
@@ -1,19 +1,9 @@
|
||||
import { pgTable, text, serial, integer, boolean, timestamp, jsonb } from "drizzle-orm/pg-core";
|
||||
import { pgTable, text, serial, integer, boolean, timestamp, jsonb, foreignKey, unique } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
import { relations } from "drizzle-orm";
|
||||
|
||||
// Users & Authentication
|
||||
export const users = pgTable("users", {
|
||||
id: serial("id").primaryKey(),
|
||||
username: text("username").notNull().unique(),
|
||||
password: text("password").notNull(),
|
||||
email: text("email").notNull().unique(),
|
||||
fullName: text("full_name"),
|
||||
role: text("role").default("user").notNull(),
|
||||
organizationId: integer("organization_id"),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const organizations = pgTable("organizations", {
|
||||
id: serial("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
@@ -21,12 +11,41 @@ export const organizations = pgTable("organizations", {
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const users = pgTable("users", {
|
||||
id: serial("id").primaryKey(),
|
||||
username: text("username").notNull().unique(),
|
||||
password: text("password").notNull(),
|
||||
email: text("email").notNull().unique(),
|
||||
fullName: text("full_name"),
|
||||
role: text("role").default("user").notNull(),
|
||||
organizationId: integer("organization_id").references(() => organizations.id, { onDelete: "set null" }),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const usersRelations = relations(users, ({ one }) => ({
|
||||
organization: one(organizations, {
|
||||
fields: [users.organizationId],
|
||||
references: [organizations.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
// We'll define this after all tables are declared
|
||||
|
||||
// DNS Management
|
||||
export const providers = pgTable("providers", {
|
||||
id: serial("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
type: text("type").notNull(),
|
||||
credentials: jsonb("credentials"),
|
||||
isActive: boolean("is_active").default(true).notNull(),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const domains = pgTable("domains", {
|
||||
id: serial("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
organizationId: integer("organization_id").notNull(),
|
||||
providerId: integer("provider_id").notNull(),
|
||||
organizationId: integer("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
|
||||
providerId: integer("provider_id").notNull().references(() => providers.id, { onDelete: "cascade" }),
|
||||
isActive: boolean("is_active").default(true).notNull(),
|
||||
lastUpdated: timestamp("last_updated"),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
@@ -34,7 +53,7 @@ export const domains = pgTable("domains", {
|
||||
|
||||
export const dnsRecords = pgTable("dns_records", {
|
||||
id: serial("id").primaryKey(),
|
||||
domainId: integer("domain_id").notNull(),
|
||||
domainId: integer("domain_id").notNull().references(() => domains.id, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
type: text("type").notNull(),
|
||||
content: text("content").notNull(),
|
||||
@@ -45,22 +64,13 @@ export const dnsRecords = pgTable("dns_records", {
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const providers = pgTable("providers", {
|
||||
id: serial("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
type: text("type").notNull(),
|
||||
credentials: jsonb("credentials"),
|
||||
isActive: boolean("is_active").default(true).notNull(),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const dnsHistory = pgTable("dns_history", {
|
||||
id: serial("id").primaryKey(),
|
||||
recordId: integer("record_id").notNull(),
|
||||
recordId: integer("record_id").notNull().references(() => dnsRecords.id, { onDelete: "cascade" }),
|
||||
action: text("action").notNull(),
|
||||
previousValue: text("previous_value"),
|
||||
newValue: text("new_value"),
|
||||
userId: integer("user_id"),
|
||||
userId: integer("user_id").references(() => users.id, { onDelete: "set null" }),
|
||||
timestamp: timestamp("timestamp").defaultNow().notNull(),
|
||||
});
|
||||
|
||||
@@ -69,9 +79,9 @@ export const apiTokens = pgTable("api_tokens", {
|
||||
id: serial("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
token: text("token").notNull().unique(),
|
||||
organizationId: integer("organization_id").notNull(),
|
||||
organizationId: integer("organization_id").notNull().references(() => organizations.id, { onDelete: "cascade" }),
|
||||
permissions: text("permissions").array(),
|
||||
createdBy: integer("created_by").notNull(),
|
||||
createdBy: integer("created_by").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||||
isActive: boolean("is_active").default(true).notNull(),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
expiresAt: timestamp("expires_at"),
|
||||
@@ -158,3 +168,56 @@ export type ProviderType = typeof providerTypes[number];
|
||||
// DNS Record Types
|
||||
export const recordTypes = ['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'SRV', 'NS', 'CAA', 'PTR'] as const;
|
||||
export type RecordType = typeof recordTypes[number];
|
||||
|
||||
// Define table relations
|
||||
export const organizationsRelations = relations(organizations, ({ many }) => ({
|
||||
users: many(users),
|
||||
domains: many(domains),
|
||||
apiTokens: many(apiTokens),
|
||||
}));
|
||||
|
||||
export const domainsRelations = relations(domains, ({ one, many }) => ({
|
||||
organization: one(organizations, {
|
||||
fields: [domains.organizationId],
|
||||
references: [organizations.id],
|
||||
}),
|
||||
provider: one(providers, {
|
||||
fields: [domains.providerId],
|
||||
references: [providers.id],
|
||||
}),
|
||||
dnsRecords: many(dnsRecords),
|
||||
}));
|
||||
|
||||
export const dnsRecordsRelations = relations(dnsRecords, ({ one, many }) => ({
|
||||
domain: one(domains, {
|
||||
fields: [dnsRecords.domainId],
|
||||
references: [domains.id],
|
||||
}),
|
||||
history: many(dnsHistory),
|
||||
}));
|
||||
|
||||
export const dnsHistoryRelations = relations(dnsHistory, ({ one }) => ({
|
||||
record: one(dnsRecords, {
|
||||
fields: [dnsHistory.recordId],
|
||||
references: [dnsRecords.id],
|
||||
}),
|
||||
user: one(users, {
|
||||
fields: [dnsHistory.userId],
|
||||
references: [users.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const providersRelations = relations(providers, ({ many }) => ({
|
||||
domains: many(domains),
|
||||
}));
|
||||
|
||||
export const apiTokensRelations = relations(apiTokens, ({ one }) => ({
|
||||
organization: one(organizations, {
|
||||
fields: [apiTokens.organizationId],
|
||||
references: [organizations.id],
|
||||
}),
|
||||
creator: one(users, {
|
||||
fields: [apiTokens.createdBy],
|
||||
references: [users.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user