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:
Ryan Moon
2026-03-29 14:58:33 -05:00
parent 55f8591cf1
commit d36c6f7135
35 changed files with 353 additions and 511 deletions

View File

@@ -59,17 +59,17 @@ async function setupDatabase() {
END $$
`)
// Seed company + location
// Seed company + location (company table stays as store settings)
await testSql`INSERT INTO company (id, name, timezone) VALUES (${COMPANY_ID}, 'Test Music Co.', 'America/Chicago')`
await testSql`INSERT INTO location (id, company_id, name) VALUES (${LOCATION_ID}, ${COMPANY_ID}, 'Test Location')`
await testSql`INSERT INTO location (id, name) VALUES (${LOCATION_ID}, 'Test Location')`
// Seed lookup tables
const { SYSTEM_UNIT_STATUSES, SYSTEM_ITEM_CONDITIONS } = await import('../src/db/schema/lookups.js')
for (const s of SYSTEM_UNIT_STATUSES) {
await testSql`INSERT INTO inventory_unit_status (company_id, name, slug, description, is_system, sort_order) VALUES (${COMPANY_ID}, ${s.name}, ${s.slug}, ${s.description}, true, ${s.sortOrder})`
await testSql`INSERT INTO inventory_unit_status (name, slug, description, is_system, sort_order) VALUES (${s.name}, ${s.slug}, ${s.description}, true, ${s.sortOrder})`
}
for (const c of SYSTEM_ITEM_CONDITIONS) {
await testSql`INSERT INTO item_condition (company_id, name, slug, description, is_system, sort_order) VALUES (${COMPANY_ID}, ${c.name}, ${c.slug}, ${c.description}, true, ${c.sortOrder})`
await testSql`INSERT INTO item_condition (name, slug, description, is_system, sort_order) VALUES (${c.name}, ${c.slug}, ${c.description}, true, ${c.sortOrder})`
}
// Seed RBAC permissions and default roles
@@ -82,7 +82,7 @@ async function setupDatabase() {
const permMap = new Map(permRows.map((r: any) => [r.slug, r.id]))
for (const roleDef of DEFAULT_ROLES) {
const [role] = await testSql`INSERT INTO role (company_id, name, slug, description, is_system) VALUES (${COMPANY_ID}, ${roleDef.name}, ${roleDef.slug}, ${roleDef.description}, true) RETURNING id`
const [role] = await testSql`INSERT INTO role (name, slug, description, is_system) VALUES (${roleDef.name}, ${roleDef.slug}, ${roleDef.description}, true) RETURNING id`
for (const permSlug of roleDef.permissions) {
const permId = permMap.get(permSlug)
if (permId) {
@@ -149,8 +149,7 @@ async function startBackend(): Promise<Subprocess> {
async function registerTestUser(): Promise<string> {
const testPassword = 'testpassword1234'
// Register needs x-company-id header
const headers = { 'Content-Type': 'application/json', 'x-company-id': COMPANY_ID }
const headers = { 'Content-Type': 'application/json' }
const registerRes = await fetch(`${BASE_URL}/v1/auth/register`, {
method: 'POST',
headers,
@@ -167,7 +166,7 @@ async function registerTestUser(): Promise<string> {
// Assign admin role to the user via direct SQL
if (registerRes.status === 201 && registerData.user) {
const assignSql = postgres(`postgresql://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${TEST_DB}`)
const [adminRole] = await assignSql`SELECT id FROM role WHERE company_id = ${COMPANY_ID} AND slug = 'admin' LIMIT 1`
const [adminRole] = await assignSql`SELECT id FROM role WHERE slug = 'admin' LIMIT 1`
if (adminRole) {
await assignSql`INSERT INTO user_role_assignment (user_id, role_id) VALUES (${registerData.user.id}, ${adminRole.id}) ON CONFLICT DO NOTHING`
}

View File

@@ -6,10 +6,10 @@ suite('RBAC', { tags: ['rbac', 'permissions'] }, (t) => {
const email = `restricted-${Date.now()}@test.com`
const password = 'testpassword1234'
// Register via raw fetch (needs x-company-id)
// Register via raw fetch
const registerRes = await fetch(`${t.baseUrl}/v1/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-company-id': 'a0000000-0000-0000-0000-000000000001' },
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, firstName: 'Restricted', lastName: 'User', role: 'staff' }),
})
const registerData = await registerRes.json() as { token: string }
@@ -31,7 +31,7 @@ suite('RBAC', { tags: ['rbac', 'permissions'] }, (t) => {
const registerRes = await fetch(`${t.baseUrl}/v1/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-company-id': 'a0000000-0000-0000-0000-000000000001' },
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, firstName: roleSlug, lastName: 'User', role: 'staff' }),
})
const registerData = await registerRes.json() as { user: { id: string } }
@@ -143,7 +143,7 @@ suite('RBAC', { tags: ['rbac', 'permissions'] }, (t) => {
const password = 'testpassword1234'
const regRes = await fetch(`${t.baseUrl}/v1/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-company-id': 'a0000000-0000-0000-0000-000000000001' },
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, firstName: 'Inherit', lastName: 'Test', role: 'staff' }),
})
const regData = await regRes.json() as { user: { id: string } }
@@ -233,7 +233,7 @@ suite('RBAC', { tags: ['rbac', 'permissions'] }, (t) => {
// Create a user with a distinctive name
await fetch(`${t.baseUrl}/v1/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-company-id': 'a0000000-0000-0000-0000-000000000001' },
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: `searchme-${Date.now()}@test.com`, password: 'testpassword1234', firstName: 'Searchable', lastName: 'Pessoa', role: 'staff' }),
})
@@ -257,7 +257,7 @@ suite('RBAC', { tags: ['rbac', 'permissions'] }, (t) => {
const password = 'testpassword1234'
const regRes = await fetch(`${t.baseUrl}/v1/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-company-id': 'a0000000-0000-0000-0000-000000000001' },
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, firstName: 'Disable', lastName: 'Me', role: 'staff' }),
})
const regData = await regRes.json() as { user: { id: string } }