|
|
@@ -0,0 +1,142 @@
|
|
|
+{ config
|
|
|
+, lib
|
|
|
+, pkgs
|
|
|
+, ...
|
|
|
+}:
|
|
|
+let
|
|
|
+ cfg = config.services.grist;
|
|
|
+in
|
|
|
+{
|
|
|
+ options.services.grist = {
|
|
|
+ enable = lib.mkEnableOption "Grist Core self-hosted spreadsheet server";
|
|
|
+
|
|
|
+ package = lib.mkPackageOption pkgs "grist-core" { };
|
|
|
+
|
|
|
+ port = lib.mkOption {
|
|
|
+ type = lib.types.port;
|
|
|
+ default = 8484;
|
|
|
+ description = "Port on which Grist listens.";
|
|
|
+ };
|
|
|
+
|
|
|
+ dataDir = lib.mkOption {
|
|
|
+ type = lib.types.path;
|
|
|
+ default = "/var/lib/grist";
|
|
|
+ description = "Directory for persistent data (documents, home database, etc.). Equivalent to /persist in Docker.";
|
|
|
+ };
|
|
|
+
|
|
|
+ sandboxFlavor = lib.mkOption {
|
|
|
+ type = lib.types.enum [ "unsandboxed" "gvisor" "pyodide" "macSandboxExec" ];
|
|
|
+ default = "unsandboxed";
|
|
|
+ description = ''
|
|
|
+ Sandbox flavor for executing Python formulas.
|
|
|
+ - unsandboxed: No sandbox (fastest, least secure)
|
|
|
+ - gvisor: gVisor sandbox (Linux only, most secure)
|
|
|
+ - pyodide: WebAssembly sandbox (cross-platform, experimental)
|
|
|
+ - macSandboxExec: macOS native sandbox
|
|
|
+ '';
|
|
|
+ };
|
|
|
+
|
|
|
+ singleOrg = lib.mkOption {
|
|
|
+ type = lib.types.nullOr lib.types.str;
|
|
|
+ default = null;
|
|
|
+ example = "mycompany";
|
|
|
+ description = "If set, pins Grist to a single organization (simplifies UI, common for self-hosted).";
|
|
|
+ };
|
|
|
+
|
|
|
+ defaultEmail = lib.mkOption {
|
|
|
+ type = lib.types.str;
|
|
|
+ default = "[email protected]";
|
|
|
+ description = "Default admin email used for initial login.";
|
|
|
+ };
|
|
|
+
|
|
|
+ host = lib.mkOption {
|
|
|
+ type = lib.types.str;
|
|
|
+ default = "0.0.0.0";
|
|
|
+ description = "Host interface to bind (0.0.0.0 for all interfaces).";
|
|
|
+ };
|
|
|
+
|
|
|
+ orgInPath = lib.mkOption {
|
|
|
+ type = lib.types.bool;
|
|
|
+ default = true;
|
|
|
+ description = "Include organization in URL path (Docker default).";
|
|
|
+ };
|
|
|
+
|
|
|
+ serveSameOrigin = lib.mkOption {
|
|
|
+ type = lib.types.bool;
|
|
|
+ default = true;
|
|
|
+ description = "Serve home and docs on the same origin/port (Docker default).";
|
|
|
+ };
|
|
|
+
|
|
|
+ openFirewall = lib.mkOption {
|
|
|
+ type = lib.types.bool;
|
|
|
+ default = false;
|
|
|
+ description = "Open the Grist port in the firewall.";
|
|
|
+ };
|
|
|
+
|
|
|
+ environment = lib.mkOption {
|
|
|
+ type = lib.types.attrsOf lib.types.str;
|
|
|
+ default = { };
|
|
|
+ description = "Additional environment variables to pass to Grist.";
|
|
|
+ };
|
|
|
+
|
|
|
+ environmentFile = lib.mkOption {
|
|
|
+ type = lib.types.nullOr lib.types.path;
|
|
|
+ default = null;
|
|
|
+ description = "File containing environment variables (e.g., for secrets like GRIST_SESSION_SECRET).";
|
|
|
+ };
|
|
|
+ };
|
|
|
+
|
|
|
+ config = lib.mkIf cfg.enable {
|
|
|
+ systemd.services.grist = {
|
|
|
+ description = "Grist Core Spreadsheet Server";
|
|
|
+ wantedBy = [ "multi-user.target" ];
|
|
|
+ after = [ "network.target" ];
|
|
|
+
|
|
|
+ environment = {
|
|
|
+ PORT = toString cfg.port;
|
|
|
+ GRIST_HOST = cfg.host;
|
|
|
+ GRIST_SINGLE_PORT = if cfg.serveSameOrigin then "true" else "false";
|
|
|
+ GRIST_ORG_IN_PATH = if cfg.orgInPath then "true" else "false";
|
|
|
+ GRIST_DATA_DIR = "${cfg.dataDir}/docs";
|
|
|
+ GRIST_INST_DIR = cfg.dataDir;
|
|
|
+ GRIST_SESSION_COOKIE = "grist_core";
|
|
|
+ GRIST_SANDBOX_FLAVOR = cfg.sandboxFlavor;
|
|
|
+ NODE_OPTIONS = "--no-deprecation";
|
|
|
+ NODE_ENV = "production";
|
|
|
+ TYPEORM_DATABASE = "${cfg.dataDir}/home.sqlite3";
|
|
|
+ } // lib.optionalAttrs (cfg.singleOrg != null) {
|
|
|
+ GRIST_SINGLE_ORG = cfg.singleOrg;
|
|
|
+ GRIST_DEFAULT_EMAIL = cfg.defaultEmail;
|
|
|
+ } // cfg.environment;
|
|
|
+
|
|
|
+ serviceConfig = {
|
|
|
+ ExecStart = "${lib.getExe cfg.package}";
|
|
|
+ DynamicUser = true;
|
|
|
+ StateDirectory = "grist";
|
|
|
+ StateDirectoryMode = "0700";
|
|
|
+ WorkingDirectory = cfg.dataDir;
|
|
|
+ EnvironmentFile = lib.optionalString (cfg.environmentFile != null) cfg.environmentFile;
|
|
|
+ Restart = "always";
|
|
|
+ # Hardening
|
|
|
+ ProtectSystem = "strict";
|
|
|
+ ProtectHome = true;
|
|
|
+ PrivateTmp = true;
|
|
|
+ NoNewPrivileges = true;
|
|
|
+ RestrictSUIDSGID = true;
|
|
|
+ };
|
|
|
+
|
|
|
+ preStart = ''
|
|
|
+ # Ensure data directories exist with correct ownership
|
|
|
+ mkdir -p ${cfg.dataDir}/docs
|
|
|
+ chown -R $USER:$GROUP ${cfg.dataDir}
|
|
|
+ '';
|
|
|
+ };
|
|
|
+
|
|
|
+ networking.firewall = lib.mkIf cfg.openFirewall {
|
|
|
+ allowedTCPPorts = [ cfg.port ];
|
|
|
+ };
|
|
|
+
|
|
|
+ # Optional: expose the package for passthru.tests or manual use
|
|
|
+ environment.systemPackages = [ cfg.package ];
|
|
|
+ };
|
|
|
+}
|