blob: 30014c3434926064fec1b7eefe46f63359cb7d02 [file] [log] [blame]
# SPDX-FileCopyrightText: 2024 Clicks Codes
#
# SPDX-License-Identifier: GPL-3.0-only
{
lib,
config,
pkgs,
...
}:
let
cfg = config.clicks.services.fava;
in
{
options.clicks.services.fava = {
enable = lib.mkEnableOption "The fava web beancount accounting server";
tailscaleAuth = lib.mkEnableOption "Lock fava to only be accessible on your tailnet - recommended as fava does not contain any authentication";
readOnly = lib.mkEnableOption "Make fava unable to edit the files, only read them";
iWillSetUpAuthMyself = lib.mkOption {
type = lib.types.bool;
internal = true;
description = "Disable warnings about disrecommended or possibly unsafe authentication configurations";
};
accounts = lib.mkOption {
type = lib.home-manager.hm.types.dagOf lib.types.str;
example = {
"minion" = "Skyler Grey";
"clicks" = "Clicks Codes";
"coded" = "Samuel Shuert";
};
description = "A mapping of file names to ledger titles. Titles will not override any existing ledgers, they will only be added to new ones";
};
addr = lib.mkOption {
type = lib.types.str;
description = "The host to listen on";
default = "127.0.0.1";
};
port = lib.mkOption {
type = lib.types.int;
description = "The port to listen on";
default = 1025;
};
domain = lib.mkOption {
type = lib.types.str;
description = "The domain that fava should be hosted on";
};
};
config = lib.mkIf cfg.enable {
warnings =
(if !(cfg.tailscaleAuth || cfg.readOnly || cfg.iWillSetUpAuthMyself)
then [ ("You have not made fava either restricted to a tailnet or read only. " +
"Unless you set up auth yourself or do not route traffic from the internet to this server, this is a really bad idea. " +
"To confirm you know what you're doing, set 'clicks.services.fava.iWillSetUpAuthMyself = true' to turn off this warning") ]
else []) ++
(if cfg.addr == "0.0.0.0" && cfg.tailscaleAuth && !cfg.iWillSetUpAuthMyself
then [ ("You have started hosting fava on '0.0.0.0' but additionally enabled tailnet-only access. We won't be able to control access " +
"that doesn't come through nginx, so any traffic that is allowed to the machine on port ${cfg.port} will still get to fava, tailnet or not. "
"To confirm you know what you're doing, set 'clicks.services.fava.iWillSetUpAuthMyself = true' to turn off this warning") ]
else []);
clicks = {
services.nginx.enable = true;
services.nginx.hosts.${cfg.domain} = {
service = lib.clicks.nginx.http.reverseProxy cfg.addr cfg.port;
www = false;
authWith = if cfg.tailscaleAuth then "tailscale" else null;
};
networking.tailscale.enable = lib.mkIf cfg.tailscaleAuth true;
storage.impermanence.persist.directories = [
{ directory = "/var/lib/private/fava"; mode = "0700"; defaultPerms.mode = "0700"; } # Set defaultperms.mode because systemd also needs /var/lib/private 0700
];
};
systemd.services.fava = let
getFilePath = name: "/var/lib/private/fava/${name}.beancount";
# Cannot use $STATE_DIRECTORY here as:
# (1) it's not supported via fava environment variables
# (2) it is a symlink and fava does not like that
attrNamesToPaths = accounts: lib.attrsets.mapAttrs' (name: value: {
name = getFilePath name;
inherit value;
}) accounts;
sortedAccounts = (lib.home-manager.hm.dag.topoSort cfg.accounts).result;
fileNames = lib.trivial.pipe sortedAccounts [
(map (account: account.name))
(map getFilePath)
(lib.strings.concatStringsSep " ")
];
accountToCreationScript = { name, data }: let
path = getFilePath name;
in ''
${pkgs.coreutils}/bin/mkdir -p "$(${pkgs.coreutils}/bin/dirname "${path}")"
if [ ! -f "${path}" ]; then
${pkgs.coreutils}/bin/echo 'option "title" "${data}"' >> ${path}
fi
'';
fileCreateCommands = lib.trivial.pipe sortedAccounts [
(map accountToCreationScript)
(lib.strings.concatStringsSep "\n")
];
in {
requires = (if config.clicks.services.nginx.enable then [ "nginx.service" ] else []);
after = (if config.clicks.services.nginx.enable then [ "nginx.service" ] else []);
environment = {
FAVA_HOST = cfg.addr;
FAVA_PORT = builtins.toString cfg.port;
};
serviceConfig = {
StateDirectory = "fava";
DynamicUser = true;
};
script = "${pkgs.fava}/bin/fava ${if cfg.readOnly then "--read-only" else ""} ${fileNames}";
preStart = fileCreateCommands;
wantedBy = ["multi-user.target"];
};
};
}