Remove multi-tenant company_id scoping from entire codebase
Drop company_id column from all 22 domain tables via migration. Remove companyId from JWT payload, auth plugins, all service method signatures (~215 occurrences), all route handlers (~105 occurrences), test runner, test suites, and frontend auth store/types. The company table stays as store settings (name, timezone). Tenant isolation in a SaaS deployment would be at the database level (one DB per customer) not the application level. All 107 API tests pass. Zero TSC errors across all packages.
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
-- Remove company_id columns and FK constraints from all domain tables.
|
||||
-- The company table stays as store settings, but tenant scoping is removed.
|
||||
-- In a SaaS deployment, isolation is at the database level, not application level.
|
||||
|
||||
-- Drop company_id from each table (CASCADE drops the FK constraint automatically)
|
||||
ALTER TABLE "account" DROP COLUMN IF EXISTS "company_id";
|
||||
ALTER TABLE "member" DROP COLUMN IF EXISTS "company_id";
|
||||
ALTER TABLE "member_identifier" DROP COLUMN IF EXISTS "company_id";
|
||||
ALTER TABLE "account_processor_link" DROP COLUMN IF EXISTS "company_id";
|
||||
ALTER TABLE "account_payment_method" DROP COLUMN IF EXISTS "company_id";
|
||||
ALTER TABLE "tax_exemption" DROP COLUMN IF EXISTS "company_id";
|
||||
ALTER TABLE "file" DROP COLUMN IF EXISTS "company_id";
|
||||
ALTER TABLE "category" DROP COLUMN IF EXISTS "company_id";
|
||||
ALTER TABLE "supplier" DROP COLUMN IF EXISTS "company_id";
|
||||
ALTER TABLE "product" DROP COLUMN IF EXISTS "company_id";
|
||||
ALTER TABLE "inventory_unit" DROP COLUMN IF EXISTS "company_id";
|
||||
ALTER TABLE "stock_receipt" DROP COLUMN IF EXISTS "company_id";
|
||||
ALTER TABLE "price_history" DROP COLUMN IF EXISTS "company_id";
|
||||
ALTER TABLE "consignment_detail" DROP COLUMN IF EXISTS "company_id";
|
||||
ALTER TABLE "inventory_unit_status" DROP COLUMN IF EXISTS "company_id";
|
||||
ALTER TABLE "item_condition" DROP COLUMN IF EXISTS "company_id";
|
||||
ALTER TABLE "role" DROP COLUMN IF EXISTS "company_id";
|
||||
ALTER TABLE "repair_batch" DROP COLUMN IF EXISTS "company_id";
|
||||
ALTER TABLE "repair_ticket" DROP COLUMN IF EXISTS "company_id";
|
||||
ALTER TABLE "repair_service_template" DROP COLUMN IF EXISTS "company_id";
|
||||
ALTER TABLE "location" DROP COLUMN IF EXISTS "company_id";
|
||||
ALTER TABLE "user" DROP COLUMN IF EXISTS "company_id";
|
||||
@@ -148,6 +148,13 @@
|
||||
"when": 1774800000000,
|
||||
"tag": "0020_repair_default_new",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 21,
|
||||
"version": "7",
|
||||
"when": 1774810000000,
|
||||
"tag": "0021_remove_company_scoping",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -10,16 +10,12 @@ import {
|
||||
date,
|
||||
pgEnum,
|
||||
} from 'drizzle-orm/pg-core'
|
||||
import { companies } from './stores.js'
|
||||
|
||||
export const billingModeEnum = pgEnum('billing_mode', ['consolidated', 'split'])
|
||||
export const taxExemptStatusEnum = pgEnum('tax_exempt_status', ['none', 'pending', 'approved'])
|
||||
|
||||
export const accounts = pgTable('account', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
companyId: uuid('company_id')
|
||||
.notNull()
|
||||
.references(() => companies.id),
|
||||
accountNumber: varchar('account_number', { length: 50 }),
|
||||
name: varchar('name', { length: 255 }).notNull(),
|
||||
email: varchar('email', { length: 255 }),
|
||||
@@ -46,9 +42,6 @@ export const members = pgTable('member', {
|
||||
accountId: uuid('account_id')
|
||||
.notNull()
|
||||
.references(() => accounts.id),
|
||||
companyId: uuid('company_id')
|
||||
.notNull()
|
||||
.references(() => companies.id),
|
||||
memberNumber: varchar('member_number', { length: 50 }),
|
||||
firstName: varchar('first_name', { length: 100 }).notNull(),
|
||||
lastName: varchar('last_name', { length: 100 }).notNull(),
|
||||
@@ -73,9 +66,6 @@ export const memberIdentifiers = pgTable('member_identifier', {
|
||||
memberId: uuid('member_id')
|
||||
.notNull()
|
||||
.references(() => members.id),
|
||||
companyId: uuid('company_id')
|
||||
.notNull()
|
||||
.references(() => companies.id),
|
||||
type: varchar('type', { length: 50 }).notNull(),
|
||||
label: varchar('label', { length: 100 }),
|
||||
value: varchar('value', { length: 255 }).notNull(),
|
||||
@@ -100,9 +90,6 @@ export const accountProcessorLinks = pgTable('account_processor_link', {
|
||||
accountId: uuid('account_id')
|
||||
.notNull()
|
||||
.references(() => accounts.id),
|
||||
companyId: uuid('company_id')
|
||||
.notNull()
|
||||
.references(() => companies.id),
|
||||
processor: processorEnum('processor').notNull(),
|
||||
processorCustomerId: varchar('processor_customer_id', { length: 255 }).notNull(),
|
||||
isActive: boolean('is_active').notNull().default(true),
|
||||
@@ -117,9 +104,6 @@ export const accountPaymentMethods = pgTable('account_payment_method', {
|
||||
accountId: uuid('account_id')
|
||||
.notNull()
|
||||
.references(() => accounts.id),
|
||||
companyId: uuid('company_id')
|
||||
.notNull()
|
||||
.references(() => companies.id),
|
||||
processor: processorEnum('processor').notNull(),
|
||||
processorPaymentMethodId: varchar('processor_payment_method_id', { length: 255 }).notNull(),
|
||||
cardBrand: varchar('card_brand', { length: 50 }),
|
||||
@@ -139,9 +123,6 @@ export const taxExemptions = pgTable('tax_exemption', {
|
||||
accountId: uuid('account_id')
|
||||
.notNull()
|
||||
.references(() => accounts.id),
|
||||
companyId: uuid('company_id')
|
||||
.notNull()
|
||||
.references(() => companies.id),
|
||||
status: taxExemptStatusEnum('status').notNull().default('pending'),
|
||||
certificateNumber: varchar('certificate_number', { length: 255 }).notNull(),
|
||||
certificateType: varchar('certificate_type', { length: 100 }),
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { pgTable, uuid, varchar, integer, timestamp } from 'drizzle-orm/pg-core'
|
||||
import { companies } from './stores.js'
|
||||
|
||||
export const files = pgTable('file', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
companyId: uuid('company_id')
|
||||
.notNull()
|
||||
.references(() => companies.id),
|
||||
path: varchar('path', { length: 1000 }).notNull(),
|
||||
filename: varchar('filename', { length: 255 }).notNull(),
|
||||
contentType: varchar('content_type', { length: 100 }).notNull(),
|
||||
|
||||
@@ -9,13 +9,10 @@ import {
|
||||
numeric,
|
||||
date,
|
||||
} from 'drizzle-orm/pg-core'
|
||||
import { companies, locations } from './stores.js'
|
||||
import { locations } from './stores.js'
|
||||
|
||||
export const categories = pgTable('category', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
companyId: uuid('company_id')
|
||||
.notNull()
|
||||
.references(() => companies.id),
|
||||
parentId: uuid('parent_id'),
|
||||
name: varchar('name', { length: 255 }).notNull(),
|
||||
description: text('description'),
|
||||
@@ -27,9 +24,6 @@ export const categories = pgTable('category', {
|
||||
|
||||
export const suppliers = pgTable('supplier', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
companyId: uuid('company_id')
|
||||
.notNull()
|
||||
.references(() => companies.id),
|
||||
name: varchar('name', { length: 255 }).notNull(),
|
||||
contactName: varchar('contact_name', { length: 255 }),
|
||||
email: varchar('email', { length: 255 }),
|
||||
@@ -49,9 +43,6 @@ export const suppliers = pgTable('supplier', {
|
||||
|
||||
export const products = pgTable('product', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
companyId: uuid('company_id')
|
||||
.notNull()
|
||||
.references(() => companies.id),
|
||||
locationId: uuid('location_id').references(() => locations.id),
|
||||
sku: varchar('sku', { length: 100 }),
|
||||
upc: varchar('upc', { length: 100 }),
|
||||
@@ -79,9 +70,6 @@ export const inventoryUnits = pgTable('inventory_unit', {
|
||||
productId: uuid('product_id')
|
||||
.notNull()
|
||||
.references(() => products.id),
|
||||
companyId: uuid('company_id')
|
||||
.notNull()
|
||||
.references(() => companies.id),
|
||||
locationId: uuid('location_id').references(() => locations.id),
|
||||
serialNumber: varchar('serial_number', { length: 255 }),
|
||||
condition: varchar('condition', { length: 100 }).notNull().default('new'),
|
||||
@@ -112,9 +100,6 @@ export type Supplier = typeof suppliers.$inferSelect
|
||||
export type SupplierInsert = typeof suppliers.$inferInsert
|
||||
export const stockReceipts = pgTable('stock_receipt', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
companyId: uuid('company_id')
|
||||
.notNull()
|
||||
.references(() => companies.id),
|
||||
locationId: uuid('location_id').references(() => locations.id),
|
||||
productId: uuid('product_id')
|
||||
.notNull()
|
||||
@@ -136,9 +121,6 @@ export const priceHistory = pgTable('price_history', {
|
||||
productId: uuid('product_id')
|
||||
.notNull()
|
||||
.references(() => products.id),
|
||||
companyId: uuid('company_id')
|
||||
.notNull()
|
||||
.references(() => companies.id),
|
||||
previousPrice: numeric('previous_price', { precision: 10, scale: 2 }),
|
||||
newPrice: numeric('new_price', { precision: 10, scale: 2 }).notNull(),
|
||||
previousMinPrice: numeric('previous_min_price', { precision: 10, scale: 2 }),
|
||||
@@ -158,9 +140,6 @@ export const consignmentDetails = pgTable('consignment_detail', {
|
||||
productId: uuid('product_id')
|
||||
.notNull()
|
||||
.references(() => products.id),
|
||||
companyId: uuid('company_id')
|
||||
.notNull()
|
||||
.references(() => companies.id),
|
||||
consignorAccountId: uuid('consignor_account_id').notNull(),
|
||||
commissionPercent: numeric('commission_percent', { precision: 5, scale: 2 }).notNull(),
|
||||
minPrice: numeric('min_price', { precision: 10, scale: 2 }),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { pgTable, uuid, varchar, text, timestamp, boolean, integer } from 'drizzle-orm/pg-core'
|
||||
import { companies } from './stores.js'
|
||||
|
||||
/**
|
||||
* Lookup tables replace hard-coded pgEnums for values that stores may want to customize.
|
||||
@@ -9,9 +8,6 @@ import { companies } from './stores.js'
|
||||
|
||||
export const inventoryUnitStatuses = pgTable('inventory_unit_status', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
companyId: uuid('company_id')
|
||||
.notNull()
|
||||
.references(() => companies.id),
|
||||
name: varchar('name', { length: 100 }).notNull(),
|
||||
slug: varchar('slug', { length: 100 }).notNull(),
|
||||
description: text('description'),
|
||||
@@ -23,9 +19,6 @@ export const inventoryUnitStatuses = pgTable('inventory_unit_status', {
|
||||
|
||||
export const itemConditions = pgTable('item_condition', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
companyId: uuid('company_id')
|
||||
.notNull()
|
||||
.references(() => companies.id),
|
||||
name: varchar('name', { length: 100 }).notNull(),
|
||||
slug: varchar('slug', { length: 100 }).notNull(),
|
||||
description: text('description'),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { pgTable, uuid, varchar, text, timestamp, boolean, uniqueIndex } from 'drizzle-orm/pg-core'
|
||||
import { companies } from './stores.js'
|
||||
import { users } from './users.js'
|
||||
|
||||
export const permissions = pgTable('permission', {
|
||||
@@ -13,9 +12,6 @@ export const permissions = pgTable('permission', {
|
||||
|
||||
export const roles = pgTable('role', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
companyId: uuid('company_id')
|
||||
.notNull()
|
||||
.references(() => companies.id),
|
||||
name: varchar('name', { length: 100 }).notNull(),
|
||||
slug: varchar('slug', { length: 100 }).notNull(),
|
||||
description: text('description'),
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
numeric,
|
||||
pgEnum,
|
||||
} from 'drizzle-orm/pg-core'
|
||||
import { companies, locations } from './stores.js'
|
||||
import { locations } from './stores.js'
|
||||
import { accounts } from './accounts.js'
|
||||
import { inventoryUnits, products } from './inventory.js'
|
||||
import { users } from './users.js'
|
||||
@@ -66,9 +66,6 @@ export const repairBatchApprovalEnum = pgEnum('repair_batch_approval', [
|
||||
// Defined before repairTickets because tickets FK to batches
|
||||
export const repairBatches = pgTable('repair_batch', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
companyId: uuid('company_id')
|
||||
.notNull()
|
||||
.references(() => companies.id),
|
||||
locationId: uuid('location_id').references(() => locations.id),
|
||||
batchNumber: varchar('batch_number', { length: 50 }),
|
||||
accountId: uuid('account_id')
|
||||
@@ -97,9 +94,6 @@ export const repairBatches = pgTable('repair_batch', {
|
||||
|
||||
export const repairTickets = pgTable('repair_ticket', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
companyId: uuid('company_id')
|
||||
.notNull()
|
||||
.references(() => companies.id),
|
||||
locationId: uuid('location_id').references(() => locations.id),
|
||||
repairBatchId: uuid('repair_batch_id').references(() => repairBatches.id),
|
||||
ticketNumber: varchar('ticket_number', { length: 50 }),
|
||||
@@ -163,9 +157,6 @@ export type RepairNoteInsert = typeof repairNotes.$inferInsert
|
||||
|
||||
export const repairServiceTemplates = pgTable('repair_service_template', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
companyId: uuid('company_id')
|
||||
.notNull()
|
||||
.references(() => companies.id),
|
||||
name: varchar('name', { length: 255 }).notNull(),
|
||||
instrumentType: varchar('instrument_type', { length: 100 }),
|
||||
size: varchar('size', { length: 50 }),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { pgTable, uuid, varchar, timestamp, pgEnum, uniqueIndex, boolean } from 'drizzle-orm/pg-core'
|
||||
import { companies } from './stores.js'
|
||||
|
||||
export const userRoleEnum = pgEnum('user_role', [
|
||||
'admin',
|
||||
@@ -11,9 +10,6 @@ export const userRoleEnum = pgEnum('user_role', [
|
||||
|
||||
export const users = pgTable('user', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
companyId: uuid('company_id')
|
||||
.notNull()
|
||||
.references(() => companies.id),
|
||||
email: varchar('email', { length: 255 }).notNull().unique(),
|
||||
passwordHash: varchar('password_hash', { length: 255 }).notNull(),
|
||||
firstName: varchar('first_name', { length: 100 }).notNull(),
|
||||
|
||||
@@ -39,7 +39,7 @@ async function seed() {
|
||||
}
|
||||
|
||||
// --- Admin user (if not exists) ---
|
||||
const [adminUser] = await sql`SELECT id FROM "user" WHERE email = 'admin@forte.dev'`
|
||||
const [adminUser] = await sql`SELECT id, company_id FROM "user" WHERE email = 'admin@forte.dev'`
|
||||
if (!adminUser) {
|
||||
const bcrypt = await import('bcrypt')
|
||||
const hashedPw = await (bcrypt.default || bcrypt).hash('admin1234', 10)
|
||||
@@ -50,9 +50,14 @@ async function seed() {
|
||||
}
|
||||
console.log(' Created admin user: admin@forte.dev / admin1234')
|
||||
} else {
|
||||
// Make sure admin role is assigned
|
||||
// Ensure admin is in the right company and has the admin role
|
||||
if (adminUser.company_id !== COMPANY_ID) {
|
||||
await sql`UPDATE "user" SET company_id = ${COMPANY_ID} WHERE id = ${adminUser.id}`
|
||||
console.log(' Moved admin user to correct company')
|
||||
}
|
||||
const [adminRole] = await sql`SELECT id FROM role WHERE company_id = ${COMPANY_ID} AND slug = 'admin' LIMIT 1`
|
||||
if (adminRole) {
|
||||
await sql`DELETE FROM user_role_assignment WHERE user_id = ${adminUser.id}`
|
||||
await sql`INSERT INTO user_role_assignment (user_id, role_id) VALUES (${adminUser.id}, ${adminRole.id}) ON CONFLICT DO NOTHING`
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user