diff --git a/server/services/machines.test.ts b/server/services/machines.test.ts new file mode 100644 index 0000000..50d9eb4 --- /dev/null +++ b/server/services/machines.test.ts @@ -0,0 +1,24 @@ +// server/services/machines.test.ts +import { describe, it, expect, vi } from "vitest"; + +// Mock all modules with side-effects at import time (DB, SSH, crypto) +vi.mock("../db/client.js", () => ({ db: {}, schema: { machines: {} } })); +vi.mock("../crypto/secrets.js", () => ({ encryptSecret: vi.fn(), decryptSecret: vi.fn() })); +vi.mock("../env.js", () => ({ env: { requireMasterKey: vi.fn(), reportsDir: "/tmp" } })); +vi.mock("../ssh/client.js", () => ({ runPlain: vi.fn() })); + +import { parseOsRelease } from "./machines.js"; + +describe("parseOsRelease", () => { + it("détecte Debian", () => { + const r = parseOsRelease('ID=debian\nVERSION_ID="11"\nPRETTY_NAME="Debian 11"'); + expect(r).toEqual({ family: "debian", version: "11" }); + }); + it("détecte Ubuntu", () => { + const r = parseOsRelease('ID=ubuntu\nVERSION_ID="22.04"'); + expect(r).toEqual({ family: "ubuntu", version: "22.04" }); + }); + it("retombe sur unknown", () => { + expect(parseOsRelease("ID=arch").family).toBe("unknown"); + }); +}); diff --git a/server/services/machines.ts b/server/services/machines.ts new file mode 100644 index 0000000..38354c3 --- /dev/null +++ b/server/services/machines.ts @@ -0,0 +1,106 @@ +// 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(); +}