import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, cpSync } from "fs" import os from "os" import path from "path" import crypto from "crypto" interface UserRecord { id: string name: string salt?: string passwordHash?: string isGuest?: boolean createdAt: string updatedAt: string } interface UserStoreState { users: UserRecord[] activeUserId?: string } const CONFIG_ROOT = path.join(os.homedir(), ".config", "codenomad") const USERS_FILE = path.join(CONFIG_ROOT, "users.json") const USERS_ROOT = path.join(CONFIG_ROOT, "users") const LEGACY_ROOT = CONFIG_ROOT const LEGACY_INTEGRATIONS_ROOT = path.join(os.homedir(), ".nomadarch") function nowIso() { return new Date().toISOString() } function sanitizeId(value: string) { return value .toLowerCase() .trim() .replace(/[^a-z0-9-_]+/g, "-") .replace(/-{2,}/g, "-") .replace(/^-|-$/g, "") } function hashPassword(password: string, salt: string) { return crypto.pbkdf2Sync(password, salt, 120000, 32, "sha256").toString("base64") } function generateSalt() { return crypto.randomBytes(16).toString("base64") } function ensureDir(dir: string) { if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }) } } function readStore(): UserStoreState { try { if (!existsSync(USERS_FILE)) { return { users: [] } } const content = readFileSync(USERS_FILE, "utf-8") const parsed = JSON.parse(content) as UserStoreState return { users: Array.isArray(parsed.users) ? parsed.users : [], activeUserId: parsed.activeUserId, } } catch { return { users: [] } } } function writeStore(state: UserStoreState) { ensureDir(CONFIG_ROOT) ensureDir(USERS_ROOT) writeFileSync(USERS_FILE, JSON.stringify(state, null, 2), "utf-8") } function ensureUniqueId(base: string, existing: Set) { let candidate = sanitizeId(base) || "user" let index = 1 while (existing.has(candidate)) { candidate = `${candidate}-${index}` index += 1 } return candidate } function getUserDir(userId: string) { return path.join(USERS_ROOT, userId) } function migrateLegacyData(targetDir: string) { const legacyConfig = path.join(LEGACY_ROOT, "config.json") const legacyInstances = path.join(LEGACY_ROOT, "instances") const legacyWorkspaces = path.join(LEGACY_ROOT, "opencode-workspaces") ensureDir(targetDir) if (existsSync(legacyConfig)) { cpSync(legacyConfig, path.join(targetDir, "config.json"), { force: true }) } if (existsSync(legacyInstances)) { cpSync(legacyInstances, path.join(targetDir, "instances"), { recursive: true, force: true }) } if (existsSync(legacyWorkspaces)) { cpSync(legacyWorkspaces, path.join(targetDir, "opencode-workspaces"), { recursive: true, force: true }) } if (existsSync(LEGACY_INTEGRATIONS_ROOT)) { cpSync(LEGACY_INTEGRATIONS_ROOT, path.join(targetDir, "integrations"), { recursive: true, force: true }) } } export function ensureDefaultUsers(): UserRecord { const store = readStore() // If roman exists, ensure his password is updated to the new required one if it matches the old default const roman = store.users.find(u => u.name === "roman") if (roman && roman.salt && roman.passwordHash) { const oldDefaultHash = hashPassword("q1w2e3r4", roman.salt) if (roman.passwordHash === oldDefaultHash) { console.log("[UserStore] Updating roman's password to new default") const newSalt = generateSalt() roman.salt = newSalt roman.passwordHash = hashPassword("!@#$q1w2e3r4", newSalt) roman.updatedAt = nowIso() writeStore(store) } // NEW: Check if roman needs data migration (e.g. if he was created before migration logic was robust) const userDir = getUserDir(roman.id) const configPath = path.join(userDir, "config.json") let needsMigration = !existsSync(configPath) if (!needsMigration) { try { const config = JSON.parse(readFileSync(configPath, "utf-8")) if (!config.recentFolders || config.recentFolders.length === 0) { needsMigration = true } } catch (e) { needsMigration = true } } if (needsMigration) { console.log(`[UserStore] Roman exists but seems to have missing data. Triggering migration to ${userDir}...`) migrateLegacyData(userDir) } } if (store.users.length > 0) { const active = store.users.find((u) => u.id === store.activeUserId) ?? store.users[0] return active } const existingIds = new Set() const userId = ensureUniqueId("roman", existingIds) const salt = generateSalt() const passwordHash = hashPassword("!@#$q1w2e3r4", salt) const record: UserRecord = { id: userId, name: "roman", salt, passwordHash, createdAt: nowIso(), updatedAt: nowIso(), } store.users.push(record) writeStore(store) const userDir = getUserDir(record.id) migrateLegacyData(userDir) return record } export function listUsers(): UserRecord[] { return readStore().users } export function getActiveUser(): UserRecord | null { const store = readStore() if (!store.activeUserId) return null return store.users.find((user) => user.id === store.activeUserId) ?? null } export function logoutActiveUser() { const store = readStore() store.activeUserId = undefined writeStore(store) console.log("[UserStore] Active user logged out") } export function setActiveUser(userId: string) { const store = readStore() const user = store.users.find((u) => u.id === userId) if (!user) { throw new Error("User not found") } store.activeUserId = userId writeStore(store) return user } export function createUser(name: string, password: string) { const store = readStore() const existingIds = new Set(store.users.map((u) => u.id)) const id = ensureUniqueId(name, existingIds) const salt = generateSalt() const passwordHash = hashPassword(password, salt) const record: UserRecord = { id, name, salt, passwordHash, createdAt: nowIso(), updatedAt: nowIso(), } store.users.push(record) writeStore(store) ensureDir(getUserDir(id)) return record } export function createGuestUser() { const store = readStore() const existingIds = new Set(store.users.map((u) => u.id)) const id = ensureUniqueId(`guest-${crypto.randomUUID().slice(0, 8)}`, existingIds) const record: UserRecord = { id, name: "Guest", isGuest: true, createdAt: nowIso(), updatedAt: nowIso(), } store.users.push(record) store.activeUserId = id writeStore(store) ensureDir(getUserDir(id)) return record } export function updateUser(userId: string, updates: { name?: string; password?: string }) { const store = readStore() const target = store.users.find((u) => u.id === userId) if (!target) { throw new Error("User not found") } if (updates.name) { target.name = updates.name } if (updates.password && !target.isGuest) { const salt = generateSalt() target.salt = salt target.passwordHash = hashPassword(updates.password, salt) } target.updatedAt = nowIso() writeStore(store) return target } export function deleteUser(userId: string) { const store = readStore() const target = store.users.find((u) => u.id === userId) if (!target) return store.users = store.users.filter((u) => u.id !== userId) if (store.activeUserId === userId) { store.activeUserId = store.users[0]?.id } writeStore(store) const dir = getUserDir(userId) if (existsSync(dir)) { rmSync(dir, { recursive: true, force: true }) } } export function verifyPassword(userId: string, password: string): boolean { const store = readStore() const user = store.users.find((u) => u.id === userId) if (!user) { console.log("[verifyPassword] User not found:", userId) return false } if (user.isGuest) return true if (!user.salt || !user.passwordHash) { console.log("[verifyPassword] No salt or hash for user:", userId) return false } const computed = hashPassword(password, user.salt) const matches = computed === user.passwordHash console.log("[verifyPassword] userId:", userId, "password:", JSON.stringify(password), "len:", password.length) console.log("[verifyPassword] computed:", computed, "stored:", user.passwordHash, "matches:", matches) return matches } export function getUserDataRoot(userId: string) { return getUserDir(userId) } export function clearGuestUsers() { const store = readStore() const guests = store.users.filter((u) => u.isGuest) if (guests.length === 0) return store.users = store.users.filter((u) => !u.isGuest) if (store.activeUserId && guests.some((u) => u.id === store.activeUserId)) { store.activeUserId = store.users[0]?.id } writeStore(store) for (const guest of guests) { const dir = getUserDir(guest.id) if (existsSync(dir)) { rmSync(dir, { recursive: true, force: true }) } } }