# Copyright Tom Hubrecht, (2023) # SPDX-FileCopyrightText: 2024 Tom Hubrecht # # SPDX-License-Identifier: EUPL-1.2 { config, lib, pkgs, ... }: let inherit (lib) mkDefault mkEnableOption mkIf mkOption optional optionalString types ; cfg = config.services.demarches-simplifiees; settingsFormat = pkgs.formats.keyValue { }; env = settingsFormat.generate "ds-fr-env" cfg.settings; ds-fr = pkgs.writeShellScriptBin "ds-fr" '' set -a cd ${cfg.package} ${optionalString (cfg.secretFile != null) "source ${cfg.secretFile}"} source ${env} BIN="$1" shift SUDO="exec" if [[ $USER != ${cfg.user} ]]; then SUDO='exec /run/wrappers/bin/sudo -u ${cfg.user} --preserve-env' fi $SUDO ${cfg.package}/bin/$BIN "$@" ''; in { options.services.demarches-simplifiees = { enable = mkEnableOption "demarches-simplifiees."; package = mkOption { type = types.package; default = pkgs.callPackage ./package { inherit (cfg) initialDeploymentDate dataDir logDir; }; }; user = mkOption { type = types.str; default = "ds-fr"; description = "User account under which DS runs."; }; group = mkOption { type = types.str; default = "ds-fr"; description = "Group account under which DS runs."; }; dataDir = mkOption { type = types.str; default = "/var/lib/ds-fr"; }; logDir = mkOption { type = types.str; default = "/var/log/ds-fr"; }; secretFile = mkOption { type = types.nullOr types.path; default = null; }; settings = mkOption { inherit (settingsFormat) type; }; initialDeploymentDate = mkOption { type = types.nullOr types.str; default = null; }; }; config = mkIf cfg.enable { environment.systemPackages = [ ds-fr ]; systemd.tmpfiles.rules = [ "f '${cfg.logDir}/production.log' 0640 ${cfg.user} ${cfg.group} - -" "f '${cfg.dataDir}/.env' 0600 ${cfg.user} ${cfg.group} - -" "d '${cfg.dataDir}/tmp' 0700 ${cfg.user} ${cfg.group} 10d -" "d '${cfg.dataDir}/storage' 0700 ${cfg.user} ${cfg.group} - -" ]; systemd.services = { ds-fr-setup = { description = "Demarches Simplifiees setup"; wantedBy = [ "multi-user.target" ]; path = [ pkgs.bash ds-fr ]; after = [ "postgresql.service" ]; serviceConfig = { Type = "oneshot"; User = cfg.user; Group = cfg.group; EnvironmentFile = [ env ] ++ (optional (cfg.secretFile != null) cfg.secretFile); StateDirectory = mkIf (cfg.dataDir == "/var/lib/ds-fr") "ds-fr"; LogsDirectory = mkIf (cfg.logDir == "/var/log/ds-fr") "ds-fr"; }; script = '' [[ ! -f ${cfg.dataDir}/.initial-migration ]] \ && ds-fr rails db:environment:set \ && ds-fr rails db:schema:load \ && ds-fr rails db:seed \ && touch ${cfg.dataDir}/.initial-migration ds-fr rake db:migrate ds-fr rake after_party:run ''; }; ds-fr-work = { description = "Demarches Simplifiees work service"; wantedBy = [ "multi-user.target" "ds-fr.service" ]; after = [ "network.target" "ds-fr-setup.service" ]; requires = [ "ds-fr-setup.service" ]; serviceConfig = { ExecStart = "${ds-fr}/bin/ds-fr rails jobs:work"; EnvironmentFile = [ env ] ++ (optional (cfg.secretFile != null) cfg.secretFile); User = cfg.user; Group = cfg.group; StateDirectory = mkIf (cfg.dataDir == "/var/lib/ds-fr") "ds-fr"; LogsDirectory = mkIf (cfg.logDir == "/var/log/ds-fr") "ds-fr"; }; }; ds-fr = { description = "Demarches Simplifiees web service"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" "ds-fr-setup.service" ]; requires = [ "ds-fr-setup.service" ]; path = [ pkgs.imagemagick ]; serviceConfig = { ExecStart = "${ds-fr}/bin/ds-fr rails server"; Environment = [ "RAILS_QUEUE_ADAPTER=delayed_job" ]; EnvironmentFile = [ env ] ++ (optional (cfg.secretFile != null) cfg.secretFile); User = cfg.user; Group = cfg.group; StateDirectory = mkIf (cfg.dataDir == "/var/lib/ds-fr") "ds-fr"; LogsDirectory = mkIf (cfg.logDir == "/var/log/ds-fr") "ds-fr"; }; }; }; services = { demarches-simplifiees.settings = (builtins.mapAttrs (_: mkDefault) { RAILS_ENV = "production"; RAILS_ROOT = builtins.toString cfg.package; # Application host name # # Examples: # * For local development: localhost:3000 # * For preproduction: staging.ds.example.org # * For production: ds.example.org APP_HOST = "localhost:3000"; # Rails key for signing sensitive data # See https://guides.rubyonrails.org/security.html # # For production you MUST generate a new key, and keep it secret. # Secrets must be long and random. Use bin/rails secret to get new unique secrets. # Secret key for One-Time-Password codes, used for 2-factors authentication # OTP_SECRET_KEY = ""; # Protect access to the instance with a static login/password (useful for staging environments) BASIC_AUTH_ENABLED = "disabled"; BASIC_AUTH_USERNAME = ""; BASIC_AUTH_PASSWORD = ""; # ActiveStorage service to use for attached files. # Possible values: # - "local": store files on the local filesystem # - "amazon": store files remotely on an S3 storage service # - "openstack": store files remotely on an OpenStack storage service # # (See config/storage.yml for the configuration of each service.) ACTIVE_STORAGE_SERVICE = "local"; # Configuration for the OpenStack storage service (if enabled) FOG_OPENSTACK_API_KEY = ""; FOG_OPENSTACK_USERNAME = ""; FOG_OPENSTACK_URL = ""; FOG_OPENSTACK_REGION = ""; DS_PROXY_URL = ""; # SAML SAML_IDP_ENABLED = "disabled"; # External service: authentication through France Connect FC_PARTICULIER_ID = ""; FC_PARTICULIER_SECRET = ""; FC_PARTICULIER_BASE_URL = ""; # External service: authentication through Agent Connect AGENT_CONNECT_ID = ""; AGENT_CONNECT_SECRET = ""; AGENT_CONNECT_BASE_URL = ""; AGENT_CONNECT_JWKS = ""; AGENT_CONNECT_REDIRECT = ""; # External service: integration with HelpScout (optional) HELPSCOUT_MAILBOX_ID = ""; HELPSCOUT_CLIENT_ID = ""; HELPSCOUT_CLIENT_SECRET = ""; HELPSCOUT_WEBHOOK_SECRET = ""; # External service: external supervision SENTRY_ENABLED = "disabled"; SENTRY_CURRENT_ENV = "development"; SENTRY_DSN_RAILS = ""; SENTRY_DSN_JS = ""; # External service: Matomo web analytics MATOMO_ENABLED = "disabled"; MATOMO_COOKIE_DOMAIN = "*.www.demarches-simplifiees.fr"; MATOMO_DOMAIN = "*.www.demarches-simplifiees.fr"; MATOMO_ID = ""; MATOMO_HOST = "matomo.example.org"; # Default SMTP Provider: Mailjet MAILJET_API_KEY = ""; MAILJET_SECRET_KEY = ""; # Alternate SMTP Provider: SendInBlue/DoList SENDINBLUE_CLIENT_KEY = ""; SENDINBLUE_SMTP_KEY = ""; SENDINBLUE_USER_NAME = ""; # SENDINBLUE_LOGIN_URL="https://app.sendinblue.com/account/saml/login/truc" # Alternate SMTP Provider: Mailtrap (mail catcher for staging environments) # When enabled, all emails will be sent using this provider MAILTRAP_ENABLED = "disabled"; MAILTRAP_USERNAME = ""; MAILTRAP_PASSWORD = ""; # Alternative SMTP Provider: Mailcatcher (Catches mail and serves it through a dream.) # When enabled, all emails will be sent using this provider MAILCATCHER_ENABLED = "disabled"; MAILCATCHER_HOST = ""; MAILCATCHER_PORT = ""; # External service: live chat for admins (specific to démarches-simplifiées.fr) CRISP_ENABLED = "disabled"; CRISP_CLIENT_KEY = ""; # API Entreprise credentials # https://api.gouv.fr/api/api-entreprise.html API_ENTREPRISE_KEY = ""; # External service: CRM for following admin accounts pipeline (specific to démarches-simplifiées.fr) PIPEDRIVE_KEY = ""; # Networks bypassing the email login token that verifies new devices, and rack-attack throttling TRUSTED_NETWORKS = ""; # External service: mesuring performance of the Rails app (specific to démarches-simplifiées.fr) SKYLIGHT_AUTHENTICATION_KEY = ""; # "sXaot-fKhBlkI8qaSirQyuZbrpv5sVFoOturQ0pFEh0"; # Enable or disable Lograge logs LOGRAGE_ENABLED = "disabled"; # Logs source for Lograge # # Examples: # * For local development: tps_local # * For preproduction: tps_staging # * For production: tps_prod LOGRAGE_SOURCE = "tps_prod"; # External service: timestamping a daily archive of dossiers status changes UNIVERSIGN_API_URL = "https://ws.universign.eu/tsa/post/"; UNIVERSIGN_USERPWD = ""; # External service: API Geo / Adresse API_ADRESSE_URL = "https://api-adresse.data.gouv.fr"; API_GEO_URL = "https://geo.api.gouv.fr"; # External service: API Education API_EDUCATION_URL = "https://data.education.gouv.fr/api/records/1.0"; # Encryption key for sensitive columns in the database ENCRYPTION_SERVICE_SALT = ""; # ActiveRecord encryption keys. Generate them with bin/rails db:encryption:init (you can omit deterministic_key) AR_ENCRYPTION_PRIMARY_KEY = ""; AR_ENCRYPTION_KEY_DERIVATION_SALT = ""; # Salt for invisible_captcha session data. # Must be the same value for all app instances behind a load-balancer. INVISIBLE_CAPTCHA_SECRET = "kikooloool"; # Clamav antivirus usage CLAMAV_ENABLED = "disabled"; # Siret number used for API Entreprise, by default we use SIRET from dinum API_ENTREPRISE_DEFAULT_SIRET = "put_your_own_siret"; }) // { # Database credentials DB_DATABASE = "ds-fr"; DB_USERNAME = cfg.user; DB_PASSWORD = ""; DB_HOST = "/run/postgresql"; DB_POOL = ""; # Log on stdout RAILS_LOG_TO_STDOUT = true; }; postgresql = { enable = true; ensureDatabases = [ "ds-fr" ]; ensureUsers = optional (cfg.user == "ds-fr") { name = "ds-fr"; ensureDBOwnership = true; }; extraPlugins = with config.services.postgresql.package.pkgs; [ postgis ]; }; nginx = { enable = true; virtualHosts.${cfg.settings.APP_HOST} = { enableACME = true; forceSSL = true; root = "${cfg.package}/public/"; locations."/".tryFiles = "$uri @proxy"; locations."@proxy" = { proxyPass = "http://127.0.0.1:3000"; }; }; }; }; users.users = mkIf (cfg.user == "ds-fr") { ds-fr = { inherit (cfg) group; isSystemUser = true; home = cfg.package; }; }; users.groups.${cfg.group} = { }; }; }