Moves the Prosody server, xmpp-web frontends (chat/plaza), and Forgejo's wiki-update webhook notifier from pinix to blunix, co-located with Traefik/ACME so TLS comes straight from Traefik's cert store instead of the old manual copy process. pinix's prosody/xmpp-web/webhook modules stay in place but deactivated as a fallback, matching the earlier Forgejo migration's pattern. Implements docs/intent/xmpp-umzug-pinix-blunix.md. Deployed and verified live on blunix (nixos-rebuild switch, chat/plaza returning 200, prosody healthy, agent XMPP listeners reconnected). Two first-bootstrap bugs found and fixed during that verification, both specific to bringing Prosody up on a host it never ran on before: - prosody-certs-sync.service used to call a blocking `systemctl reload-or-restart prosody.service` whenever the cert file differed from what's installed -- including on first activation, where prosody.service's own start job is still queued behind this unit (Requires+After), deadlocking forever. Now only reloads an already-running Prosody; ordering alone brings up a fresh one with the cert just installed. - hosts/blunix/localServices/secrets.nix force-owned the global `bindPW` secret to root with the (previously correct) rationale that it belongs to a pinix-only service. Prosody now runs on blunix too and needs to read it directly as its own LDAP bind config include, so that override no longer applies to it. pinix's own deploy (data migration, DNS/webhook cutover) is separate follow-up work, not part of this commit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ANg2L2xUmNJ45t9mcv6HVT
271 lines
8.7 KiB
Nix
271 lines
8.7 KiB
Nix
# Moved from hosts/pinix/localServices/xmpp-web.nix on 2026-09-06
|
|
# (docs/intent/xmpp-umzug-pinix-blunix.md), alongside setup-prosody.nix.
|
|
# nginx no longer terminates TLS itself: Traefik already does that for
|
|
# every other co-located blunix service (see traefik/prosody.nix), so
|
|
# these vhosts are now plain HTTP behind it on loopback, and no longer
|
|
# need their own certificate or public firewall opening.
|
|
{ config, lib, pkgs, ... }:
|
|
|
|
let
|
|
xmppWebVersion = "0.12.0";
|
|
|
|
launcherPalette = {
|
|
red = "#ff0026";
|
|
strawberry = "#d91e4d";
|
|
pink = "#ff0094";
|
|
violet = "#e000ff";
|
|
orange = "#ff5a00";
|
|
};
|
|
|
|
xmppWebArchive = pkgs.fetchurl {
|
|
url = "https://github.com/nioc/xmpp-web/releases/download/${xmppWebVersion}/xmpp-web-${xmppWebVersion}.tar.gz";
|
|
hash = "sha256-IL9MLf75yP7gqMaV6vzYVXlGqWwIlbdmEK5wH831Ybc=";
|
|
};
|
|
|
|
xmppWebCustomCss = pkgs.writeText "xmpp-web-0ff-space.css" ''
|
|
${builtins.readFile ../../globalServices/branding/0ff-space-background.css}
|
|
|
|
html {
|
|
--palette-1: ${launcherPalette.red};
|
|
--palette-2: ${launcherPalette.strawberry};
|
|
--palette-3: ${launcherPalette.pink};
|
|
--palette-4: ${launcherPalette.violet};
|
|
--palette-5: ${launcherPalette.orange};
|
|
}
|
|
|
|
body,
|
|
#body,
|
|
.hero.is-fullheight,
|
|
.hero.is-fullheight-with-navbar {
|
|
background-color: transparent !important;
|
|
background-image: none !important;
|
|
}
|
|
|
|
/* Registered/Login.vue's <h3 class="title ..."> wraps both the logo
|
|
(logoSrc falls back to a bundled default icon whenever
|
|
window.config.logoUrl is falsy -- and both configs below set it
|
|
to "", which *is* falsy in JS, so it never actually suppressed
|
|
anything) and the "{{ appName }}" text ("0ff.space chat"). Hiding
|
|
the whole heading removes both and brings the "Login" subtitle
|
|
right below it up to the top of the box, directly above the
|
|
fields. The four stacked classes are specific enough to this one
|
|
element that this shouldn't catch any other heading. Guest/
|
|
Login.vue (plaza) has no equivalent heading, so this is a no-op
|
|
there. */
|
|
.title.has-text-grey.is-flex.is-justify-content-center.is-align-items-center {
|
|
display: none !important;
|
|
}
|
|
|
|
/* The "Login" subtitle right below the (now-hidden) heading is
|
|
redundant with the bold "Login" submit button further down the
|
|
same form -- it doesn't mean "you're not logged in", it's just
|
|
the form's own label. */
|
|
form > p.subtitle.has-text-grey {
|
|
display: none !important;
|
|
}
|
|
|
|
'';
|
|
|
|
# Two independent xmpp-web builds sharing the same upstream release, one
|
|
# per frontend/vhost (plaza = anonymous guest lobby, chat = login with a
|
|
# real 0ff.space account). Same tar extraction + local.js swap either
|
|
# way, only autoJoinLobby (guest-only redirect-into-lobby behavior)
|
|
# differs structurally enough to need the extra step.
|
|
mkXmppWeb = { name, configJs, autoJoinLobby ? false }:
|
|
let
|
|
configFile = pkgs.writeText "xmpp-web-local-${name}.js" configJs;
|
|
in
|
|
pkgs.runCommand "xmpp-web-${xmppWebVersion}-${name}"
|
|
{
|
|
nativeBuildInputs = [
|
|
pkgs.gnutar
|
|
pkgs.gzip
|
|
];
|
|
}
|
|
(''
|
|
mkdir -p "$out"
|
|
tar -xzf ${xmppWebArchive} --strip-components=1 -C "$out"
|
|
cp ${configFile} "$out/local.js"
|
|
cp ${xmppWebCustomCss} "$out/0ff-space.css"
|
|
substituteInPlace "$out/index.html" \
|
|
--replace-fail '</head>' '<link rel="stylesheet" href="./0ff-space.css"></head>'
|
|
|
|
# Replace Bulma's five semantic color families with the launcher
|
|
# palette, including their common hover/active/focus variants.
|
|
for css in "$out"/assets/*.css; do
|
|
substituteInPlace "$css" \
|
|
--replace-warn '#2da192' '${launcherPalette.strawberry}' \
|
|
--replace-warn '#2a9789' '#c31945' \
|
|
--replace-warn '#278d80' '#ad153d' \
|
|
--replace-warn '#3e8ed0' '${launcherPalette.violet}' \
|
|
--replace-warn '#3488ce' '#cd00e8' \
|
|
--replace-warn '#3082c5' '#b900d1' \
|
|
--replace-warn '#48c78e' '${launcherPalette.pink}' \
|
|
--replace-warn '#3ec487' '#e60085' \
|
|
--replace-warn '#3abb81' '#cc0076' \
|
|
--replace-warn '#ffe08a' '${launcherPalette.orange}' \
|
|
--replace-warn '#ffdc7d' '#e85100' \
|
|
--replace-warn '#ffd970' '#d14900' \
|
|
--replace-warn '#f14668' '${launcherPalette.red}' \
|
|
--replace-warn '#f03a5f' '#e60022' \
|
|
--replace-warn '#ef2e55' '#cc001e'
|
|
done
|
|
''
|
|
+ lib.optionalString autoJoinLobby ''
|
|
substituteInPlace "$out/index.html" \
|
|
--replace-fail '<head>' '<head><script>if (!window.location.hash) window.location.replace("/#/guest?join=lobby%40muc.${config.domain}");</script>'
|
|
'');
|
|
|
|
plazaConfigJs = ''
|
|
// eslint-disable-next-line no-unused-vars, no-var
|
|
var config = {
|
|
name: '0ff.space public chat',
|
|
transports: {
|
|
websocket: 'wss://plaza.${config.domain}/xmpp-websocket',
|
|
},
|
|
hasGuestAccess: true,
|
|
hasRegisteredAccess: false,
|
|
anonymousHost: 'anon.${config.domain}',
|
|
isTransportsUserAllowed: false,
|
|
hasHttpAutoDiscovery: false,
|
|
resource: '0ff.space Web Chat',
|
|
defaultDomain: '${config.domain}',
|
|
defaultMuc: 'muc.${config.domain}',
|
|
isStylingDisabled: false,
|
|
hasSendingEnterKey: true,
|
|
connectTimeout: 5000,
|
|
pinnedMucs: [
|
|
'lobby@muc.${config.domain}',
|
|
],
|
|
logoUrl: "",
|
|
sso: {
|
|
endpoint: false,
|
|
jidHeader: 'jid',
|
|
passwordHeader: 'password',
|
|
},
|
|
guestDescription: '<p>Öffentlicher Chat auf 0ff.space. Wähle einen Nicknamen und schreibe ohne Account mit.</p>',
|
|
}
|
|
'';
|
|
|
|
# Registered-only client for real 0ff.space accounts (same LDAP-backed
|
|
# auth Monal uses on the main vhost) -- no guest access, no forced
|
|
# redirect into the lobby, just a normal client landing on the user's
|
|
# own roster/joined rooms.
|
|
chatConfigJs = ''
|
|
// eslint-disable-next-line no-unused-vars, no-var
|
|
var config = {
|
|
name: '0ff.space chat',
|
|
transports: {
|
|
websocket: 'wss://chat.${config.domain}/xmpp-websocket',
|
|
},
|
|
hasGuestAccess: false,
|
|
hasRegisteredAccess: true,
|
|
isTransportsUserAllowed: false,
|
|
hasHttpAutoDiscovery: false,
|
|
resource: '0ff.space Web Chat',
|
|
defaultDomain: '${config.domain}',
|
|
defaultMuc: 'muc.${config.domain}',
|
|
isStylingDisabled: false,
|
|
hasSendingEnterKey: true,
|
|
connectTimeout: 5000,
|
|
logoUrl: "",
|
|
sso: {
|
|
endpoint: false,
|
|
jidHeader: 'jid',
|
|
passwordHeader: 'password',
|
|
},
|
|
}
|
|
'';
|
|
|
|
xmppWebPlaza = mkXmppWeb {
|
|
name = "plaza";
|
|
configJs = plazaConfigJs;
|
|
autoJoinLobby = true;
|
|
};
|
|
|
|
xmppWebChat = mkXmppWeb {
|
|
name = "chat";
|
|
configJs = chatConfigJs;
|
|
};
|
|
|
|
websocketLocation = {
|
|
proxyPass = "http://127.0.0.1:5280/xmpp-websocket";
|
|
proxyWebsockets = true;
|
|
extraConfig = ''
|
|
proxy_read_timeout 900s;
|
|
'';
|
|
};
|
|
|
|
rootLocation = {
|
|
tryFiles = "$uri $uri/ /index.html";
|
|
};
|
|
|
|
# Both vhosts share one plain-HTTP listen socket; nginx dispatches by
|
|
# Host header the same way it dispatched by SNI before, just without a
|
|
# TLS handshake of its own -- Traefik (traefik/prosody.nix) already
|
|
# terminated TLS before the request reaches here.
|
|
listenOn8080 = [
|
|
{
|
|
addr = "127.0.0.1";
|
|
port = 8080;
|
|
}
|
|
];
|
|
in
|
|
{
|
|
services.nginx = {
|
|
enable = true;
|
|
recommendedProxySettings = true;
|
|
|
|
virtualHosts."plaza.${config.domain}" = {
|
|
listen = listenOn8080;
|
|
root = xmppWebPlaza;
|
|
|
|
locations."/" = rootLocation;
|
|
locations."/xmpp-websocket" = websocketLocation;
|
|
};
|
|
|
|
virtualHosts."chat.${config.domain}" = {
|
|
listen = listenOn8080;
|
|
root = xmppWebChat;
|
|
|
|
locations."/" = rootLocation;
|
|
locations."/xmpp-websocket" = websocketLocation;
|
|
};
|
|
};
|
|
|
|
services.prosody.virtualHosts."anon.${config.domain}" = {
|
|
enabled = true;
|
|
domain = "anon.${config.domain}";
|
|
extraConfig = ''
|
|
authentication = "anonymous"
|
|
allow_anonymous_s2s = false
|
|
|
|
modules_enabled = {
|
|
"websocket";
|
|
}
|
|
|
|
modules_disabled = {
|
|
"bookmarks";
|
|
"cloud_notify";
|
|
"mam";
|
|
"pep";
|
|
"private";
|
|
"proxy65";
|
|
"register";
|
|
"roster";
|
|
"vcard_legacy";
|
|
}
|
|
|
|
http_host = "plaza.${config.domain}"
|
|
http_external_url = "https://plaza.${config.domain}/"
|
|
http_paths = {
|
|
websocket = "/xmpp-websocket";
|
|
}
|
|
cross_domain_websocket = { "https://plaza.${config.domain}"; }
|
|
'';
|
|
ssl = {
|
|
cert = "/var/lib/prosody/certs/${config.domain}.crt.pem";
|
|
key = "/var/lib/prosody/certs/${config.domain}.key.pem";
|
|
};
|
|
};
|
|
}
|