Merge pull request 'Actual Budget module (NixOS 25.05)' (#157) from cl0vrfi3ld/selfprivacy-nixos-config:actual into flakes

Reviewed-on: https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config/pulls/157
Reviewed-by: Inex Code <inex.code@selfprivacy.org>
This commit is contained in:
Inex Code
2025-07-07 11:46:32 +03:00
5 changed files with 417 additions and 0 deletions

View File

@@ -0,0 +1,59 @@
[
[
"services",
"actual"
],
[
"systemd",
"services",
"actual"
],
[
"selfprivacy",
"domain"
],
[
"selfprivacy",
"useBinds"
],
[
"selfprivacy",
"modules",
"actual"
],
[
"selfprivacy",
"modules",
"auth",
"enable"
],
[
"selfprivacy",
"sso",
"enable"
],
[
"selfprivacy",
"passthru",
"auth",
"mkOAuth2ClientSecretFP"
],
[
"selfprivacy",
"passthru",
"auth",
"mkServiceAccountTokenFP"
],
[
"selfprivacy",
"passthru",
"auth",
"oauth2-discovery-url"
],
[
"selfprivacy",
"passthru",
"auth",
"oauth2-provider-name"
]
]

View File

@@ -0,0 +1,40 @@
{
description = "Actual (aka Actual Budget) is a super fast and privacy-focused app for managing your finances.";
outputs =
{ self }:
{
nixosModules.default = import ./module.nix;
configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json);
meta =
{ lib, ... }:
{
spModuleSchemaVersion = 1;
id = "actual";
name = "Actual";
description = "Actual (aka Actual Budget) is a super fast and privacy-focused app for managing your finances.";
svgIcon = builtins.readFile ./icon.svg;
showUrl = true;
primarySubdomain = "subdomain";
isMovable = true;
isRequired = false;
canBeBackedUp = true;
backupDescription = "Your budgets, settings, and account secrets (where applicable).";
systemdServices = [
"actual.service"
];
user = "actual";
group = "actual";
folders = [
"/var/lib/actual"
];
license = [
lib.licenses.mit
];
homepage = "https://actualbudget.org/";
sourcePage = "https://github.com/actualbudget/actual";
supportLevel = "community";
};
};
}

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="511.99997"
height="512.00012"
viewBox="0 0 135.46666 135.4667"
version="1.1"
id="svg1"
xml:space="preserve"
sodipodi:docname="icon.svg"
inkscape:version="1.4.2 (ebf0e940, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="0.14207697"
inkscape:cx="837.57418"
inkscape:cy="112.61502"
inkscape:window-width="1320"
inkscape:window-height="719"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="svg1"><inkscape:page
x="0"
y="0"
width="135.46666"
height="135.46671"
id="page2"
margin="0"
bleed="0" /></sodipodi:namedview><defs
id="defs1" /><g
id="layer1"
style="display:inline"
transform="matrix(0.37209672,0,0,0.37220296,295.55537,76.048367)"><path
style="fill:#7241d5;stroke:none"
d="m -617.60526,-204.09814 c -13.81494,1.81372 -27.27957,2.21964 -40.92222,5.80667 -39.22608,10.31367 -74.22837,33.76786 -98.8186,66.01027 -13.13472,17.22207 -22.89294,36.833512 -29.24844,57.502772 -19.22678,62.529063 -1.61568,132.119065 43.41426,179.211108 15.64477,16.36121 34.82444,29.61585 55.37222,39.04947 31.62109,14.51737 66.92692,18.98631 101.24722,14.47341 89.54029,-11.77409 158.47753,-93.797723 156.27538,-183.697878 -1.9196,-78.366402 -54.46872,-147.693892 -129.46427,-170.753142 -14.57617,-4.48181 -29.92928,-7.03182 -45.15555,-7.4048 -4.21288,-0.10318 -8.47515,-0.75254 -12.7,-0.19788 m 67.73333,185.058605 c 4.5742,-1.08398 9.00494,-3.310593 13.40556,-4.956593 1.47404,-0.55134 3.66284,-1.96681 5.27616,-1.4514 2.84569,0.90914 4.48332,8.755523 5.44273,11.346883 0.43133,1.16498 0.90242,2.65487 0.004,3.73302 -1.01004,1.2125404 -3.29553,1.7114304 -4.72602,2.2465004 -3.82641,1.43122 -7.6572,3.32876 -11.64167,4.25103 1.0266,3.56372002 3.2351,6.89777 4.8477,10.23056 3.23986,6.6957996 6.37646,13.4546896 9.70329,20.1083296 1.37377,2.74753 5.59571,8.50814 5.01457,11.62642 -0.26446,1.41904 -1.84644,1.99966 -2.985,2.57181 -2.2604,1.13585 -9.56096,6.19042 -11.76961,3.99187 -3.22627,-3.21152 -5.14648,-9.84061 -7.0365,-13.95677 -4.35546,-9.48542 -9.64048,-18.58043 -13.64945,-28.2222196 -35.61369,12.0482096 -70.26267,27.3790796 -105.48055,40.5517396 -8.37255,3.13164 -16.72722,6.34555 -25.04722,9.6143 -1.96993,0.77394 -6.74736,3.651582 -8.53091,1.58015 -3.14387,-3.65134 0.81077,-8.75192 2.33062,-12.23508 4.82647,-11.06117 10.73824,-21.66241 15.37251,-32.8083296 -4.21574,1.8686996 -8.70903,3.2790396 -13.05278,4.8302796 -1.48101,0.52891 -3.69887,1.91461 -5.21626,0.89092 -2.02088,-1.36337 -2.70738,-5.6585896 -3.5354,-7.8378696 -0.62723,-1.65082 -2.06826,-4.1924 -1.70428,-5.99067 0.32311,-1.59632998 2.08216,-2.04186998 3.40039,-2.54378 3.39342,-1.29204 6.8355,-2.45501 10.23055,-3.74499 4.56257,-1.73357 9.17431,-3.34889 13.75834,-5.0254404 1.92198,-0.70295 4.68469,-1.21216 6.21226,-2.6268 2.38188,-2.20583 3.58538,-6.80811 4.98544,-9.701653 13.46881,-27.83626 25.76509,-56.26194 39.51492,-83.961112 3.27898,-6.60554 6.5134,-13.34927 9.46266,-20.10833 0.86136,-1.97406 2.08467,-6.32808 4.39829,-6.94342 1.50503,-0.40027 4.31554,-0.41856 5.49146,0.70556 2.19849,2.10166 3.45188,6.31598 4.67485,9.06008 2.2769,5.10895 4.8742,10.16452 7.37024,15.16945 9.4005,18.849512 18.27175,37.959002 27.69086,56.797222 3.62236,7.24475 7.06022,14.58252 10.58742,21.87222 1.72186,3.55859 4.10457,7.13175 5.20048,10.936113 m -58.91389,-82.197225 -8.35778,17.991662 -18.19174,38.80556 -6.69869,14.11111 -4.14623,9.525 43.03889,-15.85622 21.87222,-8.13267 c -1.4127,-4.90582 -4.35245,-9.55299 -6.6154,-14.11111 -3.83431,-7.72321 -7.43211,-15.56923 -11.28917,-23.28333 -3.00628,-6.01258 -5.31888,-13.86853 -9.6121,-19.050002 m -59.97222,128.058335 30.33889,-11.49196 73.025,-28.01915 -8.11389,-16.933343 -59.97222,21.9400134 -15.52222,5.68527 -7.61358,3.21393 -4.13697,8.31913 z"
id="path25105" /></g></svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="23.999994"
height="24.000004"
viewBox="0 0 6.3499986 6.3500011"
version="1.1"
id="svg1"
xml:space="preserve"
sodipodi:docname="icon.svg"
inkscape:version="1.4.2 (ebf0e940, 2025-05-08)"
inkscape:export-filename="icon-lg.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
inkscape:zoom="9.7583124"
inkscape:cx="11.272441"
inkscape:cy="5.7386972"
inkscape:window-width="1320"
inkscape:window-height="719"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" /><defs
id="defs1" /><g
id="layer1"
style="display:inline"
transform="matrix(0.01744203,0,0,0.01744701,13.854155,3.5647664)"><path
style="fill:#000000;stroke:none;fill-opacity:1"
d="m -617.60526,-204.09814 c -13.81494,1.81372 -27.27957,2.21964 -40.92222,5.80667 -39.22608,10.31367 -74.22837,33.76786 -98.8186,66.01027 -13.13472,17.22207 -22.89294,36.833512 -29.24844,57.502772 -19.22678,62.529063 -1.61568,132.119065 43.41426,179.211108 15.64477,16.36121 34.82444,29.61585 55.37222,39.04947 31.62109,14.51737 66.92692,18.98631 101.24722,14.47341 89.54029,-11.77409 158.47753,-93.797723 156.27538,-183.697878 -1.9196,-78.366402 -54.46872,-147.693892 -129.46427,-170.753142 -14.57617,-4.48181 -29.92928,-7.03182 -45.15555,-7.4048 -4.21288,-0.10318 -8.47515,-0.75254 -12.7,-0.19788 m 67.73333,185.058605 c 4.5742,-1.08398 9.00494,-3.310593 13.40556,-4.956593 1.47404,-0.55134 3.66284,-1.96681 5.27616,-1.4514 2.84569,0.90914 4.48332,8.755523 5.44273,11.346883 0.43133,1.16498 0.90242,2.65487 0.004,3.73302 -1.01004,1.2125404 -3.29553,1.7114304 -4.72602,2.2465004 -3.82641,1.43122 -7.6572,3.32876 -11.64167,4.25103 1.0266,3.56372002 3.2351,6.89777 4.8477,10.23056 3.23986,6.6957996 6.37646,13.4546896 9.70329,20.1083296 1.37377,2.74753 5.59571,8.50814 5.01457,11.62642 -0.26446,1.41904 -1.84644,1.99966 -2.985,2.57181 -2.2604,1.13585 -9.56096,6.19042 -11.76961,3.99187 -3.22627,-3.21152 -5.14648,-9.84061 -7.0365,-13.95677 -4.35546,-9.48542 -9.64048,-18.58043 -13.64945,-28.2222196 -35.61369,12.0482096 -70.26267,27.3790796 -105.48055,40.5517396 -8.37255,3.13164 -16.72722,6.34555 -25.04722,9.6143 -1.96993,0.77394 -6.74736,3.651582 -8.53091,1.58015 -3.14387,-3.65134 0.81077,-8.75192 2.33062,-12.23508 4.82647,-11.06117 10.73824,-21.66241 15.37251,-32.8083296 -4.21574,1.8686996 -8.70903,3.2790396 -13.05278,4.8302796 -1.48101,0.52891 -3.69887,1.91461 -5.21626,0.89092 -2.02088,-1.36337 -2.70738,-5.6585896 -3.5354,-7.8378696 -0.62723,-1.65082 -2.06826,-4.1924 -1.70428,-5.99067 0.32311,-1.59632998 2.08216,-2.04186998 3.40039,-2.54378 3.39342,-1.29204 6.8355,-2.45501 10.23055,-3.74499 4.56257,-1.73357 9.17431,-3.34889 13.75834,-5.0254404 1.92198,-0.70295 4.68469,-1.21216 6.21226,-2.6268 2.38188,-2.20583 3.58538,-6.80811 4.98544,-9.701653 13.46881,-27.83626 25.76509,-56.26194 39.51492,-83.961112 3.27898,-6.60554 6.5134,-13.34927 9.46266,-20.10833 0.86136,-1.97406 2.08467,-6.32808 4.39829,-6.94342 1.50503,-0.40027 4.31554,-0.41856 5.49146,0.70556 2.19849,2.10166 3.45188,6.31598 4.67485,9.06008 2.2769,5.10895 4.8742,10.16452 7.37024,15.16945 9.4005,18.849512 18.27175,37.959002 27.69086,56.797222 3.62236,7.24475 7.06022,14.58252 10.58742,21.87222 1.72186,3.55859 4.10457,7.13175 5.20048,10.936113 m -58.91389,-82.197225 -8.35778,17.991662 -18.19174,38.80556 -6.69869,14.11111 -4.14623,9.525 43.03889,-15.85622 21.87222,-8.13267 c -1.4127,-4.90582 -4.35245,-9.55299 -6.6154,-14.11111 -3.83431,-7.72321 -7.43211,-15.56923 -11.28917,-23.28333 -3.00628,-6.01258 -5.31888,-13.86853 -9.6121,-19.050002 m -59.97222,128.058335 30.33889,-11.49196 73.025,-28.01915 -8.11389,-16.933343 -59.97222,21.9400134 -15.52222,5.68527 -7.61358,3.21393 -4.13697,8.31913 z"
id="path25105" /></g></svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -0,0 +1,226 @@
{
config,
lib,
pkgs,
...
}:
let
# Just for convenience, this module's config values
sp = config.selfprivacy;
cfg = sp.modules.actual;
is-auth-enabled = cfg.enableSso && config.selfprivacy.sso.enable;
oauthClientID = "actual";
auth-passthru = config.selfprivacy.passthru.auth;
full-domain = "https://${cfg.subdomain}.${sp.domain}";
redirect-uri = "${full-domain}/openid/callback";
landing-uri = "${full-domain}/login";
oauthDiscoveryURL = auth-passthru.oauth2-discovery-url oauthClientID;
usersGroup = "sp.${oauthClientID}.users";
linuxUserOfService = "actual";
linuxGroupOfService = "actual";
oauthClientSecretFP = auth-passthru.mkOAuth2ClientSecretFP linuxGroupOfService;
oauthSecretDir = "/run/actual/shh";
oauthSecretFile = "${oauthSecretDir}/totallynotasecretfile.env";
# creates an env file with the oauth client secret and configures permissions for the actual user/group
oauthClientInjectScript = pkgs.writeShellScript "inject-oidc-secrets" ''
mkdir -p ${oauthSecretDir}
echo "ACTUAL_OPENID_CLIENT_SECRET=$(<${oauthClientSecretFP})" > ${oauthSecretFile}
chown actual:actual ${oauthSecretFile}
chmod 600 ${oauthSecretFile}
'';
in
{
options.selfprivacy.modules.actual = {
enable =
(lib.mkOption {
default = false;
type = lib.types.bool;
description = "Enable the Actual Budget server";
})
// {
meta = {
type = "enable";
};
};
location =
(lib.mkOption {
type = lib.types.str;
description = "Data location";
})
// {
meta = {
type = "location";
};
};
subdomain =
(lib.mkOption {
default = "actual";
type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
description = "Subdomain";
})
// {
meta = {
widget = "subdomain";
type = "string";
regex = "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
weight = 0;
};
};
# service settings
enableSso =
(lib.mkOption {
default = true;
type = lib.types.bool;
description = "Enable Single Sign-On";
})
// {
meta = {
type = "bool";
weight = 2;
};
};
enableDebug =
(lib.mkOption {
default = false;
type = lib.types.bool;
description = "Enable Debug Logging";
})
// {
meta = {
type = "bool";
weight = 3;
};
};
};
config = lib.mkIf cfg.enable (
lib.mkMerge [
{
# prevent SSO from being enabled in the module config if SSO isn't available/is disabled
assertions = [
{
assertion = cfg.enableSso -> sp.sso.enable;
message = "SSO cannot be enabled for Actual when SSO is disabled globally.";
}
];
fileSystems = lib.mkIf sp.useBinds {
"/var/lib/actual" = {
device = "/volumes/${cfg.location}/actual";
options = [
"bind"
"x-systemd.required-by=actual.service"
"x-systemd.before=actual.service"
];
};
};
# actual service config
services.actual = {
enable = true;
settings = {
port = 5006;
# default to only password logins
allowedLoginMethods = [ "password" ];
};
};
# adding the user/group to be used by the service
users = {
users.actual = {
isSystemUser = true;
group = "actual";
};
groups.actual = {};
};
systemd = {
services = {
actual = {
# extra guard against the service starting before the bind has been mounted
unitConfig.RequiresMountsFor = lib.mkIf sp.useBinds "/volumes/${cfg.location}/actual";
serviceConfig = {
Slice = "actual.slice";
# override dynamic user since service from nixpkgs enables by default, but it doesn't work in the selfprivacy environment
DynamicUser = lib.mkForce false;
# use service user
User = linuxUserOfService;
# use service group
Group = linuxGroupOfService;
};
environment =
# tell actual to log debug info to the console if option is enabled
(lib.mkIf cfg.enableDebug {
DEBUG = "actual:config,actual-sensitive:config";
}
);
};
};
slices.actual = {
description = "Actual server service slice";
};
};
services.nginx.virtualHosts."${cfg.subdomain}.${sp.domain}" = {
useACMEHost = sp.domain;
forceSSL = true;
locations = {
"/" = {
proxyPass = "http://127.0.0.1:5006";
};
};
};
}
# SSO config
(lib.mkIf is-auth-enabled {
services.actual = {
settings = {
# only permit openid logins
allowedLoginMethods = lib.mkForce [ "openid" ];
# default to openid if enabled
loginMethod = "openid";
# https://github.com/actualbudget/actual/pull/4421
userCreationMode = "login";
# service SSO config
openId = {
discoveryURL = oauthDiscoveryURL;
client_id = oauthClientID;
server_hostname = full-domain;
authMethod = "openid";
};
};
};
systemd.services.actual = {
serviceConfig={
# run inject script with root privileges
ExecStartPre = "+${oauthClientInjectScript}";
# use the file generated by the inject script, even if it doesn't yet exist
EnvironmentFile = "-${oauthSecretFile}";
RuntimeDirectory = "actual";
};
};
# OIDC for Actual is currently in beta and requires legacy cryptography algorithms
services.kanidm.provision.systems.oauth2."${oauthClientID}".enableLegacyCrypto = true;
selfprivacy.auth.clients."${oauthClientID}" = {
inherit usersGroup;
imageFile = ./icon-lg.svg;
displayName = "Actual";
subdomain = cfg.subdomain;
originLanding = landing-uri;
originUrl = redirect-uri;
clientSystemdUnits = [ "actual.service" ];
enablePkce = true;
linuxUserOfClient = linuxUserOfService;
linuxGroupOfClient = linuxGroupOfService;
useShortPreferredUsername = true;
scopeMaps.${usersGroup} = [ "email" "openid" "profile" ];
};
})
]
);
}