CoolFace
Apppublic

Leon4gr45/builder

sourceHugging Facemitupdated 2d agoView on Hugging Face
0likes
default-workspace.ts321 linesDownload Raw Back to auth
1/**2 * Default Workspace Management3 *4 * Ensures a default workspace exists for desktop mode and5 * legacy single-user (admin password) mode. Called on first6 * access when no workspace context exists.7 *8 * Handles migration: if data/osws.sqlite has existing projects,9 * copies it to the new workspace so data isn't lost on upgrade.10 */11 12import 'server-only';13 14import Database from 'better-sqlite3';15import path from 'path';16import fs from 'fs';17import {18  getSystemDatabase,19  createWorkspace,20  setDefaultWorkspace,21  getUserDefaultWorkspace,22  getUserById,23  updateWorkspace,24  getWorkspaceById,25  getDeploymentBySlug,26} from './system-database';27import { generateUniqueSlug } from '@/lib/publishing/slug-generator';28import { logger } from '@/lib/utils';29 30function openReadonlyDb(dbPath: string): Database.Database {31  const db = new Database(dbPath, { readonly: true });32  const key = process.env.DB_ENCRYPTION_KEY;33  if (key) db.pragma(`key='${key}'`);34  return db;35}36 37const DEFAULT_WORKSPACE_NAME = 'Local Workspace';38 39function getDataDir(): string {40  return process.env.DATA_DIR || path.join(process.cwd(), 'data');41}42 43/**44 * Ensure a default workspace exists for the given user ID.45 * Creates the user (if synthetic like 'admin'/'desktop') and workspace on first call.46 * If upgrading from single-user mode, migrates existing data to the workspace.47 * Returns the default workspace ID.48 */49export async function ensureDefaultWorkspace(userId: string): Promise<string> {50  // When in managed mode (WEBHOOK_URL set), workspaces start clean — no legacy migration.51  // On standalone instances, migrate legacy data/osws.sqlite into the workspace for all users.52  const isBalancerManaged = !!process.env.WEBHOOK_URL;53 54  // Check if user already has a default workspace — and that it still exists.55  // A stale default (e.g. database partially lost during a desktop update)56  // must fall through to recreation instead of returning a dead workspace id.57  const existing = getUserDefaultWorkspace(userId);58  if (existing && getWorkspaceById(existing)) {59    if (!isBalancerManaged) {60      migrateLegacyData(existing);61    }62    return existing;63  }64 65  // For synthetic users (admin, desktop), create a user record if it doesn't exist66  const user = getUserById(userId);67  if (!user) {68    const { randomBytes } = await import('crypto');69    // Synthetic local users (admin/desktop) never log in by this hash, so store70    // a non-bcrypt placeholder — avoids loading bcrypt's native addon during71    // desktop boot, where a load failure would block workspace init.72    const hash = `nologin:${randomBytes(32).toString('hex')}`;73    const db = getSystemDatabase();74    db.prepare(`75      INSERT OR IGNORE INTO users (id, email, password_hash, display_name, is_admin, active)76      VALUES (?, ?, ?, ?, 1, 1)77    `).run(userId, `${userId}@localhost`, hash, userId === 'desktop' ? 'Desktop' : 'Admin');78  }79 80  // Create default workspace with high limits for admin/desktop81  const workspaceId = createWorkspace(DEFAULT_WORKSPACE_NAME, userId);82  updateWorkspace(workspaceId, {83    max_projects: 9999,84    max_deployments: 9999,85    max_storage_mb: 99999,86  });87  setDefaultWorkspace(userId, workspaceId);88 89  // Migrate legacy data on standalone instances90  if (!isBalancerManaged) {91    migrateLegacyData(workspaceId);92  }93 94  return workspaceId;95}96 97/**98 * If upgrading from single-user mode, copy the existing data/osws.sqlite99 * and project databases into the new workspace directory.100 *101 * Checks whether the legacy DB has data AND the workspace DB is empty102 * (not just whether files exist, since the adapter may have already103 * created an empty workspace DB with schema migrations).104 */105function migrateLegacyData(workspaceId: string): void {106  const dataDir = getDataDir();107  const legacyDbPath = path.join(dataDir, 'osws.sqlite');108  const workspaceDir = path.join(dataDir, 'workspaces', workspaceId);109  const workspaceDbPath = path.join(workspaceDir, 'osws.sqlite');110 111  if (!fs.existsSync(legacyDbPath)) return;112 113  // Check if legacy DB actually has projects114  let legacyProjectCount = 0;115  try {116    const db = openReadonlyDb(legacyDbPath);117    const row = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='projects'").get();118    if (row) {119      legacyProjectCount = (db.prepare('SELECT COUNT(*) as c FROM projects').get() as { c: number }).c;120    }121    db.close();122  } catch { return; }123 124  if (legacyProjectCount === 0) return;125 126  // Check if workspace DB already has data (don't overwrite)127  if (fs.existsSync(workspaceDbPath)) {128    try {129      const db = openReadonlyDb(workspaceDbPath);130      const row = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='projects'").get();131      if (row) {132        const count = (db.prepare('SELECT COUNT(*) as c FROM projects').get() as { c: number }).c;133        db.close();134        if (count > 0) return; // Workspace already has data, skip135      } else {136        db.close();137      }138    } catch { /* workspace DB doesn't exist or is corrupt — proceed with copy */ }139  }140 141  try {142    fs.mkdirSync(workspaceDir, { recursive: true });143 144    // Copy the legacy database (overwrites the empty one if it exists)145    fs.copyFileSync(legacyDbPath, workspaceDbPath);146 147    // Copy WAL/SHM files if they exist148    for (const ext of ['-wal', '-shm']) {149      const walPath = legacyDbPath + ext;150      if (fs.existsSync(walPath)) {151        fs.copyFileSync(walPath, workspaceDbPath + ext);152      }153    }154 155    // Copy project databases (data/projects/ -> data/workspaces/{id}/projects/)156    const legacyProjectsDir = path.join(dataDir, 'projects');157    if (fs.existsSync(legacyProjectsDir)) {158      const workspaceProjectsDir = path.join(workspaceDir, 'projects');159      copyDirRecursive(legacyProjectsDir, workspaceProjectsDir);160    }161  } catch (err) {162    logger.error('[DefaultWorkspace] Failed to migrate legacy data:', err);163  }164}165 166function copyDirRecursive(src: string, dest: string): void {167  if (!fs.existsSync(src)) return;168  fs.mkdirSync(dest, { recursive: true });169 170  const entries = fs.readdirSync(src, { withFileTypes: true });171  for (const entry of entries) {172    const srcPath = path.join(src, entry.name);173    const destPath = path.join(dest, entry.name);174    if (entry.isDirectory()) {175      copyDirRecursive(srcPath, destPath);176    } else {177      fs.copyFileSync(srcPath, destPath);178    }179  }180}181 182// ---------------------------------------------------------------------------183// Repair / Heal184// ---------------------------------------------------------------------------185 186export interface RepairResult {187  legacyDbMigrated: boolean;188  legacyProjectsMigrated: number;189  deploymentRoutesCreated: number;190  errors: string[];191}192 193/**194 * Repair a workspace by detecting and fixing common issues:195 * 1. Legacy data/osws.sqlite not migrated to workspace196 * 2. Project databases in data/projects/ not copied to workspace197 * 3. Deployments in workspace DB but missing from deployment_routing198 *199 * Safe to run multiple times — skips already-fixed items.200 */201export function repairWorkspace(workspaceId: string): RepairResult {202  const dataDir = getDataDir();203  const workspaceDir = path.join(dataDir, 'workspaces', workspaceId);204  const workspaceDbPath = path.join(workspaceDir, 'osws.sqlite');205  const legacyDbPath = path.join(dataDir, 'osws.sqlite');206  const result: RepairResult = {207    legacyDbMigrated: false,208    legacyProjectsMigrated: 0,209    deploymentRoutesCreated: 0,210    errors: [],211  };212 213  // 1. If workspace DB is empty/missing but legacy DB has data, copy it214  if (fs.existsSync(legacyDbPath)) {215    const legacyHasData = (() => {216      try {217        const db = openReadonlyDb(legacyDbPath);218        const tableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='projects'").get();219        if (!tableExists) { db.close(); return false; }220        const count = (db.prepare('SELECT COUNT(*) as count FROM projects').get() as { count: number }).count;221        db.close();222        return count > 0;223      } catch { return false; }224    })();225 226    if (legacyHasData) {227      const workspaceHasData = (() => {228        if (!fs.existsSync(workspaceDbPath)) return false;229        try {230          const db = openReadonlyDb(workspaceDbPath);231          const tableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='projects'").get();232          if (!tableExists) { db.close(); return false; }233          const count = (db.prepare('SELECT COUNT(*) as count FROM projects').get() as { count: number }).count;234          db.close();235          return count > 0;236        } catch { return false; }237      })();238 239      if (!workspaceHasData) {240        try {241          fs.mkdirSync(workspaceDir, { recursive: true });242          fs.copyFileSync(legacyDbPath, workspaceDbPath);243          for (const ext of ['-wal', '-shm']) {244            const walPath = legacyDbPath + ext;245            if (fs.existsSync(walPath)) {246              fs.copyFileSync(walPath, workspaceDbPath + ext);247            }248          }249          result.legacyDbMigrated = true;250        } catch (err) {251          result.errors.push(`Failed to copy legacy DB: ${err}`);252        }253      }254    }255  }256 257  // 2. Copy orphaned project databases from data/projects/ to workspace258  const legacyProjectsDir = path.join(dataDir, 'projects');259  const workspaceProjectsDir = path.join(workspaceDir, 'projects');260  if (fs.existsSync(legacyProjectsDir) && fs.existsSync(workspaceDbPath)) {261    try {262      // Get project IDs from workspace DB263      const db = openReadonlyDb(workspaceDbPath);264      const tableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='projects'").get();265      const projectIds: string[] = [];266      if (tableExists) {267        const rows = db.prepare('SELECT id FROM projects').all() as { id: string }[];268        projectIds.push(...rows.map(r => r.id));269      }270      db.close();271 272      // For each project in workspace DB, check if its database is in legacy dir but not workspace dir273      for (const projectId of projectIds) {274        const legacyProjectDir = path.join(legacyProjectsDir, projectId);275        const workspaceProjectDir = path.join(workspaceProjectsDir, projectId);276        if (fs.existsSync(legacyProjectDir) && !fs.existsSync(workspaceProjectDir)) {277          copyDirRecursive(legacyProjectDir, workspaceProjectDir);278          result.legacyProjectsMigrated++;279        }280      }281    } catch (err) {282      result.errors.push(`Failed to migrate project databases: ${err}`);283    }284  }285 286  // 3. Ensure all deployments in workspace DB are registered in deployment_routing287  if (fs.existsSync(workspaceDbPath)) {288    try {289      const db = openReadonlyDb(workspaceDbPath);290      const tableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='deployments'").get();291      if (tableExists) {292        const deployments = db.prepare('SELECT id, slug FROM deployments').all() as { id: string; slug: string | null }[];293        db.close();294 295        const sysDb = getSystemDatabase();296        for (const deployment of deployments) {297          const existing = sysDb.prepare('SELECT deployment_id FROM deployment_routing WHERE deployment_id = ?')298            .get(deployment.id);299          if (!existing) {300            // Assign a slug so the deployment gets a subdomain route — Caddy301            // generation skips routing rows without one. Prefer the deployment's302            // own slug; generate a unique one only if it lacks it.303            const slug = deployment.slug || generateUniqueSlug(s => !!getDeploymentBySlug(s));304            sysDb.prepare(`305              INSERT OR IGNORE INTO deployment_routing (deployment_id, workspace_id, slug)306              VALUES (?, ?, ?)307            `).run(deployment.id, workspaceId, slug);308            result.deploymentRoutesCreated++;309          }310        }311      } else {312        db.close();313      }314    } catch (err) {315      result.errors.push(`Failed to repair deployment routes: ${err}`);316    }317  }318 319  return result;320}321