1{ config, pkgs, lib, ... }:
2let
3 inherit (lib) any boolToString concatStringsSep isBool isString mapAttrsToList mkDefault mkEnableOption mkIf mkMerge mkOption optionalAttrs types;
4
5 package = pkgs.dolibarr.override { inherit (cfg) stateDir; };
6
7 cfg = config.services.dolibarr;
8 vhostCfg = lib.optionalAttrs (cfg.nginx != null) config.services.nginx.virtualHosts."${cfg.domain}";
9
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" ];
14
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 '';
27
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;
40
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 };
47in
48{
49 # interface
50 options.services.dolibarr = {
51 enable = mkEnableOption (lib.mdDoc "dolibarr");
52
53 domain = mkOption {
54 type = types.str;
55 default = "localhost";
56 description = lib.mdDoc ''
57 Domain name of your server.
58 '';
59 };
60
61 user = mkOption {
62 type = types.str;
63 default = "dolibarr";
64 description = lib.mdDoc ''
65 User account under which dolibarr runs.
66
67 ::: {.note}
68 If left as the default value this user will automatically be created
69 on system activation, otherwise you are responsible for
70 ensuring the user exists before the dolibarr application starts.
71 :::
72 '';
73 };
74
75 group = mkOption {
76 type = types.str;
77 default = "dolibarr";
78 description = lib.mdDoc ''
79 Group account under which dolibarr runs.
80
81 ::: {.note}
82 If left as the default value this group will automatically be created
83 on system activation, otherwise you are responsible for
84 ensuring the group exists before the dolibarr application starts.
85 :::
86 '';
87 };
88
89 stateDir = mkOption {
90 type = types.str;
91 default = "/var/lib/dolibarr";
92 description = lib.mdDoc ''
93 State and configuration directory dolibarr will use.
94 '';
95 };
96
97 database = {
98 host = mkOption {
99 type = types.str;
100 default = "localhost";
101 description = lib.mdDoc "Database host address.";
102 };
103 port = mkOption {
104 type = types.port;
105 default = 3306;
106 description = lib.mdDoc "Database host port.";
107 };
108 name = mkOption {
109 type = types.str;
110 default = "dolibarr";
111 description = lib.mdDoc "Database name.";
112 };
113 user = mkOption {
114 type = types.str;
115 default = "dolibarr";
116 description = lib.mdDoc "Database username.";
117 };
118 passwordFile = mkOption {
119 type = with types; nullOr path;
120 default = null;
121 example = "/run/keys/dolibarr-dbpassword";
122 description = lib.mdDoc "Database password file.";
123 };
124 createLocally = mkOption {
125 type = types.bool;
126 default = true;
127 description = lib.mdDoc "Create the database and database user locally.";
128 };
129 };
130
131 settings = mkOption {
132 type = with types; (attrsOf (oneOf [ bool int str ]));
133 default = { };
134 description = lib.mdDoc "Dolibarr settings, see <https://github.com/Dolibarr/dolibarr/blob/develop/htdocs/conf/conf.php.example> for details.";
135 };
136
137 nginx = mkOption {
138 type = types.nullOr (types.submodule (
139 lib.recursiveUpdate
140 (import ../web-servers/nginx/vhost-options.nix { inherit config lib; })
141 {
142 # enable encryption by default,
143 # as sensitive login and Dolibarr (ERP) data should not be transmitted in clear text.
144 options.forceSSL.default = true;
145 options.enableACME.default = true;
146 }
147 ));
148 default = null;
149 example = lib.literalExpression ''
150 {
151 serverAliases = [
152 "dolibarr.''${config.networking.domain}"
153 "erp.''${config.networking.domain}"
154 ];
155 enableACME = false;
156 }
157 '';
158 description = lib.mdDoc ''
159 With this option, you can customize an nginx virtual host which already has sensible defaults for Dolibarr.
160 Set to {} if you do not need any customization to the virtual host.
161 If enabled, then by default, the {option}`serverName` is
162 `''${domain}`,
163 SSL is active, and certificates are acquired via ACME.
164 If this is set to null (the default), no nginx virtualHost will be configured.
165 '';
166 };
167
168 poolConfig = mkOption {
169 type = with types; attrsOf (oneOf [ str int bool ]);
170 default = {
171 "pm" = "dynamic";
172 "pm.max_children" = 32;
173 "pm.start_servers" = 2;
174 "pm.min_spare_servers" = 2;
175 "pm.max_spare_servers" = 4;
176 "pm.max_requests" = 500;
177 };
178 description = lib.mdDoc ''
179 Options for the Dolibarr PHP pool. See the documentation on [`php-fpm.conf`](https://www.php.net/manual/en/install.fpm.configuration.php)
180 for details on configuration directives.
181 '';
182 };
183 };
184
185 # implementation
186 config = mkIf cfg.enable (mkMerge [
187 {
188
189 assertions = [
190 { assertion = cfg.database.createLocally -> cfg.database.user == cfg.user;
191 message = "services.dolibarr.database.user must match services.dolibarr.user if the database is to be automatically provisioned";
192 }
193 ];
194
195 services.dolibarr.settings = {
196 dolibarr_main_url_root = "https://${cfg.domain}";
197 dolibarr_main_document_root = "${package}/htdocs";
198 dolibarr_main_url_root_alt = "/custom";
199 dolibarr_main_data_root = "${cfg.stateDir}/documents";
200
201 dolibarr_main_db_host = cfg.database.host;
202 dolibarr_main_db_port = toString cfg.database.port;
203 dolibarr_main_db_name = cfg.database.name;
204 dolibarr_main_db_prefix = "llx_";
205 dolibarr_main_db_user = cfg.database.user;
206 dolibarr_main_db_pass = mkIf (cfg.database.passwordFile != null) ''
207 file_get_contents("${cfg.database.passwordFile}")
208 '';
209 dolibarr_main_db_type = "mysqli";
210 dolibarr_main_db_character_set = mkDefault "utf8";
211 dolibarr_main_db_collation = mkDefault "utf8_unicode_ci";
212
213 # Authentication settings
214 dolibarr_main_authentication = mkDefault "dolibarr";
215
216 # Security settings
217 dolibarr_main_prod = true;
218 dolibarr_main_force_https = vhostCfg.forceSSL or false;
219 dolibarr_main_restrict_os_commands = "${pkgs.mariadb}/bin/mysqldump, ${pkgs.mariadb}/bin/mysql";
220 dolibarr_nocsrfcheck = false;
221 dolibarr_main_instance_unique_id = ''
222 file_get_contents("${cfg.stateDir}/dolibarr_main_instance_unique_id")
223 '';
224 dolibarr_mailing_limit_sendbyweb = false;
225 };
226
227 systemd.tmpfiles.rules = [
228 "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group}"
229 "d '${cfg.stateDir}/documents' 0750 ${cfg.user} ${cfg.group}"
230 "f '${cfg.stateDir}/conf.php' 0660 ${cfg.user} ${cfg.group}"
231 "L '${cfg.stateDir}/install.forced.php' - ${cfg.user} ${cfg.group} - ${mkConfigFile "install.forced.php" install}"
232 ];
233
234 services.mysql = mkIf cfg.database.createLocally {
235 enable = mkDefault true;
236 package = mkDefault pkgs.mariadb;
237 ensureDatabases = [ cfg.database.name ];
238 ensureUsers = [
239 { name = cfg.database.user;
240 ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
241 }
242 ];
243 };
244
245 services.nginx.enable = mkIf (cfg.nginx != null) true;
246 services.nginx.virtualHosts."${cfg.domain}" = mkIf (cfg.nginx != null) (lib.mkMerge [
247 cfg.nginx
248 ({
249 root = lib.mkForce "${package}/htdocs";
250 locations."/".index = "index.php";
251 locations."~ [^/]\\.php(/|$)" = {
252 extraConfig = ''
253 fastcgi_split_path_info ^(.+?\.php)(/.*)$;
254 fastcgi_pass unix:${config.services.phpfpm.pools.dolibarr.socket};
255 '';
256 };
257 })
258 ]);
259
260 systemd.services."phpfpm-dolibarr".after = mkIf cfg.database.createLocally [ "mysql.service" ];
261 services.phpfpm.pools.dolibarr = {
262 inherit (cfg) user group;
263 phpPackage = pkgs.php.buildEnv {
264 extensions = { enabled, all }: enabled ++ [ all.calendar ];
265 # recommended by dolibarr web application
266 extraConfig = ''
267 session.use_strict_mode = 1
268 session.cookie_samesite = "Lax"
269 ; open_basedir = "${package}/htdocs, ${cfg.stateDir}"
270 allow_url_fopen = 0
271 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"
272 '';
273 };
274
275 settings = {
276 "listen.mode" = "0660";
277 "listen.owner" = cfg.user;
278 "listen.group" = cfg.group;
279 } // cfg.poolConfig;
280 };
281
282 # there are several challenges with dolibarr and NixOS which we can address here
283 # - the dolibarr installer cannot be entirely automated, though it can partially be by including a file called install.forced.php
284 # - the dolibarr installer requires write access to its config file during installation, though not afterwards
285 # - 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
286 systemd.services.dolibarr-config = {
287 description = "dolibarr configuration file management via NixOS";
288 wantedBy = [ "multi-user.target" ];
289
290 script = ''
291 # 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
292 ${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);"
293
294 # replace configuration file generated by installer with the NixOS generated configuration file
295 install -m 644 ${mkConfigFile "conf.php" cfg.settings} '${cfg.stateDir}/conf.php'
296 '';
297
298 serviceConfig = {
299 Type = "oneshot";
300 User = cfg.user;
301 Group = cfg.group;
302 RemainAfterExit = "yes";
303 };
304
305 unitConfig = {
306 ConditionFileNotEmpty = "${cfg.stateDir}/conf.php";
307 };
308 };
309
310 users.users.dolibarr = mkIf (cfg.user == "dolibarr" ) {
311 isSystemUser = true;
312 group = cfg.group;
313 };
314
315 users.groups = optionalAttrs (cfg.group == "dolibarr") {
316 dolibarr = { };
317 };
318 }
319 (mkIf (cfg.nginx != null) {
320 users.users."${config.services.nginx.group}".extraGroups = mkIf (cfg.nginx != null) [ cfg.group ];
321 })
322]);
323}