Files
mesh-catalog/modules/nextcloud/index.ts
T
jschoubben 4021724ca4 nextcloud: make the mesh's own account match its secret when Nextcloud refuses it, and back off a refusal that stays (issue 1253)
The image creates NEXTCLOUD_ADMIN_USER only on a fresh install. novox's
instance was installed before the mesh adopted it, so mesh-admin was never
created there, and the shares poll asked Nextcloud every minute with a login
that cannot pass: 401, then Nextcloud's brute-force throttle (429), which
also held the backup before a delivery.

On a refused login the client now adds the account through occ (admin
group), or sets its password to the module's own secret, passing it by
OC_PASS so it is on no argv, and asks once more. A refusal that stays is
said once and the poll backs off from 5 minutes to an hour instead of nine
lines of XML every minute. An empty setting no longer becomes an empty name.
2026-10-25 16:40:01 +01:00

80 lines
3.5 KiB
TypeScript

// nextcloud's events. The tool runtime imports this once the broker is bound. It watches the user
// list and the share list and announces new arrivals.
//
// Emits (novox/hq ADR 0041/0042):
// module.nextcloud.user.created — a user account appeared (occ user:list)
// module.nextcloud.share.created — a share appeared (OCS shares)
//
// Both are diffed and primed silently on the first look, so a restart does not re-announce every
// existing user and share as freshly created.
import { emit } from "@novox/mesh-sdk/events";
import { Backoff, NextcloudClient, pollSharesGuarded } from "./client.js";
// Constructed lazily so an unconfigured node (no admin password) loads this entrypoint without
// crashing the events host — it simply watches nothing.
let nextcloud: NextcloudClient | undefined;
try {
nextcloud = NextcloudClient.fromEnv();
} catch (err) {
console.log(`[nextcloud] not configured, not watching: ${err}`);
}
const knownUsers = new Set<string>();
let usersPrimed = false;
async function pollUsers(client: NextcloudClient): Promise<void> {
const users = client.listUsers();
for (const u of users) {
if (knownUsers.has(u.uid)) continue;
if (usersPrimed) await emit("user.created", { uid: u.uid, displayName: u.displayName });
knownUsers.add(u.uid);
}
usersPrimed = true;
}
const knownShares = new Set<string>();
let sharesPrimed = false;
async function pollShares(client: NextcloudClient): Promise<void> {
const shares = await client.listShares();
for (const s of shares) {
if (knownShares.has(s.id)) continue;
if (sharesPrimed) await emit("share.created", { id: s.id, path: s.path, shareType: s.shareType, shareWith: s.shareWith, owner: s.owner });
knownShares.add(s.id);
}
sharesPrimed = true;
}
// Keeps Nextcloud's onlyoffice settings matched to the document server the mesh bound (novox/hq
// issue 1830). The client never logs; this says what it did. A failed connection check is run
// again on every later tick until it passes (issue 1832): every failing tick is logged, and one
// line marks it passing again; nothing is logged while the last check passed and nothing changed.
let onlyOfficeCheckWasFailing = false;
async function applyOnlyOffice(client: NextcloudClient): Promise<void> {
const result = client.applyOnlyOffice();
if (result.changed.length > 0) console.log(`[nextcloud] onlyoffice: set ${result.changed.join(", ")}`);
if (result.checkOk === false) {
onlyOfficeCheckWasFailing = true;
console.error("[nextcloud] onlyoffice: connection check failed" + (result.changed.length > 0 ? " after applying settings" : ""));
} else if (result.checkOk === true && onlyOfficeCheckWasFailing) {
onlyOfficeCheckWasFailing = false;
console.log("[nextcloud] onlyoffice: connection check passes again");
}
}
if (nextcloud) {
const client = nextcloud;
const tick = (fn: (c: NextcloudClient) => Promise<void>): void => {
const run = (): void => void fn(client).catch((err) => console.error(`[nextcloud] ${err}`));
setInterval(run, 60_000);
run();
};
tick(pollUsers);
// A refused login is said once and the shares are left alone for 5 minutes, doubling to an hour,
// instead of a refusal every minute that Nextcloud's brute-force protection then throttles
// (novox/hq issue 1253).
const sharesBackoff = new Backoff(5 * 60_000, 60 * 60_000);
tick((c) => pollSharesGuarded(c, sharesBackoff, pollShares));
tick(applyOnlyOffice);
console.log("[nextcloud] watching users, shares and onlyoffice settings");
}