// server/services/machines.ts import { randomUUID } from "node:crypto"; import { eq } from "drizzle-orm"; import { db, schema } from "../db/client.js"; import { encryptSecret, decryptSecret } from "../crypto/secrets.js"; import { env } from "../env.js"; import { runPlain, type SshCreds } from "../ssh/client.js"; import type { MachineView, OsFamily } from "@shared/types.js"; export interface CreateMachineInput { name: string; hostname: string; port: number; username: string; password: string; sudoPassword?: string | null; aptProxyMode?: "direct" | "runtime"; aptProxyUrl?: string | null; } type MachineRow = typeof schema.machines.$inferSelect; function toView(m: MachineRow): MachineView { return { id: m.id, name: m.name, hostname: m.hostname, port: m.port, osFamily: m.osFamily as OsFamily, username: m.username, aptProxyMode: m.aptProxyMode as "direct" | "runtime", aptProxyUrl: m.aptProxyUrl, status: m.status as MachineView["status"], lastCheckedAt: m.lastCheckedAt, }; } export function getCreds(m: MachineRow): SshCreds { const key = env.requireMasterKey(); return { hostname: m.hostname, port: m.port, username: m.username, password: decryptSecret(m.encPassword, key), sudoPassword: m.encSudoPassword ? decryptSecret(m.encSudoPassword, key) : null, }; } export function getMachineRow(id: string): MachineRow | undefined { return db.select().from(schema.machines).where(eq(schema.machines.id, id)).get(); } export function listMachines(): MachineView[] { return db.select().from(schema.machines).all().map(toView); } /** Parse /etc/os-release pour déduire family + version. */ export function parseOsRelease(content: string): { family: OsFamily; version: string } { const fields: Record = {}; for (const line of content.split("\n")) { const m = /^([A-Z_]+)=(.*)$/.exec(line.trim()); if (m) fields[m[1]!] = m[2]!.replace(/^"|"$/g, ""); } const id = (fields.ID ?? "").toLowerCase(); const family: OsFamily = id === "ubuntu" ? "ubuntu" : id === "debian" ? "debian" : "unknown"; return { family, version: fields.VERSION_ID ?? fields.VERSION ?? "" }; } export async function testConnection(creds: SshCreds): Promise<{ family: OsFamily; version: string }> { const res = await runPlain(creds, "cat /etc/os-release"); return parseOsRelease(res.stdout); } export async function createMachine(input: CreateMachineInput): Promise { const key = env.requireMasterKey(); const creds: SshCreds = { hostname: input.hostname, port: input.port, username: input.username, password: input.password, sudoPassword: input.sudoPassword ?? null, }; const os = await testConnection(creds); // lève si la connexion échoue const id = randomUUID(); const row: MachineRow = { id, name: input.name, hostname: input.hostname, port: input.port, osFamily: os.family, username: input.username, encPassword: encryptSecret(input.password, key), encSudoPassword: input.sudoPassword ? encryptSecret(input.sudoPassword, key) : null, aptProxyMode: input.aptProxyMode ?? "direct", aptProxyUrl: input.aptProxyUrl ?? null, status: "unknown", lastCheckedAt: null, createdAt: new Date().toISOString(), }; db.insert(schema.machines).values(row).run(); return toView(row); } export function deleteMachine(id: string): void { db.delete(schema.machines).where(eq(schema.machines.id, id)).run(); }