{ config, pkgs, lib, ... }: let cfg = config.services.think-backend-greaterchiangmai; think-backend-gtcm = pkgs.think-backend-gtcm.override { dataDir = cfg.dataDir; }; defaultUser = "gtcm"; defaultGroup = "gtcm"; php = pkgs.php83; artisan-be = pkgs.writeScriptBin "gtcm-be" '' #! ${pkgs.runtimeShell} cd ${think-backend-gtcm} sudo() { if [[ "$USER" != ${cfg.user} ]]; then exec /run/wrappers/bin/sudo -u ${cfg.user} "$@" else exec "$@" fi } sudo ${lib.getExe php} artisan "$@" ''; in { options.services.think-backend-greaterchiangmai = { enable = lib.mkEnableOption "To enable think.greaterchiangmai.com"; dataDir = lib.mkOption { type = lib.types.path; default = "/var/lib/think-backend.greaterchiangmai.com"; description = ''A place where to store states''; }; user = lib.mkOption { type = lib.types.str; default = defaultUser; description = "User account under which this runs."; }; group = lib.mkOption { type = lib.types.str; default = defaultGroup; defaultText = "${defaultGroup}"; description = "Group under which the website runs."; }; package = lib.mkPackageOption pkgs "think-backend-gtcm" { }; domain = lib.mkOption { type = lib.types.str; default = "think-backend.greaterchiangmai.com"; example = "forum.example.com"; description = "Domain to serve on."; }; settings = lib.mkOption { type = with lib.types; attrsOf ( nullOr ( either (oneOf [ bool int port path str ]) (submodule { options = { _secret = lib.mkOption { type = nullOr str; description = '' The path to a file containing the value the option should be set to in the final configuration file. ''; }; }; }) ) ); default = { }; description = '' Options for settings environment variables ''; example = lib.literalExpression '' { APP_NAME = "Laravel"; APP_ENV = "local"; APP_KEY = "key"; APP_DEBUG = true; APP_URL = "http://localhost"; LOG_CHANNEL = "stack"; LOG_DEPRECATIONS_CHANNEL = "null"; LOG_LEVEL = "debug"; DB_CONNECTION = "mysql"; DB_HOST = "127.0.0.1"; DB_PORT = "3306"; DB_DATABASE = "laravel"; DB_USERNAME = "root"; DB_PASSWORD = ""; } ''; }; }; config = lib.mkIf cfg.enable { users.users.${cfg.user} = lib.mkForce { isSystemUser = true; home = cfg.dataDir; createHome = true; homeMode = "755"; group = cfg.group; }; users.groups.${cfg.group} = { }; services.phpfpm.pools.think-backend-gtcm = { inherit (cfg) user group; phpPackage = php; settings = { "listen.owner" = config.services.nginx.user; "listen.group" = config.services.nginx.group; "listen.mode" = "0600"; "pm" = lib.mkDefault "dynamic"; "pm.max_children" = lib.mkDefault 10; "pm.max_requests" = lib.mkDefault 500; "pm.start_servers" = lib.mkDefault 2; "pm.min_spare_servers" = lib.mkDefault 1; "pm.max_spare_servers" = lib.mkDefault 3; }; phpOptions = '' error_log = syslog log_errors = on ''; }; environment.systemPackages = [ artisan-be ]; services.think-backend-greaterchiangmai.settings = { APP_SERVICES_CACHE = lib.mkDefault "${cfg.dataDir}/cache/services.php"; APP_PACKAGES_CACHE = lib.mkDefault "${cfg.dataDir}/cache/packages.php"; APP_CONFIG_CACHE = lib.mkDefault "${cfg.dataDir}/cache/config.php"; APP_ROUTES_CACHE = lib.mkDefault "${cfg.dataDir}/cache/routes-v7.php"; APP_EVENTS_CACHE = lib.mkDefault "${cfg.dataDir}/cache/events.php"; }; systemd.services.think-backend-gtcm-setup = { description = "think-backend.greaterchiangmai installation"; requiredBy = [ "phpfpm-think-backend-gtcm.service" ]; before = [ "phpfpm-think-backend-gtcm.service" ]; requires = [ "mysql.service" ]; after = [ "mysql.service" ]; serviceConfig = { type = "oneshot"; RemainAfterExit = true; User = cfg.user; UMask = 77; WorkingDirectory = "${think-backend-gtcm}"; RuntimeDirectory = "think-backend-gtcm/cache"; RuntimeDirectoryMode = 700; }; path = [ pkgs.replace-secret ]; script = let isSecret = v: lib.isAttrs v && v ? _secret && lib.isString v._secret; gtcmEnvVars = lib.generators.toKeyValue { mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" { mkValueString = v: with builtins; if isInt v then toString v else if isString v then v else if true == v then "true" else if false == v then "false" else if isSecret v then hashString "sha256" v._secret else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty { }) v}"; }; }; secretPaths = lib.mapAttrsToList (_: v: v._secret) (lib.filterAttrs (_: isSecret) cfg.settings); mkSecretReplacement = file: '' replace-secret ${ lib.escapeShellArgs [ (builtins.hashString "sha256" file) file "${cfg.dataDir}/.env" ] } ''; secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths; filteredConfig = lib.converge (lib.filterAttrsRecursive ( _: v: !lib.elem v [ { } null ] )) cfg.settings; gtcmEnv = pkgs.writeText "gtcm-be.env" (gtcmEnvVars filteredConfig); in '' # error handling set -euo pipefail # create .env file install -T -m 0600 -o ${cfg.user} ${gtcmEnv} "${cfg.dataDir}/.env" ${secretReplacements} if ! grep 'APP_KEY=base64:' "${cfg.dataDir}/.env" >/dev/null; then sed -i 's/APP_KEY=/APP_KEY=base64:/' "${cfg.dataDir}/.env" fi # migrate & seed db ${lib.getExe php} artisan key:generate --force ${lib.getExe php} artisan migrate --force ${lib.getExe php} artisan config:cache ''; }; systemd.tmpfiles.rules = [ "d ${cfg.dataDir} 0710 ${cfg.user} ${cfg.group} - -" "d ${cfg.dataDir}/cache 0700 ${cfg.user} ${cfg.group} - -" "d ${cfg.dataDir}/public 0750 ${cfg.user} ${cfg.group} - -" "d ${cfg.dataDir}/public/uploads 0750 ${cfg.user} ${cfg.group} - -" "d ${cfg.dataDir}/storage 0700 ${cfg.user} ${cfg.group} - -" "d ${cfg.dataDir}/storage/app 0700 ${cfg.user} ${cfg.group} - -" "d ${cfg.dataDir}/storage/fonts 0700 ${cfg.user} ${cfg.group} - -" "d ${cfg.dataDir}/storage/framework 0700 ${cfg.user} ${cfg.group} - -" "d ${cfg.dataDir}/storage/framework/cache 0700 ${cfg.user} ${cfg.group} - -" "d ${cfg.dataDir}/storage/framework/sessions 0700 ${cfg.user} ${cfg.group} - -" "d ${cfg.dataDir}/storage/framework/views 0700 ${cfg.user} ${cfg.group} - -" "d ${cfg.dataDir}/storage/logs 0700 ${cfg.user} ${cfg.group} - -" "d ${cfg.dataDir}/storage/uploads 0700 ${cfg.user} ${cfg.group} - -" ]; networking.firewall.allowedTCPPorts = [ 80 443 ]; services.nginx = { enable = true; recommendedTlsSettings = true; recommendedOptimisation = true; recommendedGzipSettings = true; recommendedBrotliSettings = true; recommendedProxySettings = true; virtualHosts."${cfg.domain}" = { root = "${think-backend-gtcm}/public"; locations = { "/" = { index = "index.php"; tryFiles = "$uri $uri/ /index.php?$query_string"; }; "~ \\.php$".extraConfig = '' fastcgi_pass unix:${config.services.phpfpm.pools."think-backend-gtcm".socket}; ''; "~ \\.(js|css|gif|png|ico|jpg|jpeg)$" = { extraConfig = "expires 365d;"; }; }; }; }; services.mysql = { enable = true; package = lib.mkForce pkgs.mariadb; ensureDatabases = [ cfg.settings.DB_DATABASE ]; ensureUsers = [ { name = cfg.settings.DB_USERNAME; ensurePermissions = { "${cfg.settings.DB_DATABASE}.*" = "ALL PRIVILEGES"; }; } ]; }; }; }