Files
NomadArch/packages/electron-app/electron/main/user-store.ts
Gemini AI 57720e6c5b
Some checks failed
Release Binaries / release (push) Has been cancelled
fix: ensure all API requests use user ID and fix missing data migration for existing users
2025-12-29 01:58:15 +04:00

305 lines
8.3 KiB
TypeScript

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<string>) {
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 = 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<string>()
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) return false
if (user.isGuest) return true
if (!user.salt || !user.passwordHash) return false
return hashPassword(password, user.salt) === user.passwordHash
}
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 })
}
}
}