1 { config, lib, pkgs, ... }:
6 cfg = config.services.snipe-it;
7 snipe-it = pkgs.snipe-it.override {
16 tlsEnabled = cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME;
18 inherit (snipe-it.passthru) phpPackage;
20 # shell script for local administration
21 artisan = (pkgs.writeScriptBin "snipe-it" ''
22 #! ${pkgs.runtimeShell}
23 cd "${snipe-it}/share/php/snipe-it"
25 if [[ "$USER" != ${user} ]]; then
26 sudo='exec /run/wrappers/bin/sudo -u ${user}'
28 $sudo ${phpPackage}/bin/php artisan $*
29 '').overrideAttrs (old: {
31 mainProgram = "snipe-it";
35 options.services.snipe-it = {
37 enable = mkEnableOption "snipe-it, a free open source IT asset/license management system";
41 description = "User snipe-it runs as.";
47 description = "Group snipe-it runs as.";
51 appKeyFile = mkOption {
53 A file containing the Laravel APP_KEY - a 32 character long,
54 base64 encoded key used for encryption where needed. Can be
55 generated with `head -c 32 /dev/urandom | base64`.
57 example = "/run/keys/snipe-it/appkey";
61 hostName = lib.mkOption {
63 default = config.networking.fqdnOrHostName;
64 defaultText = lib.literalExpression "config.networking.fqdnOrHostName";
65 example = "snipe-it.example.com";
67 The hostname to serve Snipe-IT on.
73 The root URL that you want to host Snipe-IT on. All URLs in Snipe-IT will be generated using this value.
74 If you change this in the future you may need to run a command to update stored URLs in the database.
75 Command example: `snipe-it snipe-it:update-url https://old.example.com https://new.example.com`
77 default = "http${lib.optionalString tlsEnabled "s"}://${cfg.hostName}";
79 http''${lib.optionalString tlsEnabled "s"}://''${cfg.hostName}
81 example = "https://example.com";
86 description = "snipe-it data directory";
87 default = "/var/lib/snipe-it";
94 default = "localhost";
95 description = "Database host address.";
100 description = "Database host port.";
105 description = "Database name.";
110 defaultText = literalExpression "user";
111 description = "Database username.";
113 passwordFile = mkOption {
114 type = with types; nullOr path;
116 example = "/run/keys/snipe-it/dbpassword";
118 A file containing the password corresponding to
119 {option}`database.user`.
122 createLocally = mkOption {
125 description = "Create the database and database user locally.";
131 type = types.enum [ "smtp" "sendmail" ];
133 description = "Mail driver to use.";
137 default = "localhost";
138 description = "Mail host address.";
143 description = "Mail host port.";
145 encryption = mkOption {
146 type = with types; nullOr (enum [ "tls" "ssl" ]);
148 description = "SMTP encryption mechanism to use.";
151 type = with types; nullOr str;
154 description = "Mail username.";
156 passwordFile = mkOption {
157 type = with types; nullOr path;
159 example = "/run/keys/snipe-it/mailpassword";
161 A file containing the password corresponding to
165 backupNotificationAddress = mkOption {
167 default = "backup@example.com";
168 description = "Email Address to send Backup Notifications to.";
173 default = "Snipe-IT Asset Management";
174 description = "Mail \"from\" name.";
178 default = "mail@example.com";
179 description = "Mail \"from\" address.";
185 default = "Snipe-IT Asset Management";
186 description = "Mail \"reply-to\" name.";
190 default = "mail@example.com";
191 description = "Mail \"reply-to\" address.";
196 maxUploadSize = mkOption {
200 description = "The maximum size for uploads (e.g. images).";
203 poolConfig = mkOption {
204 type = with types; attrsOf (oneOf [ str int bool ]);
207 "pm.max_children" = 32;
208 "pm.start_servers" = 2;
209 "pm.min_spare_servers" = 2;
210 "pm.max_spare_servers" = 4;
211 "pm.max_requests" = 500;
214 Options for the snipe-it PHP pool. See the documentation on `php-fpm.conf`
215 for details on configuration directives.
220 type = types.submodule (
222 (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) {}
225 example = literalExpression ''
228 "snipe-it.''${config.networking.domain}"
230 # To enable encryption and let let's encrypt take care of certificate
236 With this option, you can customize the nginx virtualHost settings.
255 type = nullOr (oneOf [ str path ]);
257 The path to a file containing the value the
258 option should be set to in the final
265 example = literalExpression ''
267 ALLOWED_IFRAME_HOSTS = "https://example.com";
268 WKHTMLTOPDF = "''${pkgs.wkhtmltopdf}/bin/wkhtmltopdf";
269 AUTH_METHOD = "oidc";
270 OIDC_NAME = "MyLogin";
271 OIDC_DISPLAY_NAME_CLAIMS = "name";
272 OIDC_CLIENT_ID = "snipe-it";
273 OIDC_CLIENT_SECRET = {_secret = "/run/keys/oidc_secret"};
274 OIDC_ISSUER = "https://keycloak.example.com/auth/realms/My%20Realm";
275 OIDC_ISSUER_DISCOVER = true;
279 Snipe-IT configuration options to set in the
281 Refer to <https://snipe-it.readme.io/docs/configuration>
282 for details on supported values.
284 Settings containing secret data should be set to an attribute
285 set containing the attribute `_secret` - a
286 string pointing to a file containing the value the option
287 should be set to. See the example to get a better picture of
288 this: in the resulting {file}`.env` file, the
289 `OIDC_CLIENT_SECRET` key will be set to the
290 contents of the {file}`/run/keys/oidc_secret`
296 config = mkIf cfg.enable {
299 { assertion = db.createLocally -> db.user == user;
300 message = "services.snipe-it.database.user must be set to ${user} if services.snipe-it.database.createLocally is set true.";
302 { assertion = db.createLocally -> db.passwordFile == null;
303 message = "services.snipe-it.database.passwordFile cannot be specified if services.snipe-it.database.createLocally is set to true.";
307 environment.systemPackages = [ artisan ];
309 services.snipe-it.config = {
310 APP_ENV = "production";
311 APP_KEY._secret = cfg.appKeyFile;
312 APP_URL = cfg.appURL;
315 DB_DATABASE = db.name;
316 DB_USERNAME = db.user;
317 DB_PASSWORD._secret = db.passwordFile;
318 MAIL_DRIVER = mail.driver;
319 MAIL_FROM_NAME = mail.from.name;
320 MAIL_FROM_ADDR = mail.from.address;
321 MAIL_REPLYTO_NAME = mail.from.name;
322 MAIL_REPLYTO_ADDR = mail.from.address;
323 MAIL_BACKUP_NOTIFICATION_ADDRESS = mail.backupNotificationAddress;
324 MAIL_HOST = mail.host;
325 MAIL_PORT = mail.port;
326 MAIL_USERNAME = mail.user;
327 MAIL_ENCRYPTION = mail.encryption;
328 MAIL_PASSWORD._secret = mail.passwordFile;
329 APP_SERVICES_CACHE = "/run/snipe-it/cache/services.php";
330 APP_PACKAGES_CACHE = "/run/snipe-it/cache/packages.php";
331 APP_CONFIG_CACHE = "/run/snipe-it/cache/config.php";
332 APP_ROUTES_CACHE = "/run/snipe-it/cache/routes-v7.php";
333 APP_EVENTS_CACHE = "/run/snipe-it/cache/events.php";
334 SESSION_SECURE_COOKIE = tlsEnabled;
337 services.mysql = mkIf db.createLocally {
339 package = mkDefault pkgs.mariadb;
340 ensureDatabases = [ db.name ];
343 ensurePermissions = { "${db.name}.*" = "ALL PRIVILEGES"; };
348 services.phpfpm.pools.snipe-it = {
349 inherit user group phpPackage;
351 post_max_size = ${cfg.maxUploadSize}
352 upload_max_filesize = ${cfg.maxUploadSize}
355 "listen.mode" = "0660";
356 "listen.owner" = user;
357 "listen.group" = group;
362 enable = mkDefault true;
363 virtualHosts."${cfg.hostName}" = mkMerge [ cfg.nginx {
364 root = mkForce "${snipe-it}/share/php/snipe-it/public";
365 extraConfig = optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "fastcgi_param HTTPS on;";
369 extraConfig = ''try_files $uri $uri/ /index.php?$query_string;'';
373 try_files $uri $uri/ /index.php?$query_string;
374 include ${config.services.nginx.package}/conf/fastcgi_params;
375 fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
376 fastcgi_param REDIRECT_STATUS 200;
377 fastcgi_pass unix:${config.services.phpfpm.pools."snipe-it".socket};
378 ${optionalString (cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME) "fastcgi_param HTTPS on;"}
381 "~ \.(js|css|gif|png|ico|jpg|jpeg)$" = {
382 extraConfig = "expires 365d;";
388 systemd.services.snipe-it-setup = {
389 description = "Preparation tasks for snipe-it";
390 before = [ "phpfpm-snipe-it.service" ];
391 after = optional db.createLocally "mysql.service";
392 wantedBy = [ "multi-user.target" ];
395 RemainAfterExit = true;
397 WorkingDirectory = snipe-it;
398 RuntimeDirectory = "snipe-it/cache";
399 RuntimeDirectoryMode = "0700";
401 path = [ pkgs.replace-secret artisan ];
404 isSecret = v: isAttrs v && v ? _secret && (isString v._secret || builtins.isPath v._secret);
405 snipeITEnvVars = lib.generators.toKeyValue {
406 mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" {
407 mkValueString = v: with builtins;
408 if isInt v then toString v
409 else if isString v then "\"${v}\""
410 else if true == v then "true"
411 else if false == v then "false"
412 else if isSecret v then
413 if (isString v._secret) then
414 hashString "sha256" v._secret
416 hashString "sha256" (builtins.readFile v._secret)
417 else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}";
420 secretPaths = lib.mapAttrsToList (_: v: v._secret) (lib.filterAttrs (_: isSecret) cfg.config);
421 mkSecretReplacement = file: ''
422 replace-secret ${escapeShellArgs [
424 if (isString file) then
425 builtins.hashString "sha256" file
427 builtins.hashString "sha256" (builtins.readFile file)
430 "${cfg.dataDir}/.env"
433 secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths;
434 filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! elem v [ {} null ])) cfg.config;
435 snipeITEnv = pkgs.writeText "snipeIT.env" (snipeITEnvVars filteredConfig);
444 install -T -m 0600 -o ${user} ${snipeITEnv} "${cfg.dataDir}/.env"
447 ${secretReplacements}
449 # prepend `base64:` if it does not exist in APP_KEY
450 if ! grep 'APP_KEY=base64:' "${cfg.dataDir}/.env" >/dev/null; then
451 sed -i 's/APP_KEY=/APP_KEY=base64:/' "${cfg.dataDir}/.env"
455 rm "${cfg.dataDir}"/bootstrap/cache/*.php || true
458 ${lib.getExe artisan} migrate --force
460 # A placeholder file for invalid barcodes
461 invalid_barcode_location="${cfg.dataDir}/public/uploads/barcodes/invalid_barcode.gif"
462 if [ ! -e "$invalid_barcode_location" ]; then
463 cp ${snipe-it}/share/snipe-it/invalid_barcode.gif "$invalid_barcode_location"
468 systemd.tmpfiles.rules = [
469 "d ${cfg.dataDir} 0710 ${user} ${group} - -"
470 "d ${cfg.dataDir}/bootstrap 0750 ${user} ${group} - -"
471 "d ${cfg.dataDir}/bootstrap/cache 0750 ${user} ${group} - -"
472 "d ${cfg.dataDir}/public 0750 ${user} ${group} - -"
473 "d ${cfg.dataDir}/public/uploads 0750 ${user} ${group} - -"
474 "d ${cfg.dataDir}/public/uploads/accessories 0750 ${user} ${group} - -"
475 "d ${cfg.dataDir}/public/uploads/assets 0750 ${user} ${group} - -"
476 "d ${cfg.dataDir}/public/uploads/avatars 0750 ${user} ${group} - -"
477 "d ${cfg.dataDir}/public/uploads/barcodes 0750 ${user} ${group} - -"
478 "d ${cfg.dataDir}/public/uploads/categories 0750 ${user} ${group} - -"
479 "d ${cfg.dataDir}/public/uploads/companies 0750 ${user} ${group} - -"
480 "d ${cfg.dataDir}/public/uploads/components 0750 ${user} ${group} - -"
481 "d ${cfg.dataDir}/public/uploads/consumables 0750 ${user} ${group} - -"
482 "d ${cfg.dataDir}/public/uploads/departments 0750 ${user} ${group} - -"
483 "d ${cfg.dataDir}/public/uploads/locations 0750 ${user} ${group} - -"
484 "d ${cfg.dataDir}/public/uploads/manufacturers 0750 ${user} ${group} - -"
485 "d ${cfg.dataDir}/public/uploads/models 0750 ${user} ${group} - -"
486 "d ${cfg.dataDir}/public/uploads/suppliers 0750 ${user} ${group} - -"
487 "d ${cfg.dataDir}/storage 0700 ${user} ${group} - -"
488 "d ${cfg.dataDir}/storage/app 0700 ${user} ${group} - -"
489 "d ${cfg.dataDir}/storage/fonts 0700 ${user} ${group} - -"
490 "d ${cfg.dataDir}/storage/framework 0700 ${user} ${group} - -"
491 "d ${cfg.dataDir}/storage/framework/cache 0700 ${user} ${group} - -"
492 "d ${cfg.dataDir}/storage/framework/sessions 0700 ${user} ${group} - -"
493 "d ${cfg.dataDir}/storage/framework/views 0700 ${user} ${group} - -"
494 "d ${cfg.dataDir}/storage/logs 0700 ${user} ${group} - -"
495 "d ${cfg.dataDir}/storage/uploads 0700 ${user} ${group} - -"
496 "d ${cfg.dataDir}/storage/private_uploads 0700 ${user} ${group} - -"
500 users = mkIf (user == "snipeit") {
505 "${config.services.nginx.user}".extraGroups = [ group ];
507 groups = mkIf (group == "snipeit") {
514 meta.maintainers = with maintainers; [ yayayayaka ];