Revert "initial NextChair Manager."

This reverts commit b219fb4519.
This commit is contained in:
Stefan Lazic committed 2026-09-05 09:32:32 +01:00
1 parent eaa0d2e513
commit e5668b3f23
24 files changed
+11 -596

No files matched your search

@@ -1,51 +0,0 @@
import { createHmac, timingSafeEqual } from "node:crypto";
import { cookies } from "next/headers";
import { getManagerConfig } from "./manager-config";
const COOKIE_NAME = "nextchair_manager_session";
function signature(value: string, secret: string) {
return createHmac("sha256", secret).update(value).digest("hex");
}
export function isManagerConfigured() {
const config = getManagerConfig();
return Boolean(config.adminEmail && config.adminPassword && config.authSecret);
}
export function authenticateManager(email: string, password: string) {
const config = getManagerConfig();
if (!isManagerConfigured() || email.trim().toLowerCase() !== config.adminEmail.trim().toLowerCase()) return false;
const expected = Buffer.from(config.adminPassword);
const received = Buffer.from(password);
return expected.length === received.length && timingSafeEqual(expected, received);
}
export async function createManagerSession() {
const config = getManagerConfig();
const value = `${config.adminEmail.trim().toLowerCase()}.${Date.now()}`;
(await cookies()).set(COOKIE_NAME, `${value}.${signature(value, config.authSecret)}`, {
httpOnly: true,
sameSite: "strict",
secure: process.env.NODE_ENV === "production",
path: "/manager",
maxAge: 60 * 60 * 8,
});
}
export async function clearManagerSession() {
(await cookies()).delete(COOKIE_NAME);
}
export async function isManagerAuthenticated() {
const config = getManagerConfig();
if (!config.authSecret) return false;
const value = (await cookies()).get(COOKIE_NAME)?.value;
if (!value) return false;
const separator = value.lastIndexOf(".");
if (separator < 1) return false;
const payload = value.slice(0, separator);
const received = Buffer.from(value.slice(separator + 1));
const expected = Buffer.from(signature(payload, config.authSecret));
return received.length === expected.length && timingSafeEqual(received, expected);
}
-11
View File
@@ -1,11 +0,0 @@
export function getManagerConfig() {
const stackRoot = process.env.NEXTCHAIR_STACK_ROOT || `${process.cwd()}/stacks`;
return {
stackRoot,
caddySitesDir: process.env.NEXTCHAIR_CADDY_SITES_DIR || `${stackRoot}/caddy/sites`,
dockgeUrl: process.env.NEXTCHAIR_DOCKGE_URL || "",
adminEmail: process.env.MANAGER_ADMIN_EMAIL || "",
adminPassword: process.env.MANAGER_ADMIN_PASSWORD || "",
authSecret: process.env.MANAGER_AUTH_SECRET || process.env.AUTH_SECRET || "",
};
}
@@ -1,116 +0,0 @@
import { promises as fs } from "node:fs";
import path from "node:path";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import type { BusinessDeployment, BusinessStatus, ProvisionBusinessInput } from "./types";
import { getManagerConfig } from "./manager-config";
const run = promisify(execFile);
function registryPath() {
return path.join(getManagerConfig().stackRoot, "registry.json");
}
function containerName(slug: string) {
return `nextchair-${slug}`;
}
async function readRegistry(): Promise<BusinessDeployment[]> {
try {
return JSON.parse(await fs.readFile(registryPath(), "utf8")) as BusinessDeployment[];
} catch {
return [];
}
}
async function writeRegistry(items: BusinessDeployment[]) {
await fs.mkdir(path.dirname(registryPath()), { recursive: true });
await fs.writeFile(registryPath(), `${JSON.stringify(items, null, 2)}\n`, { mode: 0o600 });
}
async function docker(args: string[]) {
return run("docker", args, { maxBuffer: 1024 * 1024 * 4 });
}
async function inspectContainer(slug: string) {
try {
const result = await docker(["inspect", "-f", "{{.State.Status}}|{{if .State.Health}}{{.State.Health.Status}}{{end}}", containerName(slug)]);
const [container, health] = result.stdout.trim().split("|");
return { container: container === "running" ? "running" : container ? "stopped" : "unknown", health: health || "unknown" } as const;
} catch {
return { container: "unknown", health: "unknown" } as const;
}
}
async function databaseStatus() {
try {
const result = await docker(["inspect", "-f", "{{.State.Status}}", "nextchair-mysql"]);
return result.stdout.trim() === "running" ? "online" : "offline";
} catch {
return "unknown";
}
}
async function storageBytes(slug: string) {
try {
const volume = await docker(["volume", "inspect", "-f", "{{.Mountpoint}}", `nextchair-media-${slug}`]);
const usage = await run("du", ["-sk", volume.stdout.trim()], { maxBuffer: 1024 * 64 });
return Number.parseInt(usage.stdout.trim().split(/\s+/)[0], 10) * 1024;
} catch {
return null;
}
}
export async function listBusinesses(): Promise<BusinessStatus[]> {
const [items, database] = await Promise.all([readRegistry(), databaseStatus()]);
return Promise.all(items.map(async (item) => {
const [runtime, storage] = await Promise.all([inspectContainer(item.slug), storageBytes(item.slug)]);
return { ...item, ...runtime, database, storageBytes: storage };
}));
}
export async function getBusiness(slug: string) {
const business = (await listBusinesses()).find((item) => item.slug === slug);
if (!business) throw new Error("Business deployment not found.");
return business;
}
export async function createBusiness(input: ProvisionBusinessInput) {
const existing = (await readRegistry()).some((item) => item.slug === input.slug);
if (existing) throw new Error("A business with this slug already exists.");
const script = path.join(process.cwd(), "scripts", "create-business.sh");
await run(script, [input.slug, input.domain, input.image, input.name], { maxBuffer: 1024 * 1024 * 4 });
return getBusiness(input.slug);
}
async function compose(slug: string, args: string[]) {
const composeFile = path.join(getManagerConfig().stackRoot, slug, "compose.yaml");
await run("docker", ["compose", "-f", composeFile, ...args], { maxBuffer: 1024 * 1024 * 4 });
}
export async function startBusiness(slug: string) { await compose(slug, ["up", "-d"]); return getBusiness(slug); }
export async function stopBusiness(slug: string) { await compose(slug, ["stop"]); return getBusiness(slug); }
export async function restartBusiness(slug: string) { await compose(slug, ["restart"]); return getBusiness(slug); }
export async function updateBusiness(slug: string) {
// A local tag may not exist in a registry, so a failed pull is allowed to
// fall through to Compose using the already-built local image.
try { await compose(slug, ["pull"]); } catch { /* local/self-hosted image */ }
await compose(slug, ["up", "-d"]);
return getBusiness(slug);
}
export async function getBusinessLogs(slug: string) {
const composeFile = path.join(getManagerConfig().stackRoot, slug, "compose.yaml");
const result = await run("docker", ["compose", "-f", composeFile, "logs", "--tail", "200", `nextchair-${slug}`], { maxBuffer: 1024 * 1024 * 4 });
return result.stdout;
}
export async function removeBusiness(slug: string) {
const items = await readRegistry();
const target = items.find((item) => item.slug === slug);
if (!target) throw new Error("Business deployment not found.");
await compose(slug, ["down"]);
await fs.rm(path.join(getManagerConfig().stackRoot, slug), { recursive: true, force: false });
await fs.rm(path.join(getManagerConfig().caddySitesDir, `${slug}.caddy`), { force: false });
await writeRegistry(items.filter((item) => item.slug !== slug));
}
-22
View File
@@ -1,22 +0,0 @@
export type BusinessDeployment = {
name: string;
slug: string;
domain: string;
image: string;
createdAt: string;
updatedAt: string;
};
export type BusinessStatus = BusinessDeployment & {
container: "running" | "stopped" | "unknown";
health: "healthy" | "unhealthy" | "starting" | "unknown";
database: "online" | "offline" | "unknown";
storageBytes: number | null;
};
export type ProvisionBusinessInput = {
name: string;
slug: string;
domain: string;
image: string;
};