feat:Update pnpm-lock.yaml to link shared package; modify API routes to utilize new schemas for domain and service management; enhance DNS record handling with CNAME support; refactor service and subdomain routes for improved functionality; implement confirm dialog for domain deletion in the frontend; clean up unused components and improve UI consistency.
Build, Test, and Push CFDM Docker Image / test (push) Failing after 51s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Has been skipped
Build, Test, and Push CFDM Docker Image / update-wiki (push) Has been skipped
Build, Test, and Push CFDM Docker Image / create-release (push) Has been skipped

This commit is contained in:
Denozordec
2026-06-22 13:52:33 +07:00
parent 4a70cf5854
commit 6d1d8ca4f3
99 changed files with 6692 additions and 1483 deletions
+257
View File
@@ -0,0 +1,257 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { repos } from "@cfdm/db";
import {
CERT_ERROR,
CERT_MONITOR_REQUIRED,
CERT_MONITOR_SKIPPED,
} from "@cfdm/shared";
import { buildApp } from "../src/app.js";
import { loadConfig } from "../src/config.js";
import * as certificateService from "../src/services/certificate-service.js";
async function authHeaders(app: Awaited<ReturnType<typeof buildApp>>) {
const config = loadConfig();
const res = await app.inject({
method: "POST",
url: "/api/v1/auth/login",
payload: { username: config.adminUsername, password: "admin" },
});
expect(res.statusCode).toBe(200);
const { token } = res.json() as { token: string };
return { authorization: `Bearer ${token}` };
}
describe("certificates", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("auto mode does not monitor DNS-only domain", async () => {
const testApp = await buildApp({
config: { ...loadConfig(), staticDir: null },
memory: true,
});
const headers = await authHeaders(testApp);
const domain = repos.createDomain(
testApp.db,
null,
"dns-only.example.com",
"cf-zone-dns",
);
vi.spyOn(certificateService, "checkHostname").mockResolvedValue({
expiresAt: null,
error: "connection refused",
});
const checkRes = await testApp.inject({
method: "POST",
url: "/api/v1/certificates/check",
headers,
});
expect(checkRes.statusCode).toBe(200);
const certs = repos.listCertificates(testApp.db);
expect(certs.find((c) => c.hostname === domain.zone_name)).toBeUndefined();
await testApp.close();
});
it("monitors host with enabled service binding", async () => {
const testApp = await buildApp({
config: { ...loadConfig(), staticDir: null },
memory: true,
});
const headers = await authHeaders(testApp);
const domain = repos.createDomain(
testApp.db,
null,
"app.example.com",
"cf-zone-app",
);
const service = repos.createService(testApp.db, "Web", "web");
repos.setServiceEnabled(testApp.db, service.id, true);
repos.insertBinding(testApp.db, domain.id, service.id, "api", null);
const expiresAt = new Date(Date.now() + 90 * 24 * 60 * 60 * 1000);
vi.spyOn(certificateService, "checkHostname").mockResolvedValue({
expiresAt,
error: null,
});
await testApp.inject({
method: "POST",
url: "/api/v1/certificates/check",
headers,
});
const certs = repos.listCertificates(testApp.db);
expect(certs.some((c) => c.hostname === "api.app.example.com")).toBe(true);
expect(certs.some((c) => c.hostname === "app.example.com")).toBe(false);
await testApp.close();
});
it("does not monitor host when service is disabled", async () => {
const testApp = await buildApp({
config: { ...loadConfig(), staticDir: null },
memory: true,
});
const headers = await authHeaders(testApp);
const domain = repos.createDomain(
testApp.db,
null,
"off.example.com",
"cf-zone-off",
);
const service = repos.createService(testApp.db, "Off", "off");
repos.insertBinding(testApp.db, domain.id, service.id, "@", null);
vi.spyOn(certificateService, "checkHostname").mockResolvedValue({
expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000),
error: null,
});
await testApp.inject({
method: "POST",
url: "/api/v1/certificates/check",
headers,
});
expect(
repos.listCertificates(testApp.db).some((c) => c.hostname === domain.zone_name),
).toBe(false);
await testApp.close();
});
it("required apex is monitored without bindings", async () => {
const testApp = await buildApp({
config: { ...loadConfig(), staticDir: null },
memory: true,
});
const headers = await authHeaders(testApp);
const domain = repos.createDomain(
testApp.db,
null,
"required.example.com",
"cf-zone-req",
);
repos.updateDomain(
testApp.db,
domain.id,
null,
"active",
CERT_MONITOR_REQUIRED,
);
vi.spyOn(certificateService, "checkHostname").mockResolvedValue({
expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000),
error: null,
});
await testApp.inject({
method: "POST",
url: "/api/v1/certificates/check",
headers,
});
expect(
repos.listCertificates(testApp.db).some(
(c) => c.hostname === domain.zone_name,
),
).toBe(true);
await testApp.close();
});
it("skipped apex removes stale certificate on check", async () => {
const testApp = await buildApp({
config: { ...loadConfig(), staticDir: null },
memory: true,
});
const headers = await authHeaders(testApp);
const domain = repos.createDomain(
testApp.db,
null,
"skipped.example.com",
"cf-zone-skip",
);
repos.upsertCertificateCheck(
testApp.db,
domain.id,
null,
domain.zone_name,
null,
CERT_ERROR,
"stale",
);
repos.updateDomain(
testApp.db,
domain.id,
null,
"active",
CERT_MONITOR_SKIPPED,
);
vi.spyOn(certificateService, "checkHostname").mockResolvedValue({
expiresAt: null,
error: "should not be called",
});
await testApp.inject({
method: "POST",
url: "/api/v1/certificates/check",
headers,
});
expect(repos.listCertificates(testApp.db)).toHaveLength(0);
expect(certificateService.checkHostname).not.toHaveBeenCalled();
await testApp.close();
});
it("TLS failure on monitored host is stored as error", async () => {
const testApp = await buildApp({
config: { ...loadConfig(), staticDir: null },
memory: true,
});
const headers = await authHeaders(testApp);
const domain = repos.createDomain(
testApp.db,
null,
"broken.example.com",
"cf-zone-broken",
);
repos.updateDomain(
testApp.db,
domain.id,
null,
"active",
CERT_MONITOR_REQUIRED,
);
vi.spyOn(certificateService, "checkHostname").mockResolvedValue({
expiresAt: null,
error: "certificate has expired",
});
await testApp.inject({
method: "POST",
url: "/api/v1/certificates/check",
headers,
});
const cert = repos.listCertificates(testApp.db)[0];
expect(cert?.status).toBe(CERT_ERROR);
expect(cert?.last_error).toBeTruthy();
await testApp.close();
});
});
+106
View File
@@ -0,0 +1,106 @@
import { describe, expect, it } from "vitest";
import { createMemoryDb, repos, runMigrations } from "@cfdm/db";
import { SYNC_CONFLICT, SYNC_ERROR, SYNC_SYNCED } from "@cfdm/shared";
import type { CloudflareClient } from "../src/lib/cf-client.js";
import { pullSync } from "../src/services/sync-service.js";
const ZONE = "rkns.top";
const CF_ZONE_ID = "zone-rkns";
function mockCf(remote: Array<{
id: string;
type: string;
name: string;
content: string;
}>): CloudflareClient {
return {
listDnsRecords: async () =>
remote.map((record) => ({
...record,
ttl: 1,
proxied: false,
})),
} as unknown as CloudflareClient;
}
function setupDb() {
const { db, sqlite } = createMemoryDb();
runMigrations(sqlite);
return db;
}
describe("pullSync", () => {
it("reconciles short local names with Cloudflare FQDNs", async () => {
const db = setupDb();
const domain = repos.createDomain(db, null, ZONE, CF_ZONE_ID);
repos.insertDnsRecord(
db,
domain.id,
"A",
"de",
"193.233.134.103",
1,
false,
null,
SYNC_CONFLICT,
"local",
"cf-de",
);
const cf = mockCf([
{
id: "cf-de",
type: "A",
name: "de.rkns.top",
content: "193.233.134.103",
},
]);
const changes = await pullSync(db, cf, domain);
expect(changes).toBeGreaterThan(0);
const records = repos.listDnsByDomain(db, domain.id);
expect(records).toHaveLength(1);
expect(records[0].name).toBe("de.rkns.top");
expect(records[0].sync_status).toBe(SYNC_SYNCED);
});
it("marks local record as conflict when Cloudflare has another type", async () => {
const db = setupDb();
const domain = repos.createDomain(db, null, ZONE, CF_ZONE_ID);
repos.insertDnsRecord(
db,
domain.id,
"A",
"mhome",
"185.244.181.61",
1,
false,
null,
SYNC_ERROR,
"local",
null,
);
const cf = mockCf([
{
id: "cf-mhome",
type: "CNAME",
name: "mhome.rkns.top",
content: "mmsk.rkns.top",
},
]);
await pullSync(db, cf, domain);
const records = repos.listDnsByDomain(db, domain.id);
const localA = records.find((r) => r.record_type === "A");
expect(localA?.sync_status).toBe(SYNC_CONFLICT);
expect(localA?.last_error).toBe("type mismatch with cloudflare");
const localCname = records.find((r) => r.record_type === "CNAME");
expect(localCname?.sync_status).toBe(SYNC_SYNCED);
});
});
+64
View File
@@ -0,0 +1,64 @@
import { describe, expect, it } from "vitest";
import { repos } from "@cfdm/db";
import { buildApp } from "../src/app.js";
import { loadConfig } from "../src/config.js";
async function authHeaders(app: Awaited<ReturnType<typeof buildApp>>) {
const config = loadConfig();
const res = await app.inject({
method: "POST",
url: "/api/v1/auth/login",
payload: { username: config.adminUsername, password: "admin" },
});
expect(res.statusCode).toBe(200);
const { token } = res.json() as { token: string };
return { authorization: `Bearer ${token}` };
}
describe("services reorder", () => {
it("PATCH /services/reorder persists order within group and ungrouped", async () => {
const app = await buildApp({
config: { ...loadConfig(), staticDir: null },
memory: true,
});
const headers = await authHeaders(app);
const group = repos.createServiceGroup(app.db, "VPN", "vpn", null, null);
const alpha = repos.createService(app.db, "Alpha", "alpha");
const beta = repos.createService(app.db, "Beta", "beta");
const gamma = repos.createService(app.db, "Gamma", "gamma");
repos.setServiceGroup(app.db, alpha.id, group.id);
repos.setServiceGroup(app.db, beta.id, group.id);
const reorderRes = await app.inject({
method: "PATCH",
url: "/api/v1/services/reorder",
headers,
payload: {
group_id: group.id,
service_ids: [beta.id, alpha.id],
},
});
expect(reorderRes.statusCode).toBe(200);
const grouped = repos.listServicesByGroup(app.db, group.id);
expect(grouped.map((s) => s.id)).toEqual([beta.id, alpha.id]);
const ungroupedReorderRes = await app.inject({
method: "PATCH",
url: "/api/v1/services/reorder",
headers,
payload: {
group_id: null,
service_ids: [gamma.id],
},
});
expect(ungroupedReorderRes.statusCode).toBe(200);
const ungrouped = repos.listUngroupedServices(app.db);
expect(ungrouped[0]?.id).toBe(gamma.id);
await app.close();
});
});
+62
View File
@@ -0,0 +1,62 @@
import { describe, expect, it } from "vitest";
import { repos } from "@cfdm/db";
import { buildApp } from "../src/app.js";
import { loadConfig } from "../src/config.js";
async function authHeaders(app: Awaited<ReturnType<typeof buildApp>>) {
const config = loadConfig();
const res = await app.inject({
method: "POST",
url: "/api/v1/auth/login",
payload: { username: config.adminUsername, password: "admin" },
});
expect(res.statusCode).toBe(200);
const { token } = res.json() as { token: string };
return { authorization: `Bearer ${token}` };
}
describe("subdomains", () => {
it("PATCH /subdomains/:id toggles enabled and renames", async () => {
const app = await buildApp({
config: { ...loadConfig(), staticDir: null },
memory: true,
});
const headers = await authHeaders(app);
const domain = repos.createDomain(
app.db,
null,
"example.com",
"cf-zone-1",
);
const created = repos.createSubdomain(
app.db,
domain.id,
"www",
"www.example.com",
);
expect(created.enabled).toBe(true);
const disableRes = await app.inject({
method: "PATCH",
url: `/api/v1/subdomains/${created.id}`,
headers,
payload: { enabled: false },
});
expect(disableRes.statusCode).toBe(200);
expect(disableRes.json().enabled).toBe(false);
const renameRes = await app.inject({
method: "PATCH",
url: `/api/v1/subdomains/${created.id}`,
headers,
payload: { name: "api" },
});
expect(renameRes.statusCode).toBe(200);
const renamed = renameRes.json();
expect(renamed.name).toBe("api");
expect(renamed.fqdn).toBe("api.example.com");
await app.close();
});
});