# SPDX-FileCopyrightText: 2023-2024 Tom Hubrecht # # SPDX-License-Identifier: EUPL-1.2 { config, lib, pkgs, utils, ... }: let inherit (lib) getExe getExe' mapAttrs mkDefault mkEnableOption mkIf mkOption mkPackageOption optional ; inherit (lib.types) attrsOf nullOr oneOf package path port str ; inherit (utils) escapeSystemdExecArgs; cfg = config.services.demarches-simplifiees; weasyprintEnv = pkgs.python3.withPackages (ps: [ ps.flask ps.sentry-sdk ps.weasyprint ]); in { options.services.demarches-simplifiees = { enable = mkEnableOption "Démarches Simplifiées"; package = mkPackageOption pkgs "demarches-simplifiees" { }; finalPackage = mkOption { type = package; default = cfg.package.override { inherit (cfg) initialDeploymentDate; }; }; host = mkOption { type = str; description = '' Hostname of the web server. ''; }; port = mkOption { type = port; default = 3000; description = '' Listening port for the web server. ''; }; weasyprintPort = mkOption { type = port; default = 5000; description = '' Port of the weasyprint server. ''; }; environment = mkOption { type = attrsOf ( nullOr (oneOf [ package path str ]) ); description = '' Evironment variables available to Démarches Simplifiées. ''; }; environmentFile = mkOption { type = nullOr path; default = null; description = '' Path to a file containing environment variables. Required secrets are `SECRET_KEY_BASE` and `OTP_SECRET_KEY`, which can be generated using `rails secret`. ''; }; initialDeploymentDate = mkOption { type = nullOr str; default = null; description = '' Initial deployment date, used to ignore some migrations, which are known to be buggy and are supposed to change old production data. ''; }; interactScript = mkOption { type = package; default = pkgs.writeShellApplication { name = "ds-fr"; runtimeInputs = [ cfg.finalPackage config.systemd.package pkgs.util-linux ]; text = '' MainPID=$(systemctl show -p MainPID --value demarches-simplifiees.service) nsenter -e -a -w -t "$MainPID" -G follow -S follow "$@" ''; }; description = '' Script to run ds-fr tasks. ''; }; }; config = mkIf cfg.enable { environment.systemPackages = [ cfg.interactScript ]; systemd.services = let serviceConfig = { User = "ds-fr"; DynamicUser = true; EnvironmentFile = optional (cfg.environmentFile != null) cfg.environmentFile; CacheDirectory = "demarches-simplifiees"; LogsDirectory = "demarches-simplifiees"; RuntimeDirectory = "demarches-simplifiees"; StateDirectory = "demarches-simplifiees"; WorkingDirectory = cfg.finalPackage; }; in { demarches-simplifiees = { description = "Démarches Simplifiées"; inherit (cfg) environment; path = [ cfg.finalPackage pkgs.imagemagick ]; after = [ "network.target" "postgresql.target" ]; wantedBy = [ "multi-user.target" ]; preStart = '' mkdir -p "$STATE_DIRECTORY/storage" if [[ ! -f "$STATE_DIRECTORY/.version" ]]; then # Run initial setup rails db:environment:set rails db:schema:load rails db:seed rails jobs:schedule touch "$STATE_DIRECTORY/.version" fi if [[ $(cat "$STATE_DIRECTORY/.version") != "$__DS_VERSION" ]]; then # Run migrations on version change rake db:migrate rake after_party:run echo "$__DS_VERSION" > "$STATE_DIRECTORY/.version" fi ''; serviceConfig = serviceConfig // { ExecStart = escapeSystemdExecArgs [ (getExe' cfg.finalPackage "rails") "server" "-b" "127.0.0.1" "-p" cfg.port ]; }; }; demarches-simplifiees-work = { description = "Démarches Simplifiées work service"; inherit (cfg) environment; after = [ "demarches-simplifiees.service" ]; wantedBy = [ "multi-user.target" ]; bindsTo = [ "demarches-simplifiees.service" ]; partOf = [ "demarches-simplifiees.service" ]; serviceConfig = serviceConfig // { ExecStart = escapeSystemdExecArgs [ (getExe' cfg.finalPackage "rails") "jobs:work" ]; }; }; weasyprint-server = { description = "Weasyprint server"; wantedBy = [ "multi-user.target" ]; environment = { BASE_URL = "https://${cfg.host}"; LOG_DIR = "/var/log/weasyprint"; UWSGI_PYTHONPATH = weasyprintEnv; UWSGI_MODULE = "wgsi:app"; }; serviceConfig = { DynamicUser = true; Type = "notify"; WorkingDirectory = cfg.finalPackage.weasyprint_server; LogsDirectory = "weasyprint"; ExecStart = escapeSystemdExecArgs [ (getExe (pkgs.uwsgi.override { plugins = [ "python3" ]; })) "--http-socket" "127.0.0.1:${builtins.toString cfg.weasyprintPort}" "--processes=4" "--enable-threads" ]; NotifyAccess = "all"; KillSignal = "SIGQUIT"; ExecReload = "${getExe' pkgs.coreutils "kill"} -HUP $MainPID"; ExecStop = "${getExe' pkgs.coreutils "kill"} -INT $MainPID"; ProtectSystem = "full"; ProtectHome = true; NoNewPrivileges = true; PrivateDevices = true; }; }; }; services = { demarches-simplifiees.environment = # Hardcoded values { # Application host name # # Examples: # * For local development: localhost:3000 # * For preproduction: staging.ds.example.org # * For production: ds.example.org APP_HOST = cfg.host; # Database credentials DB_DATABASE = "ds-fr"; DB_USERNAME = "ds-fr"; DB_HOST = "/run/postgresql"; DB_PORT = "5432"; # The variables must be present even if empty... DB_PASSWORD = ""; DB_POOL = ""; # Jobs configuration RAILS_QUEUE_ADAPTER = "delayed_job"; # Log on stdout RAILS_LOG_TO_STDOUT = "true"; # Package version __DS_VERSION = cfg.finalPackage.version; # Weasyprint endpoint generating attestations v2 # See https://github.com/demarches-simplifiees/weasyprint_server WEASYPRINT_URL = "http://127.0.0.1:${builtins.toString cfg.weasyprintPort}/pdf"; } // (mapAttrs (_: mkDefault) { RAILS_ENV = "production"; RAILS_ROOT = builtins.toString cfg.finalPackage; # 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: 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 = ""; # 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 = "enabled"; # 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"; # Date from which email validation requires a TLD in email adresses. # This change had been introduced by : cc53946d221d6f64c365ad6c6c4c544802eb94b4 # Records (users, …) created before this date won't be affected. See #9978 # To set a date, we recommend using *the day after* you have deployed this commit, # so existing records won't be invalid. STRICT_EMAIL_VALIDATION_STARTS_ON = "2024-02-19"; }); postgresql = { enable = true; ensureDatabases = [ "ds-fr" ]; ensureUsers = [ { name = "ds-fr"; ensureDBOwnership = true; } ]; extensions = [ config.services.postgresql.package.pkgs.postgis ]; }; nginx = { enable = true; virtualHosts.${cfg.host} = { enableACME = true; forceSSL = true; root = "${cfg.finalPackage}/public/"; locations."/".tryFiles = "$uri @proxy"; locations."@proxy".proxyPass = "http://127.0.0.1:${builtins.toString cfg.port}"; }; }; }; }; }