grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / web-apps / dolibarr.nix
blob3f9f853e3b252559750b1d92375e610197e3786c
1 { config, pkgs, lib, ... }:
2 let
3   inherit (lib) any boolToString concatStringsSep isBool isString mapAttrsToList mkDefault mkEnableOption mkIf mkMerge mkOption optionalAttrs types mkPackageOption;
5   package = cfg.package.override { inherit (cfg) stateDir; };
7   cfg = config.services.dolibarr;
8   vhostCfg = lib.optionalAttrs (cfg.nginx != null) config.services.nginx.virtualHosts."${cfg.domain}";
10   mkConfigFile = filename: settings:
11     let
12       # hack in special logic for secrets so we read them from a separate file avoiding the nix store
13       secretKeys = [ "force_install_databasepass" "dolibarr_main_db_pass" "dolibarr_main_instance_unique_id" ];
15       toStr = k: v:
16         if (any (str: k == str) secretKeys) then v
17         else if isString v then "'${v}'"
18         else if isBool v then boolToString v
19         else if v == null then "null"
20         else toString v
21       ;
22     in
23       pkgs.writeText filename ''
24         <?php
25         ${concatStringsSep "\n" (mapAttrsToList (k: v: "\$${k} = ${toStr k v};") settings)}
26       '';
28   # see https://github.com/Dolibarr/dolibarr/blob/develop/htdocs/install/install.forced.sample.php for all possible values
29   install = {
30     force_install_noedit = 2;
31     force_install_main_data_root = "${cfg.stateDir}/documents";
32     force_install_nophpinfo = true;
33     force_install_lockinstall = "444";
34     force_install_distrib = "nixos";
35     force_install_type = "mysqli";
36     force_install_dbserver = cfg.database.host;
37     force_install_port = toString cfg.database.port;
38     force_install_database = cfg.database.name;
39     force_install_databaselogin = cfg.database.user;
41     force_install_mainforcehttps = vhostCfg.forceSSL or false;
42     force_install_createuser = false;
43     force_install_dolibarrlogin = null;
44   } // optionalAttrs (cfg.database.passwordFile != null) {
45     force_install_databasepass = ''file_get_contents("${cfg.database.passwordFile}")'';
46   };
49   # interface
50   options.services.dolibarr = {
51     enable = mkEnableOption "dolibarr";
53     package = mkPackageOption pkgs "dolibarr" { };
55     domain = mkOption {
56       type = types.str;
57       default = "localhost";
58       description = ''
59         Domain name of your server.
60       '';
61     };
63     user = mkOption {
64       type = types.str;
65       default = "dolibarr";
66       description = ''
67         User account under which dolibarr runs.
69         ::: {.note}
70         If left as the default value this user will automatically be created
71         on system activation, otherwise you are responsible for
72         ensuring the user exists before the dolibarr application starts.
73         :::
74       '';
75     };
77     group = mkOption {
78       type = types.str;
79       default = "dolibarr";
80       description = ''
81         Group account under which dolibarr runs.
83         ::: {.note}
84         If left as the default value this group will automatically be created
85         on system activation, otherwise you are responsible for
86         ensuring the group exists before the dolibarr application starts.
87         :::
88       '';
89     };
91     stateDir = mkOption {
92       type = types.str;
93       default = "/var/lib/dolibarr";
94       description = ''
95         State and configuration directory dolibarr will use.
96       '';
97     };
99     database = {
100       host = mkOption {
101         type = types.str;
102         default = "localhost";
103         description = "Database host address.";
104       };
105       port = mkOption {
106         type = types.port;
107         default = 3306;
108         description = "Database host port.";
109       };
110       name = mkOption {
111         type = types.str;
112         default = "dolibarr";
113         description = "Database name.";
114       };
115       user = mkOption {
116         type = types.str;
117         default = "dolibarr";
118         description = "Database username.";
119       };
120       passwordFile = mkOption {
121         type = with types; nullOr path;
122         default = null;
123         example = "/run/keys/dolibarr-dbpassword";
124         description = "Database password file.";
125       };
126       createLocally = mkOption {
127         type = types.bool;
128         default = true;
129         description = "Create the database and database user locally.";
130       };
131     };
133     settings = mkOption {
134       type = with types; (attrsOf (oneOf [ bool int str ]));
135       default = { };
136       description = "Dolibarr settings, see <https://github.com/Dolibarr/dolibarr/blob/develop/htdocs/conf/conf.php.example> for details.";
137     };
139     nginx = mkOption {
140       type = types.nullOr (types.submodule (
141         lib.recursiveUpdate
142           (import ../web-servers/nginx/vhost-options.nix { inherit config lib; })
143           {
144             # enable encryption by default,
145             # as sensitive login and Dolibarr (ERP) data should not be transmitted in clear text.
146             options.forceSSL.default = true;
147             options.enableACME.default = true;
148           }
149       ));
150       default = null;
151       example = lib.literalExpression ''
152         {
153           serverAliases = [
154             "dolibarr.''${config.networking.domain}"
155             "erp.''${config.networking.domain}"
156           ];
157           enableACME = false;
158         }
159       '';
160       description = ''
161           With this option, you can customize an nginx virtual host which already has sensible defaults for Dolibarr.
162           Set to {} if you do not need any customization to the virtual host.
163           If enabled, then by default, the {option}`serverName` is
164           `''${domain}`,
165           SSL is active, and certificates are acquired via ACME.
166           If this is set to null (the default), no nginx virtualHost will be configured.
167       '';
168     };
170     poolConfig = mkOption {
171       type = with types; attrsOf (oneOf [ str int bool ]);
172       default = {
173         "pm" = "dynamic";
174         "pm.max_children" = 32;
175         "pm.start_servers" = 2;
176         "pm.min_spare_servers" = 2;
177         "pm.max_spare_servers" = 4;
178         "pm.max_requests" = 500;
179       };
180       description = ''
181         Options for the Dolibarr PHP pool. See the documentation on [`php-fpm.conf`](https://www.php.net/manual/en/install.fpm.configuration.php)
182         for details on configuration directives.
183       '';
184     };
185   };
187   # implementation
188   config = mkIf cfg.enable (mkMerge [
189     {
191     assertions = [
192       { assertion = cfg.database.createLocally -> cfg.database.user == cfg.user;
193         message = "services.dolibarr.database.user must match services.dolibarr.user if the database is to be automatically provisioned";
194       }
195     ];
197     services.dolibarr.settings = {
198       dolibarr_main_url_root = "https://${cfg.domain}";
199       dolibarr_main_document_root = "${package}/htdocs";
200       dolibarr_main_url_root_alt = "/custom";
201       dolibarr_main_data_root = "${cfg.stateDir}/documents";
203       dolibarr_main_db_host = cfg.database.host;
204       dolibarr_main_db_port = toString cfg.database.port;
205       dolibarr_main_db_name = cfg.database.name;
206       dolibarr_main_db_prefix = "llx_";
207       dolibarr_main_db_user = cfg.database.user;
208       dolibarr_main_db_pass = mkIf (cfg.database.passwordFile != null) ''
209         file_get_contents("${cfg.database.passwordFile}")
210       '';
211       dolibarr_main_db_type = "mysqli";
212       dolibarr_main_db_character_set = mkDefault "utf8";
213       dolibarr_main_db_collation = mkDefault "utf8_unicode_ci";
215       # Authentication settings
216       dolibarr_main_authentication = mkDefault "dolibarr";
218       # Security settings
219       dolibarr_main_prod = true;
220       dolibarr_main_force_https = vhostCfg.forceSSL or false;
221       dolibarr_main_restrict_os_commands = "${pkgs.mariadb}/bin/mysqldump, ${pkgs.mariadb}/bin/mysql";
222       dolibarr_nocsrfcheck = false;
223       dolibarr_main_instance_unique_id = ''
224         file_get_contents("${cfg.stateDir}/dolibarr_main_instance_unique_id")
225       '';
226       dolibarr_mailing_limit_sendbyweb = false;
227     };
229     systemd.tmpfiles.rules = [
230       "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group}"
231       "d '${cfg.stateDir}/documents' 0750 ${cfg.user} ${cfg.group}"
232       "f '${cfg.stateDir}/conf.php' 0660 ${cfg.user} ${cfg.group}"
233       "L '${cfg.stateDir}/install.forced.php' - ${cfg.user} ${cfg.group} - ${mkConfigFile "install.forced.php" install}"
234     ];
236     services.mysql = mkIf cfg.database.createLocally {
237       enable = mkDefault true;
238       package = mkDefault pkgs.mariadb;
239       ensureDatabases = [ cfg.database.name ];
240       ensureUsers = [
241         { name = cfg.database.user;
242           ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
243         }
244       ];
245     };
247     services.nginx.enable = mkIf (cfg.nginx != null) true;
248     services.nginx.virtualHosts."${cfg.domain}" = mkIf (cfg.nginx != null) (lib.mkMerge [
249       cfg.nginx
250       ({
251         root = lib.mkForce "${package}/htdocs";
252         locations."/".index = "index.php";
253         locations."~ [^/]\\.php(/|$)" = {
254           extraConfig = ''
255             fastcgi_split_path_info ^(.+?\.php)(/.*)$;
256             fastcgi_pass unix:${config.services.phpfpm.pools.dolibarr.socket};
257           '';
258         };
259       })
260     ]);
262     systemd.services."phpfpm-dolibarr".after = mkIf cfg.database.createLocally [ "mysql.service" ];
263     services.phpfpm.pools.dolibarr = {
264       inherit (cfg) user group;
265       phpPackage = pkgs.php.buildEnv {
266         extensions = { enabled, all }: enabled ++ [ all.calendar ];
267         # recommended by dolibarr web application
268         extraConfig = ''
269           session.use_strict_mode = 1
270           session.cookie_samesite = "Lax"
271           ; open_basedir = "${package}/htdocs, ${cfg.stateDir}"
272           allow_url_fopen = 0
273           disable_functions = "pcntl_alarm, pcntl_fork, pcntl_waitpid, pcntl_wait, pcntl_wifexited, pcntl_wifstopped, pcntl_wifsignaled, pcntl_wifcontinued, pcntl_wexitstatus, pcntl_wtermsig, pcntl_wstopsig, pcntl_signal, pcntl_signal_get_handler, pcntl_signal_dispatch, pcntl_get_last_error, pcntl_strerror, pcntl_sigprocmask, pcntl_sigwaitinfo, pcntl_sigtimedwait, pcntl_exec, pcntl_getpriority, pcntl_setpriority, pcntl_async_signals"
274         '';
275       };
277       settings = {
278         "listen.mode" = "0660";
279         "listen.owner" = cfg.user;
280         "listen.group" = cfg.group;
281       } // cfg.poolConfig;
282     };
284     # there are several challenges with dolibarr and NixOS which we can address here
285     # - the dolibarr installer cannot be entirely automated, though it can partially be by including a file called install.forced.php
286     # - the dolibarr installer requires write access to its config file during installation, though not afterwards
287     # - the dolibarr config file generally holds secrets generated by the installer, though the config file is a php file so we can read and write these secrets from an external file
288     systemd.services.dolibarr-config = {
289       description = "dolibarr configuration file management via NixOS";
290       wantedBy = [ "multi-user.target" ];
292       script = ''
293         # extract the 'main instance unique id' secret that the dolibarr installer generated for us, store it in a file for use by our own NixOS generated configuration file
294         ${pkgs.php}/bin/php -r "include '${cfg.stateDir}/conf.php'; file_put_contents('${cfg.stateDir}/dolibarr_main_instance_unique_id', \$dolibarr_main_instance_unique_id);"
296         # replace configuration file generated by installer with the NixOS generated configuration file
297         install -m 644 ${mkConfigFile "conf.php" cfg.settings} '${cfg.stateDir}/conf.php'
298       '';
300       serviceConfig = {
301         Type = "oneshot";
302         User = cfg.user;
303         Group = cfg.group;
304         RemainAfterExit = "yes";
305       };
307       unitConfig = {
308         ConditionFileNotEmpty = "${cfg.stateDir}/conf.php";
309       };
310     };
312     users.users.dolibarr = mkIf (cfg.user == "dolibarr" ) {
313       isSystemUser = true;
314       group = cfg.group;
315     };
317     users.groups = optionalAttrs (cfg.group == "dolibarr") {
318       dolibarr = { };
319     };
320   }
321   (mkIf (cfg.nginx != null) {
322     users.users."${config.services.nginx.group}".extraGroups = mkIf (cfg.nginx != null) [ cfg.group ];
323   })