UNPKG

nuxt-supabase-team-auth

Version:

Drop-in Nuxt 3 module for team-based authentication with Supabase

428 lines (426 loc) 18.4 kB
#!/usr/bin/env node import { existsSync, readdirSync, readFileSync, writeFileSync, copyFileSync, mkdirSync } from 'node:fs'; import { join, relative, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { execSync } from 'node:child_process'; import { program } from 'commander'; function findSupabaseDir() { const startDir = process.cwd(); let currentDir = startDir; const rootDir = "/"; while (currentDir !== rootDir) { const supabaseConfigPath = join(currentDir, "supabase", "config.toml"); if (existsSync(supabaseConfigPath)) { const supabaseDir = join(currentDir, "supabase"); const relativePath = relative(startDir, supabaseDir); return relativePath || "supabase"; } const parentDir = dirname(currentDir); if (parentDir === currentDir) { break; } currentDir = parentDir; } return null; } function getModuleRoot() { const currentPath = fileURLToPath(import.meta.url); const distDir = dirname(currentPath); let moduleRoot = dirname(distDir); if (!existsSync(join(moduleRoot, "package.json"))) { moduleRoot = dirname(moduleRoot); } return moduleRoot; } function copyDirectoryRecursive(source, target, force = false) { let copiedCount = 0; if (!existsSync(source)) { return copiedCount; } if (!existsSync(target)) { mkdirSync(target, { recursive: true }); } const entries = readdirSync(source, { withFileTypes: true }); for (const entry of entries) { const sourcePath = join(source, entry.name); const targetPath = join(target, entry.name); if (entry.isDirectory()) { copiedCount += copyDirectoryRecursive(sourcePath, targetPath, force); } else { if (!existsSync(targetPath) || force) { copyFileSync(sourcePath, targetPath); copiedCount++; } } } return copiedCount; } async function checkTableConflicts() { const conflictingTables = []; const requiredTables = ["profiles", "teams", "team_members", "impersonation_sessions"]; for (const table of requiredTables) { try { const result = execSync( `psql postgresql://postgres:postgres@127.0.0.1:54322/postgres -t -c "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_schema = 'public' AND table_name = '${table}');"`, { stdio: "pipe" } ).toString().trim(); if (result === "t") { conflictingTables.push(table); } } catch { } } return conflictingTables; } function loadMigrationTracker() { const trackerPath = ".team-auth-version.json"; if (!existsSync(trackerPath)) { return null; } try { const content = readFileSync(trackerPath, "utf-8"); return JSON.parse(content); } catch { return null; } } function saveMigrationTracker(tracker) { const trackerPath = ".team-auth-version.json"; writeFileSync(trackerPath, JSON.stringify(tracker, null, 2)); } program.name("team-auth").description("CLI for Nuxt Supabase Team Auth module").version("0.1.0"); program.command("init").description("Initialize team-auth in your Supabase project").option("--force", "Overwrite existing files").action(async (options) => { try { console.log("\u{1F680} Initializing team-auth module..."); const supabaseDir = findSupabaseDir(); if (!supabaseDir) { console.error("\u274C No Supabase project found. Run `supabase init` first."); console.error(" Searched up directory tree from current working directory"); process.exit(1); } if (supabaseDir !== "supabase") { console.log(`\u{1F4C1} Found Supabase project at: ${supabaseDir}`); } let supabaseRunning = false; try { execSync("supabase status", { stdio: "pipe" }); supabaseRunning = true; } catch { console.warn("\u26A0\uFE0F Supabase not running. Table conflict detection skipped."); } if (supabaseRunning) { const conflicts = await checkTableConflicts(); if (conflicts.length > 0) { console.log(""); console.log("\u26A0\uFE0F Found existing tables that may conflict:"); conflicts.forEach((table) => { console.log(` - public.${table} (existing table)`); }); console.log(""); console.log("This module requires these tables for team authentication."); console.log("Options:"); console.log("1. Backup and remove existing tables first"); console.log("2. Use --force to continue anyway (advanced users)"); console.log("3. See documentation for manual integration"); console.log(""); if (!options.force) { const confirm = await confirmAction("Continue anyway?"); if (!confirm) { console.log("\u274C Initialization cancelled. Please resolve conflicts first."); process.exit(0); } } } else { console.log("\u2705 No conflicting tables found"); } } const moduleRoot = getModuleRoot(); console.log("\u{1F4E6} Module root:", moduleRoot); const migrationsSource = join(moduleRoot, "supabase", "migrations"); const migrationsTarget = join(supabaseDir, "migrations"); if (existsSync(migrationsSource)) { const copiedMigrations = copyDirectoryRecursive(migrationsSource, migrationsTarget, options.force); console.log(`\u2705 Copied ${copiedMigrations} migration files`); } else { console.warn("\u26A0\uFE0F Migration files not found in module"); } const functionsSource = join(moduleRoot, "supabase", "functions"); const functionsTarget = join(supabaseDir, "functions"); if (existsSync(functionsSource)) { const functionDirs = readdirSync(functionsSource, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name); let copiedFunctions = 0; functionDirs.forEach((funcName) => { const sourceDir = join(functionsSource, funcName); const targetDir = join(functionsTarget, funcName); copiedFunctions += copyDirectoryRecursive(sourceDir, targetDir, options.force); }); console.log(`\u2705 Copied ${functionDirs.length} Edge Function directories (${copiedFunctions} files)`); } else { console.warn("\u26A0\uFE0F Edge Function files not found in module"); } const packagePath = join(moduleRoot, "package.json"); const packageJson = JSON.parse(readFileSync(packagePath, "utf-8")); const appliedMigrations = []; if (existsSync(migrationsSource)) { const migrationFiles = readdirSync(migrationsSource).filter((file) => file.endsWith(".sql")).sort(); appliedMigrations.push(...migrationFiles); } const initialTracker = { version: packageJson.version, appliedMigrations, lastUpdated: (/* @__PURE__ */ new Date()).toISOString() }; saveMigrationTracker(initialTracker); console.log(`\u2705 Version tracker initialized (v${packageJson.version})`); if (existsSync("package.json")) { const packageJson2 = JSON.parse(readFileSync("package.json", "utf-8")); if (!packageJson2.scripts) { packageJson2.scripts = {}; } packageJson2.scripts["team-auth:migrate"] = "team-auth migrate"; writeFileSync("package.json", JSON.stringify(packageJson2, null, 2)); console.log("\u2705 Updated package.json scripts"); } try { execSync("supabase status", { stdio: "pipe" }); console.log("\u2705 Supabase project is linked"); const shouldPush = process.env.SUPABASE_DB_PUSH !== "false"; if (shouldPush) { console.log("\u{1F504} Applying migrations to local database..."); execSync("supabase db push", { stdio: "inherit" }); console.log("\u2705 Migrations applied"); } } catch { console.warn("\u26A0\uFE0F Supabase CLI not available or project not linked"); console.log(" Run `supabase db push` manually after linking your project"); } console.log(""); console.log("\u{1F389} Team-auth initialization complete!"); console.log(""); console.log("Next steps:"); console.log("1. Configure your environment variables (.env)"); console.log("2. Run `npm run dev` to start development"); console.log("3. Check out the documentation for usage examples"); } catch (error) { console.error("\u274C Initialization failed:", error); process.exit(1); } }); program.command("migrate").description("Apply new team-auth migrations").option("--dry-run", "Show what would be applied without making changes").action(async (options) => { try { console.log("\u{1F504} Checking for team-auth migrations..."); const supabaseDir = findSupabaseDir(); if (!supabaseDir) { console.error("\u274C No Supabase project found. Run `supabase init` first."); console.error(" Searched up directory tree from current working directory"); process.exit(1); } const currentTracker = loadMigrationTracker(); if (!currentTracker) { console.error("\u274C No migration tracker found. Run `npx nuxt-supabase-team-auth init` first."); process.exit(1); } const moduleRoot = getModuleRoot(); const modulePackagePath = join(moduleRoot, "package.json"); const modulePackageJson = JSON.parse(readFileSync(modulePackagePath, "utf-8")); console.log(`\u{1F4E6} Current version: ${currentTracker.version}`); console.log(`\u{1F4E6} Module version: ${modulePackageJson.version}`); const migrationsSource = join(moduleRoot, "supabase", "migrations"); if (!existsSync(migrationsSource)) { console.log("\u2705 No migrations found in module"); return; } const availableMigrations = readdirSync(migrationsSource).filter((file) => file.endsWith(".sql")).sort(); const newMigrations = availableMigrations.filter( (migration) => !currentTracker.appliedMigrations.includes(migration) ); if (newMigrations.length === 0) { console.log("\u2705 No new migrations to apply"); return; } console.log(`\u{1F4CB} Found ${newMigrations.length} new migration(s):`); newMigrations.forEach((migration) => { console.log(` - ${migration}`); }); if (options.dryRun) { console.log("\u{1F50D} Dry run complete - no changes made"); return; } const confirm = await confirmAction("Apply these migrations?"); if (!confirm) { console.log("\u274C Migration cancelled"); process.exit(0); } const migrationsTarget = join(supabaseDir, "migrations"); for (const migration of newMigrations) { const sourcePath = join(migrationsSource, migration); const targetPath = join(migrationsTarget, migration); if (!existsSync(targetPath)) { copyFileSync(sourcePath, targetPath); console.log(`\u2705 Copied ${migration}`); } } const functionsSource = join(moduleRoot, "supabase", "functions"); const functionsTarget = join("supabase", "functions"); if (existsSync(functionsSource)) { const functionDirs = readdirSync(functionsSource, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name); let copiedFunctions = 0; functionDirs.forEach((funcName) => { const sourceDir = join(functionsSource, funcName); const targetDir = join(functionsTarget, funcName); copiedFunctions += copyDirectoryRecursive(sourceDir, targetDir, true); }); if (copiedFunctions > 0) { console.log(`\u2705 Updated ${functionDirs.length} Edge Function directories (${copiedFunctions} files)`); } } const updatedTracker = { version: modulePackageJson.version, appliedMigrations: [...currentTracker.appliedMigrations, ...newMigrations], lastUpdated: (/* @__PURE__ */ new Date()).toISOString() }; saveMigrationTracker(updatedTracker); try { execSync("supabase status", { stdio: "pipe" }); console.log("\u{1F504} Applying migrations to local database..."); execSync("supabase db push", { stdio: "inherit" }); console.log("\u2705 Database updated successfully"); } catch { console.warn("\u26A0\uFE0F Supabase not running. Run `supabase db push` manually."); } console.log(""); console.log("\u{1F389} Migration complete!"); console.log(`\u{1F4E6} Updated to version ${modulePackageJson.version}`); console.log(`\u{1F4CB} Applied ${newMigrations.length} new migration(s)`); } catch (error) { console.error("\u274C Migration failed:", error); process.exit(1); } }); program.command("cleanup").description("Database cleanup operations for development").option("--all", "Reset entire database (equivalent to supabase db reset)").option("--test-data", "Clean only test data (users with @example.com emails)").option("--team <team-id>", "Delete specific team by ID").action(async (options) => { try { const supabaseDir = findSupabaseDir(); if (!supabaseDir) { console.error("\u274C No Supabase project found. Must be run from Supabase project root."); console.error(" Searched up directory tree from current working directory"); process.exit(1); } try { execSync("supabase status", { stdio: "pipe" }); } catch { console.error("\u274C Supabase services not running. Run `supabase start` first."); process.exit(1); } if (options.all) { console.log("\u{1F5D1}\uFE0F Resetting entire database..."); console.log("\u26A0\uFE0F This will remove ALL data and reapply migrations."); const confirm = await confirmAction("Are you sure you want to reset the entire database?"); if (!confirm) { console.log("\u274C Database reset cancelled"); process.exit(0); } execSync("supabase db reset --debug", { stdio: "inherit" }); console.log("\u2705 Database reset complete"); } else if (options.testData) { console.log("\u{1F9F9} Cleaning test data..."); try { execSync('psql postgresql://postgres:postgres@127.0.0.1:54322/postgres -c "SELECT cleanup_all_test_data();"', { stdio: "inherit" }); console.log("\u2705 Test data cleanup complete"); } catch (error) { console.error("\u274C Test data cleanup failed:", error); process.exit(1); } } else if (options.team) { const teamId = options.team; console.log(`\u{1F5D1}\uFE0F Deleting team: ${teamId}`); const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; if (!uuidRegex.test(teamId)) { console.error("\u274C Invalid team ID format. Must be a valid UUID."); process.exit(1); } try { execSync(`psql postgresql://postgres:postgres@127.0.0.1:54322/postgres -c "SELECT cleanup_test_team('${teamId}');"`, { stdio: "inherit" }); console.log("\u2705 Team deletion complete"); } catch (error) { console.error("\u274C Team deletion failed:", error); process.exit(1); } } else { console.log("\u2753 No cleanup option specified. Use --help for available options."); console.log(""); console.log("Available cleanup options:"); console.log(" --all Reset entire database"); console.log(" --test-data Clean only test data"); console.log(" --team <id> Delete specific team"); process.exit(1); } } catch (error) { console.error("\u274C Cleanup failed:", error); process.exit(1); } }); program.command("db").description("Database status and information").option("--status", "Show Supabase services status").option("--teams", "List all teams").option("--users", "List all users").action(async (options) => { try { const supabaseDir = findSupabaseDir(); if (!supabaseDir) { console.error("\u274C No Supabase project found. Must be run from Supabase project root."); console.error(" Searched up directory tree from current working directory"); process.exit(1); } if (options.status) { console.log("\u{1F4CA} Supabase Services Status:"); execSync("supabase status", { stdio: "inherit" }); } else if (options.teams) { console.log("\u{1F465} Teams in database:"); execSync('psql postgresql://postgres:postgres@127.0.0.1:54322/postgres -c "SELECT id, name, created_at FROM teams ORDER BY created_at;"', { stdio: "inherit" }); } else if (options.users) { console.log("\u{1F464} Users in database:"); execSync('psql postgresql://postgres:postgres@127.0.0.1:54322/postgres -c "SELECT p.id, p.full_name, p.email, p.created_at FROM profiles p ORDER BY p.created_at LIMIT 50;"', { stdio: "inherit" }); } else { console.log("\u2753 No database option specified. Use --help for available options."); console.log(""); console.log("Available database options:"); console.log(" --status Show Supabase services status"); console.log(" --teams List all teams"); console.log(" --users List all users (limited to 50)"); process.exit(1); } } catch (error) { console.error("\u274C Database operation failed:", error); process.exit(1); } }); async function confirmAction(message) { const readline = await import('node:readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); return new Promise((resolve) => { rl.question(`${message} (y/N): `, (answer) => { rl.close(); resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes"); }); }); } program.command("docs").description("Open documentation (README)").action(() => { const moduleRoot = getModuleRoot(); const readmePath = join(moduleRoot, "README.md"); if (existsSync(readmePath)) { console.log("\u{1F4D6} Opening documentation..."); console.log(`\u{1F4CD} README location: ${readmePath}`); try { execSync(`open "${readmePath}"`, { stdio: "ignore" }); } catch { try { execSync(`xdg-open "${readmePath}"`, { stdio: "ignore" }); } catch { console.log("\u{1F4A1} View the README at the path above, or visit:"); console.log(" https://github.com/pengelbrecht/nuxt-supabase-team-auth"); } } } else { console.log("\u{1F4D6} Documentation: https://github.com/pengelbrecht/nuxt-supabase-team-auth"); } }); program.parse();