{ config, lib, pkgs, modulesPath, ... }: let inherit (lib) mkDefault mkEnableOption mkForce mkIf mkMerge mkOption types boolToString callPackageWith optional optionals optionalString recursiveUpdate; cfg = config.services.snipe-it; phpPackage = cfg.phpPackage.withExtensions ({ enabled, all }: enabled ++ (with all; [ openssl pdo mbstring tokenizer curl ldap zip fileinfo bcmath gd ] ++ optionals useMysql [ pdo_mysql ]) ); nginxPackage = config.services.nginx.package; user = cfg.user; group = cfg.group; db = cfg.database; useMysql = db.type == "mysql"; useSqlite = db.type == "sqlite"; mail = cfg.mail; useSSL = with cfg.nginx; (addSSL || forceSSL || onlySSL || enableACME); snipe-it = cfg.package.override { inherit (cfg) cacheDir dataDir; php = phpPackage; phpPackages = phpPackage.packages; }; artisan = pkgs.writeShellScriptBin "snipe-it" '' cd ${snipe-it} sudo=exec if [[ "$USER" != ${user} ]]; then sudo='exec /run/wrappers/bin/sudo -u ${user}' fi $sudo ${phpPackage}/bin/php artisan $* ''; in { options.services.snipe-it = { enable = mkEnableOption "Snipe-IT free open source IT asset management"; package = mkOption { type = types.package; default = callPackageWith pkgs ../../pkgs/snipe-it { }; description = "Snipe-IT derivation to use."; }; phpPackage = mkOption { type = types.package; default = pkgs.php80; description = "PHP package to use."; }; user = mkOption { type = types.str; default = "snipe-it"; description = "User Snipe-IT runs as."; }; group = mkOption { type = types.str; default = "snipe-it"; description = "Group Snipe-IT runs as."; }; hostName = mkOption { type = types.str; example = "assets.example.com"; description = "FQDN for the Snipe-IT instance."; }; maxResults = mkOption { type = types.int; default = 500; description = '' The result limit. This value determines the maximum number of results to return, even if a higher limit is passed in an API request. This is done to prevent timeouts when custom scripts are requesting large numbers of assets at a time. ''; }; # Basic app settings appKeyFile = mkOption { type = types.path; example = "/run/keys/snipe-it-appkey"; description = '' A file containing the app key. Used for encryption where needed. Can be generated with head -c32 /dev/urandom | base64. ''; }; dataDir = mkOption { type = types.path; default = "/var/lib/snipe-it"; description = "Snipe-IT's data directory."; }; cacheDir = mkOption { type = types.path; default = "/var/cache/snipe-it"; description = "Snipe-IT's cache directory"; }; database = { type = mkOption { type = types.enum [ "mysql" "sqlite" ]; default = "mysql"; description = "Database engine to use."; }; host = mkOption { type = types.str; default = "localhost"; description = "Database host address."; }; port = mkOption { type = types.port; default = if useMysql then 3306 else 5432; description = '' Database host port. This currently only has an effect when using MySQL. ''; }; name = mkOption { type = types.str; default = "snipe-it"; description = "Name of the MySQL database."; }; username = mkOption { type = types.str; default = user; description = "Username to use to connect to database."; }; passwordFile = mkOption { type = types.nullOr types.path; example = "/run/keys/snipe-it-dbpass"; description = '' File containing the password corresponding to . ''; }; }; mail = { host = mkOption { type = types.str; default = "localhost"; description = "Mail server host address."; }; port = mkOption { type = types.port; default = 25; description = "Mail server host port."; }; encryption = mkOption { type = types.nullOr (types.enum [ "ssl" "tls" ]); default = null; example = "tls"; description = '' Type of transport encryption to use. ''; }; username = mkOption { type = types.nullOr types.str; default = null; description = "User to use to connect to mail server."; }; passwordFile = mkOption { type = types.path; example = "/run/keys/snipe-it-mailpass"; description = '' File containing the password corresponding to . ''; }; fromAddress = mkOption { type = types.str; description = ''Global "From" address.''; }; fromName = mkOption { type = types.str; default = "Snipe-IT"; description = ''Global "From" name.''; }; replytoAddress = mkOption { type = types.nullOr types.str; default = null; description = '' Global "Reply-To" address. If null (the default), "Reply-To" won't be set. ''; }; replytoName = mkOption { type = types.str; default = "Snipe-IT"; description = '' Global "Reply-To" name. also has to be set for this to have any effect. ''; }; autoEmbed = { enable = mkEnableOption "Embed images into mails instead of hyperlinking them."; method = mkOption { type = types.enum [ "attachment" "base64" ]; default = "attachment"; description = "Embedding method to use."; }; }; }; memcached = { enable = mkEnableOption "memcached as caching backend"; host = mkOption { type = types.str; default = "localhost"; description = "Memcached host address."; }; port = mkOption { type = types.port; default = 11211; description = "Memcached host port."; }; }; session = { lifeTime = mkOption { type = types.int; default = 12000; description = "Session lifetime in minutes."; }; expireOnClose = mkOption { type = types.bool; default = false; description = "Expire sessions when closing browser window."; }; encrypt = mkOption { type = types.bool; default = false; description = "Encrypt stored session data."; }; }; maxUploadSize = mkOption { type = types.str; default = "16M"; example = "1G"; description = "The maximum size for uploads (e.g. images)."; }; poolConfig = mkOption { type = with types; attrsOf (oneOf [ str int bool ]); default = { "pm" = "dynamic"; "pm.max_children" = 32; "pm.start_servers" = 2; "pm.min_spare_servers" = 2; "pm.max_spare_servers" = 4; "pm.max_requests" = 500; }; description = '' Options for the Snipe-IT PHP pool. See the documentation on php-fpm.conf for details on configuration directives. ''; }; nginx = mkOption { type = types.submodule ( recursiveUpdate (import (modulesPath + "/services/web-servers/nginx/vhost-options.nix") { inherit config lib; }) {} ); default = {}; example = { serverAliases = [ "snipe-it.\${config.networking.domain}" ]; # To enable encryption and let letsencrypt take care of certificate forceSSL = true; enableACME = true; }; description = '' With this option, you can customize the nginz virtualHost settings. ''; }; extraConfig = mkOption { type = types.nullOr types.lines; default = null; example = '' LOGIN_MAX_ATTEMPTS=3 LOGIN_LOCKOUT_DURATION=300 ''; }; }; config = mkIf cfg.enable { warnings = optional (!useMysql) '' Please note: Using another database than MySQL isn't officially supported. ''; environment.systemPackages = [ artisan ]; services.mysql = mkIf (useMysql && db.host == "localhost") { enable = mkDefault true; package = mkDefault pkgs.mariadb; ensureDatabases = [ db.name ]; ensureUsers = [{ name = db.username; ensurePermissions = { "${db.name}.*" = "ALL PRIVILEGES"; }; }]; }; services.phpfpm.pools.snipe-it = { inherit user group phpPackage; phpOptions = '' log_errors = on post_max_size = ${cfg.maxUploadSize} upload_max_filesize = ${cfg.maxUploadSize} ''; settings = { "listen.mode" = "0660"; "listen.owner" = user; "listen.group" = group; } // cfg.poolConfig; }; services.nginx = { enable = mkDefault true; virtualHosts."${cfg.hostName}" = mkMerge [ cfg.nginx { root = mkForce "${snipe-it}/public"; extraConfig = '' index index.php index.html index.htm; ${optionalString useSSL "fastcgi_param HTTPS on;"} ''; locations = { "/" = { extraConfig = ''try_files $uri $uri/ /index.php$is_args$args;''; }; "~ \.php$" = { extraConfig = '' try_files $uri $uri/ =404; include ${nginxPackage}/conf/fastcgi_params; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_pass unix:${config.services.phpfpm.pools."snipe-it".socket}; ${optionalString useSSL "fastcgi_param HTTPS on;"} ''; }; }; }]; }; systemd.tmpfiles.rules = [ "d ${cfg.cacheDir} 0700 ${user} ${group} - -" "d ${cfg.cacheDir}/bootstrap 0700 ${user} ${group} - -" "d ${cfg.dataDir} 0710 ${user} ${group} - -" "d ${cfg.dataDir}/uploads 0750 ${user} ${group} - -" "d ${cfg.dataDir}/uploads/accessories 0750 ${user} ${group} - -" "d ${cfg.dataDir}/uploads/assets 0750 ${user} ${group} - -" "d ${cfg.dataDir}/uploads/avatars 0750 ${user} ${group} - -" "d ${cfg.dataDir}/uploads/barcodes 0750 ${user} ${group} - -" "d ${cfg.dataDir}/uploads/categories 0750 ${user} ${group} - -" "d ${cfg.dataDir}/uploads/companies 0750 ${user} ${group} - -" "d ${cfg.dataDir}/uploads/components 0750 ${user} ${group} - -" "d ${cfg.dataDir}/uploads/consumables 0750 ${user} ${group} - -" "d ${cfg.dataDir}/uploads/departments 0750 ${user} ${group} - -" "d ${cfg.dataDir}/uploads/locations 0750 ${user} ${group} - -" "d ${cfg.dataDir}/uploads/manufacturers 0750 ${user} ${group} - -" "d ${cfg.dataDir}/uploads/models 0750 ${user} ${group} - -" "d ${cfg.dataDir}/uploads/suppliers 0750 ${user} ${group} - -" "d ${cfg.dataDir}/storage 0700 ${user} ${group} - -" "d ${cfg.dataDir}/storage/app 0700 ${user} ${group} - -" "d ${cfg.dataDir}/storage/app/backups 0700 ${user} ${group} - -" "d ${cfg.dataDir}/storage/app/backups/env-backups 0700 ${user} ${group} - -" "d ${cfg.dataDir}/storage/debugbar 0700 ${user} ${group} - -" "d ${cfg.dataDir}/storage/framework 0700 ${user} ${group} - -" "d ${cfg.dataDir}/storage/framework/cache 0700 ${user} ${group} - -" "d ${cfg.dataDir}/storage/framework/sessions 0700 ${user} ${group} - -" "d ${cfg.dataDir}/storage/framework/views 0700 ${user} ${group} - -" "d ${cfg.dataDir}/storage/logs 0700 ${user} ${group} - -" "d ${cfg.dataDir}/storage/private_uploads 0700 ${user} ${group} - -" "d ${cfg.dataDir}/storage/private_uploads/assetmodels 0700 ${user} ${group} - -" "d ${cfg.dataDir}/storage/private_uploads/assets 0700 ${user} ${group} - -" "d ${cfg.dataDir}/storage/private_uploads/audits 0700 ${user} ${group} - -" "d ${cfg.dataDir}/storage/private_uploads/imports 0700 ${user} ${group} - -" "d ${cfg.dataDir}/storage/private_uploads/licenses 0700 ${user} ${group} - -" "d ${cfg.dataDir}/storage/private_uploads/signatures 0700 ${user} ${group} - -" "d ${cfg.dataDir}/storage/private_uploads/users 0700 ${user} ${group} - -" ] ++ optionals useSqlite [ "f ${cfg.dataDir}/database.sqlite 0600 ${user} ${group} -" ]; systemd.services.snipe-it-setup = { description = "Preparation tasks for Snipe-IT"; before = [ "phpfpm-snipe-it.service" ]; after = optional useMysql "mysql.service"; wantedBy = [ "multi-user.target" ]; serviceConfig = { Type = "oneshot"; User = user; WorkingDirectory = "${snipe-it}"; }; script = '' # create .env file cat > ${cfg.dataDir}/.env << EOF APP_KEY="base64:$(head -n1 ${cfg.appKeyFile})" APP_URL="http${optionalString useSSL "s"}://${cfg.hostName}" APP_LOG=syslog MAX_RESULTS=${toString cfg.maxResults} '' + optionalString useSSL '' ENABLE_HSTS=true SECURE_COOKIES=true '' + '' SESSION_DRIVER=file SESSION_LIFETIME=${toString cfg.session.lifeTime} EXPIRE_ON_CLOSE=${boolToString cfg.session.expireOnClose} ENCRYPT=${boolToString cfg.session.encrypt} DB_CONNECTION=${db.type} '' + optionalString (db.type != "sqlite") '' DB_HOST=${db.host} DB_PORT=${toString db.port} DB_PASSWORD="$(head -n1 ${db.passwordFile})" DB_USERNAME=${db.username} DB_DATABASE=${db.name} '' + '' MAIL_DRIVER=smtp MAIL_HOST=${mail.host} MAIL_PORT=${toString mail.port} ${optionalString (mail.encryption != null) "MAIL_ENCRYPTION=${mail.encryption}"} MAIL_FROM_ADDR=${mail.fromAddress} MAIL_FROM_NAME=${mail.fromName} MAIL_AUTO_EMBED=${boolToString mail.autoEmbed.enable} MAIL_AUTO_EMBED_METHOD=${mail.autoEmbed.method} '' + optionalString (mail.username != null) '' MAIL_USERNAME=${mail.username} MAIL_PASSWORD="$(head -n1 ${mail.passwordFile})" '' + optionalString (mail.replytoAddress != null) '' MAIL_REPLYTO_ADDR=${mail.replytoAddress} MAIL_REPLYTO_NAME=${mail.replytoName} '' + optionalString cfg.memcached.enable '' CACHE_DRIVER=memcached MEMCACHED_HOST=${cfg.memcached.host} MEMCACHED_PORT=${toString cfg.memcached.port} '' + '' ${optionalString (cfg.extraConfig != null) cfg.extraConfig} EOF chmod 600 ${cfg.dataDir}/.env # re-evaluate configuration ${phpPackage}/bin/php artisan config:clear ${phpPackage}/bin/php artisan config:cache # migrate db ${phpPackage}/bin/php artisan migrate --force # create caches ${phpPackage}/bin/php artisan event:cache ${phpPackage}/bin/php artisan view:cache ''; }; users = { users."${user}" = { isSystemUser = true; home = cfg.dataDir; group = group; }; groups."${group}" = {}; users."${config.services.nginx.user}".extraGroups = [ group ]; }; }; }