Initial commit — eLegal Software monorepo

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Leon Serfaty
2026-04-26 02:42:42 -04:00
co-authored by Claude Sonnet 4.6
commit 0700d54225
160 changed files with 22771 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
import fs from 'node:fs';
import { drizzle } from 'drizzle-orm/node-postgres';
import pg from 'pg';
import * as schema from './schema';
export * from './schema';
export { schema };
let _pool: pg.Pool | null = null;
let _db: ReturnType<typeof drizzle<typeof schema>> | null = null;
function stripSslmode(url: string): string {
try {
const u = new URL(url);
u.searchParams.delete('sslmode');
u.searchParams.delete('ssl');
return u.toString();
} catch {
return url;
}
}
export function getPool(): pg.Pool {
if (_pool) return _pool;
const connectionString = process.env.DATABASE_URL;
if (!connectionString) {
throw new Error('DATABASE_URL is not set');
}
const caPath = process.env.DATABASE_CA_CERT_PATH;
const ca = caPath && fs.existsSync(caPath) ? fs.readFileSync(caPath, 'utf8') : undefined;
// Strip sslmode from the URL so our explicit `ssl` option fully controls TLS behavior.
// Without this, pg merges URL-derived settings and may force cert verification even when
// we want to fall back to TLS-without-verification (no CA cert available).
const ssl: pg.PoolConfig['ssl'] = ca
? { ca, rejectUnauthorized: true }
: { rejectUnauthorized: false };
_pool = new pg.Pool({
connectionString: stripSslmode(connectionString),
max: Number(process.env.DATABASE_POOL_MAX ?? 5),
ssl,
});
return _pool;
}
export function getDb() {
if (_db) return _db;
_db = drizzle(getPool(), { schema });
return _db;
}
export type Database = ReturnType<typeof getDb>;
+21
View File
@@ -0,0 +1,21 @@
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import dotenv from 'dotenv';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
dotenv.config({ path: path.resolve(__dirname, '../../../.env') });
import { migrate } from 'drizzle-orm/node-postgres/migrator';
import { getDb, getPool } from './index';
async function main() {
console.log('Running migrations...');
await migrate(getDb(), { migrationsFolder: path.resolve(__dirname, '../migrations') });
console.log('Migrations complete.');
await getPool().end();
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
+103
View File
@@ -0,0 +1,103 @@
import { pgTable, uuid, text, timestamp, inet, boolean, index } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
import { firms } from './firms';
export const users = pgTable(
'users',
{
id: uuid('id').defaultRandom().primaryKey(),
firmId: uuid('firm_id').references(() => firms.id, { onDelete: 'set null' }),
email: text('email').notNull().unique(),
passwordHash: text('password_hash').notNull(),
fullName: text('full_name'),
role: text('role', { enum: ['owner', 'attorney', 'paralegal', 'staff'] })
.notNull()
.default('owner'),
isSuperadmin: boolean('is_superadmin').notNull().default(false),
isSuspended: boolean('is_suspended').notNull().default(false),
emailVerifiedAt: timestamp('email_verified_at', { withTimezone: true }),
totpSecretEnc: text('totp_secret_enc'),
totpEnabled: boolean('totp_enabled').notNull().default(false),
lastSeenAt: timestamp('last_seen_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => ({
firmIdx: index('users_firm_idx').on(t.firmId),
}),
);
export const usersRelations = relations(users, ({ one }) => ({
firm: one(firms, { fields: [users.firmId], references: [firms.id] }),
}));
export const sessions = pgTable(
'sessions',
{
// Hash of the cookie token (sha256). Cookie holds the raw token; DB stores the hash.
id: text('id').primaryKey(),
userId: uuid('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
ip: inet('ip'),
userAgent: text('user_agent'),
lastSeenAt: timestamp('last_seen_at', { withTimezone: true }).notNull().defaultNow(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => ({
userIdx: index('sessions_user_idx').on(t.userId),
expiresIdx: index('sessions_expires_idx').on(t.expiresAt),
}),
);
export const emailVerifications = pgTable('email_verifications', {
tokenHash: text('token_hash').primaryKey(),
userId: uuid('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
consumedAt: timestamp('consumed_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
});
export const passwordResets = pgTable('password_resets', {
tokenHash: text('token_hash').primaryKey(),
userId: uuid('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
consumedAt: timestamp('consumed_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
});
export const loginAttempts = pgTable(
'login_attempts',
{
id: uuid('id').defaultRandom().primaryKey(),
email: text('email').notNull(),
ip: inet('ip'),
success: boolean('success').notNull().default(false),
attemptedAt: timestamp('attempted_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => ({
emailIdx: index('login_attempts_email_idx').on(t.email, t.attemptedAt),
ipIdx: index('login_attempts_ip_idx').on(t.ip, t.attemptedAt),
}),
);
export const auditLog = pgTable(
'audit_log',
{
id: uuid('id').defaultRandom().primaryKey(),
userId: uuid('user_id').references(() => users.id, { onDelete: 'set null' }),
firmId: uuid('firm_id').references(() => firms.id, { onDelete: 'set null' }),
action: text('action').notNull(),
meta: text('meta'),
ip: inet('ip'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => ({
firmIdx: index('audit_firm_idx').on(t.firmId, t.createdAt),
}),
);
+33
View File
@@ -0,0 +1,33 @@
import { pgTable, uuid, text, timestamp, numeric, index } from 'drizzle-orm/pg-core';
import { firms } from './firms';
import { clients } from './clients';
export const cases = pgTable(
'cases',
{
id: uuid('id').defaultRandom().primaryKey(),
firmId: uuid('firm_id')
.notNull()
.references(() => firms.id, { onDelete: 'cascade' }),
clientId: uuid('client_id')
.notNull()
.references(() => clients.id, { onDelete: 'restrict' }),
title: text('title').notNull(),
caseNumber: text('case_number'),
status: text('status', { enum: ['open', 'pending', 'closed', 'archived'] })
.notNull()
.default('open'),
practiceArea: text('practice_area'),
description: text('description'),
hourlyRate: numeric('hourly_rate', { precision: 10, scale: 2 }),
openedAt: timestamp('opened_at', { withTimezone: true }).notNull().defaultNow(),
closedAt: timestamp('closed_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => ({
firmIdx: index('cases_firm_idx').on(t.firmId),
clientIdx: index('cases_client_idx').on(t.clientId),
statusIdx: index('cases_status_idx').on(t.firmId, t.status),
}),
);
+22
View File
@@ -0,0 +1,22 @@
import { pgTable, uuid, text, timestamp, index } from 'drizzle-orm/pg-core';
import { firms } from './firms';
export const clients = pgTable(
'clients',
{
id: uuid('id').defaultRandom().primaryKey(),
firmId: uuid('firm_id')
.notNull()
.references(() => firms.id, { onDelete: 'cascade' }),
name: text('name').notNull(),
email: text('email'),
phone: text('phone'),
address: text('address'),
notes: text('notes'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => ({
firmIdx: index('clients_firm_idx').on(t.firmId),
}),
);
+27
View File
@@ -0,0 +1,27 @@
import { pgTable, uuid, text, timestamp, integer, index } from 'drizzle-orm/pg-core';
import { firms } from './firms';
import { cases } from './cases';
import { users } from './auth';
export const documents = pgTable(
'documents',
{
id: uuid('id').defaultRandom().primaryKey(),
firmId: uuid('firm_id')
.notNull()
.references(() => firms.id, { onDelete: 'cascade' }),
caseId: uuid('case_id').references(() => cases.id, { onDelete: 'cascade' }),
uploadedBy: uuid('uploaded_by').references(() => users.id, { onDelete: 'set null' }),
name: text('name').notNull(),
storageKey: text('storage_key').notNull(),
mimeType: text('mime_type').notNull(),
sizeBytes: integer('size_bytes').notNull(),
version: integer('version').notNull().default(1),
parentId: uuid('parent_id'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => ({
firmIdx: index('docs_firm_idx').on(t.firmId),
caseIdx: index('docs_case_idx').on(t.caseId),
}),
);
+16
View File
@@ -0,0 +1,16 @@
import { pgTable, uuid, text, timestamp, boolean, integer } from 'drizzle-orm/pg-core';
export const firms = pgTable('firms', {
id: uuid('id').defaultRandom().primaryKey(),
name: text('name').notNull(),
plan: text('plan', { enum: ['starter', 'pro', 'lifetime'] })
.notNull()
.default('starter'),
watermarkEnabled: boolean('watermark_enabled').notNull().default(true),
storageBytesUsed: integer('storage_bytes_used').notNull().default(0),
stripeCustomerId: text('stripe_customer_id'),
stripeSubscriptionId: text('stripe_subscription_id'),
trialEndsAt: timestamp('trial_ends_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
});
+8
View File
@@ -0,0 +1,8 @@
export * from './auth';
export * from './firms';
export * from './clients';
export * from './cases';
export * from './time';
export * from './documents';
export * from './invoices';
export * from './misc';
+53
View File
@@ -0,0 +1,53 @@
import { pgTable, uuid, text, timestamp, numeric, integer, index } from 'drizzle-orm/pg-core';
import { firms } from './firms';
import { clients } from './clients';
import { cases } from './cases';
export const invoices = pgTable(
'invoices',
{
id: uuid('id').defaultRandom().primaryKey(),
firmId: uuid('firm_id')
.notNull()
.references(() => firms.id, { onDelete: 'cascade' }),
clientId: uuid('client_id')
.notNull()
.references(() => clients.id, { onDelete: 'restrict' }),
caseId: uuid('case_id').references(() => cases.id, { onDelete: 'set null' }),
number: text('number').notNull(),
status: text('status', { enum: ['draft', 'sent', 'paid', 'overdue', 'void'] })
.notNull()
.default('draft'),
subtotal: numeric('subtotal', { precision: 12, scale: 2 }).notNull().default('0'),
taxRate: numeric('tax_rate', { precision: 5, scale: 2 }).notNull().default('0'),
total: numeric('total', { precision: 12, scale: 2 }).notNull().default('0'),
notes: text('notes'),
issuedAt: timestamp('issued_at', { withTimezone: true }),
dueAt: timestamp('due_at', { withTimezone: true }),
paidAt: timestamp('paid_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => ({
firmIdx: index('invoices_firm_idx').on(t.firmId),
statusIdx: index('invoices_status_idx').on(t.firmId, t.status),
}),
);
export const invoiceItems = pgTable(
'invoice_items',
{
id: uuid('id').defaultRandom().primaryKey(),
invoiceId: uuid('invoice_id')
.notNull()
.references(() => invoices.id, { onDelete: 'cascade' }),
description: text('description').notNull(),
quantity: numeric('quantity', { precision: 10, scale: 2 }).notNull().default('1'),
rate: numeric('rate', { precision: 10, scale: 2 }).notNull(),
amount: numeric('amount', { precision: 12, scale: 2 }).notNull(),
sortOrder: integer('sort_order').notNull().default(0),
},
(t) => ({
invoiceIdx: index('invoice_items_invoice_idx').on(t.invoiceId),
}),
);
+25
View File
@@ -0,0 +1,25 @@
import { pgTable, uuid, text, timestamp, inet, index } from 'drizzle-orm/pg-core';
export const contactMessages = pgTable('contact_messages', {
id: uuid('id').defaultRandom().primaryKey(),
fullName: text('full_name').notNull(),
email: text('email').notNull(),
message: text('message').notNull(),
ip: inet('ip'),
resolvedAt: timestamp('resolved_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
});
export const toolUsage = pgTable(
'tool_usage',
{
id: uuid('id').defaultRandom().primaryKey(),
tool: text('tool').notNull(),
sessionId: text('session_id'),
ip: inet('ip'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => ({
toolIdx: index('tool_usage_tool_idx').on(t.tool, t.createdAt),
}),
);
+35
View File
@@ -0,0 +1,35 @@
import { pgTable, uuid, text, timestamp, numeric, integer, boolean, index } from 'drizzle-orm/pg-core';
import { firms } from './firms';
import { cases } from './cases';
import { users } from './auth';
export const timeEntries = pgTable(
'time_entries',
{
id: uuid('id').defaultRandom().primaryKey(),
firmId: uuid('firm_id')
.notNull()
.references(() => firms.id, { onDelete: 'cascade' }),
caseId: uuid('case_id')
.notNull()
.references(() => cases.id, { onDelete: 'cascade' }),
userId: uuid('user_id')
.notNull()
.references(() => users.id, { onDelete: 'restrict' }),
description: text('description').notNull(),
startedAt: timestamp('started_at', { withTimezone: true }).notNull(),
endedAt: timestamp('ended_at', { withTimezone: true }),
minutes: integer('minutes').notNull().default(0),
rate: numeric('rate', { precision: 10, scale: 2 }).notNull(),
billable: boolean('billable').notNull().default(true),
invoiceItemId: uuid('invoice_item_id'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => ({
firmIdx: index('time_firm_idx').on(t.firmId),
caseIdx: index('time_case_idx').on(t.caseId),
userIdx: index('time_user_idx').on(t.userId, t.startedAt),
unbilledIdx: index('time_unbilled_idx').on(t.firmId, t.invoiceItemId),
}),
);