Add org-scoped data isolation across all server actions to prevent cross-org data leakage. Add read-only demo mode with mutation guards on all write endpoints. Multi-tenancy: - org filter on executeDashboardQueries (all query types) - org boundary checks on getChannel, joinChannel - searchMentionableUsers derives org from session - getConversationUsage scoped to user, not org-wide for admins - organizations table, members, org switcher component Demo mode: - /demo route sets strict sameSite cookie - isDemoUser guards on all mutation server actions - demo banner, CTA dialog, and gate components - seed script for demo org data Also: exclude scripts/ from tsconfig (fixes build), add multi-tenancy architecture documentation. Co-authored-by: Nicholai <nicholaivogelfilms@gmail.com>
143 lines
4.2 KiB
TypeScript
143 lines
4.2 KiB
TypeScript
#!/usr/bin/env bun
|
|
/**
|
|
* Idempotent migration script to introduce organizations into existing data
|
|
*
|
|
* This script:
|
|
* 1. Creates the HPS organization if it doesn't exist
|
|
* 2. Adds all existing users to the HPS organization
|
|
* 3. Associates all existing customers, vendors, and projects with HPS
|
|
*
|
|
* Safe to run multiple times - checks for existing records before inserting.
|
|
*/
|
|
|
|
import { Database } from "bun:sqlite"
|
|
import { resolve, join } from "path"
|
|
import { randomUUID } from "crypto"
|
|
import { existsSync, readdirSync } from "fs"
|
|
|
|
const HPS_ORG_ID = "hps-org-001"
|
|
const HPS_ORG_NAME = "HPS"
|
|
const HPS_ORG_SLUG = "hps"
|
|
const HPS_ORG_TYPE = "internal"
|
|
|
|
const DB_DIR = resolve(
|
|
process.cwd(),
|
|
".wrangler/state/v3/d1/miniflare-D1DatabaseObject"
|
|
)
|
|
|
|
function findDatabase(): string {
|
|
if (!existsSync(DB_DIR)) {
|
|
console.error(`Database directory not found: ${DB_DIR}`)
|
|
console.error("Run 'bun dev' at least once to initialize the local D1 database.")
|
|
process.exit(1)
|
|
}
|
|
|
|
const files = readdirSync(DB_DIR)
|
|
const sqliteFile = files.find((f: string) => f.endsWith(".sqlite"))
|
|
|
|
if (!sqliteFile) {
|
|
console.error(`No .sqlite file found in ${DB_DIR}`)
|
|
process.exit(1)
|
|
}
|
|
|
|
return join(DB_DIR, sqliteFile)
|
|
}
|
|
|
|
function main() {
|
|
const dbPath = findDatabase()
|
|
console.log(`Using database: ${dbPath}\n`)
|
|
|
|
const db = new Database(dbPath)
|
|
db.run("PRAGMA journal_mode = WAL")
|
|
|
|
const timestamp = new Date().toISOString()
|
|
|
|
const tx = db.transaction(() => {
|
|
// 1. Create HPS organization
|
|
console.log("1. Checking for HPS organization...")
|
|
const existingOrg = db
|
|
.prepare("SELECT id FROM organizations WHERE id = ?")
|
|
.get(HPS_ORG_ID)
|
|
|
|
if (existingOrg) {
|
|
console.log(` Already exists (${HPS_ORG_ID})`)
|
|
} else {
|
|
db.prepare(
|
|
`INSERT INTO organizations (id, name, slug, type, is_active, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, 1, ?, ?)`
|
|
).run(HPS_ORG_ID, HPS_ORG_NAME, HPS_ORG_SLUG, HPS_ORG_TYPE, timestamp, timestamp)
|
|
console.log(` Created HPS organization (${HPS_ORG_ID})`)
|
|
}
|
|
|
|
// 2. Add all users to HPS
|
|
console.log("\n2. Adding users to HPS organization...")
|
|
const users = db.prepare("SELECT id, role FROM users").all() as Array<{
|
|
id: string
|
|
role: string
|
|
}>
|
|
|
|
let added = 0
|
|
let skipped = 0
|
|
|
|
for (const user of users) {
|
|
const existing = db
|
|
.prepare(
|
|
"SELECT id FROM organization_members WHERE organization_id = ? AND user_id = ?"
|
|
)
|
|
.get(HPS_ORG_ID, user.id)
|
|
|
|
if (existing) {
|
|
skipped++
|
|
} else {
|
|
db.prepare(
|
|
`INSERT INTO organization_members (id, organization_id, user_id, role, joined_at)
|
|
VALUES (?, ?, ?, ?, ?)`
|
|
).run(randomUUID(), HPS_ORG_ID, user.id, user.role, timestamp)
|
|
added++
|
|
}
|
|
}
|
|
|
|
console.log(` Added ${added} user(s), skipped ${skipped}`)
|
|
|
|
// 3. Update customers
|
|
console.log("\n3. Updating customers...")
|
|
const custResult = db
|
|
.prepare("UPDATE customers SET organization_id = ? WHERE organization_id IS NULL")
|
|
.run(HPS_ORG_ID)
|
|
console.log(` Updated ${custResult.changes} customer(s)`)
|
|
|
|
// 4. Update vendors
|
|
console.log("\n4. Updating vendors...")
|
|
const vendResult = db
|
|
.prepare("UPDATE vendors SET organization_id = ? WHERE organization_id IS NULL")
|
|
.run(HPS_ORG_ID)
|
|
console.log(` Updated ${vendResult.changes} vendor(s)`)
|
|
|
|
// 5. Update projects
|
|
console.log("\n5. Updating projects...")
|
|
const projResult = db
|
|
.prepare("UPDATE projects SET organization_id = ? WHERE organization_id IS NULL")
|
|
.run(HPS_ORG_ID)
|
|
console.log(` Updated ${projResult.changes} project(s)`)
|
|
|
|
console.log("\nSummary:")
|
|
console.log(` Org: ${HPS_ORG_NAME} (${HPS_ORG_ID})`)
|
|
console.log(` Members: ${users.length} total (${added} new)`)
|
|
console.log(` Customers: ${custResult.changes} updated`)
|
|
console.log(` Vendors: ${vendResult.changes} updated`)
|
|
console.log(` Projects: ${projResult.changes} updated`)
|
|
})
|
|
|
|
try {
|
|
tx()
|
|
console.log("\nMigration completed successfully.")
|
|
} catch (error) {
|
|
console.error("\nMigration failed:", error)
|
|
process.exit(1)
|
|
} finally {
|
|
db.close()
|
|
}
|
|
}
|
|
|
|
main()
|