simple strings server for the wentworth coding club / my personal use
at main 223 lines 7.4 kB view raw
1{ 2 description = "strings - minimal pastebin"; 3 4 inputs = { 5 nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 flake-utils.url = "github:numtide/flake-utils"; 7 }; 8 9 outputs = { self, nixpkgs, flake-utils }: 10 flake-utils.lib.eachDefaultSystem (system: 11 let 12 pkgs = nixpkgs.legacyPackages.${system}; 13 14 # Pre-fetch node_modules as a fixed-output derivation 15 nodeModules = pkgs.stdenv.mkDerivation { 16 name = "strings-node-modules"; 17 src = ./.; 18 19 nativeBuildInputs = [ pkgs.bun pkgs.cacert ]; 20 21 # Fixed-output derivation - allows network access but requires hash 22 outputHashMode = "recursive"; 23 outputHashAlgo = "sha256"; 24 outputHash = "sha256-40uExawvAkHS9Sz8KRo6tpbKXNkkxbzBvdCkULruYqM="; 25 26 buildPhase = '' 27 runHook preBuild 28 export HOME=$(mktemp -d) 29 export SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt 30 bun install --frozen-lockfile 31 runHook postBuild 32 ''; 33 34 installPhase = '' 35 runHook preInstall 36 mkdir -p $out 37 cp -r node_modules $out/ 38 runHook postInstall 39 ''; 40 }; 41 42 strings = pkgs.stdenv.mkDerivation { 43 pname = "strings"; 44 version = "0.1.0"; 45 46 src = ./.; 47 48 nativeBuildInputs = [ pkgs.makeWrapper ]; 49 50 # No build phase needed - deps are pre-fetched 51 dontBuild = true; 52 53 installPhase = '' 54 runHook preInstall 55 56 mkdir -p $out/lib/strings $out/bin 57 cp -r src package.json $out/lib/strings/ 58 ln -s ${nodeModules}/node_modules $out/lib/strings/node_modules 59 60 makeWrapper ${pkgs.bun}/bin/bun $out/bin/strings \ 61 --add-flags "run $out/lib/strings/src/index.ts" 62 63 runHook postInstall 64 ''; 65 66 meta = with pkgs.lib; { 67 description = "Minimal pastebin service"; 68 license = licenses.mit; 69 }; 70 }; 71 in 72 { 73 packages.default = strings; 74 packages.strings = strings; 75 76 packages.cli = pkgs.writeShellScriptBin "strings-cli" (builtins.readFile ./cli/strings); 77 78 devShells.default = pkgs.mkShell { 79 buildInputs = [ pkgs.bun ]; 80 }; 81 } 82 ) // { 83 nixosModules.default = { config, lib, pkgs, ... }: 84 let 85 cfg = config.services.strings; 86 87 instanceOptions = { name, ... }: { 88 options = { 89 enable = lib.mkEnableOption "strings pastebin instance"; 90 91 package = lib.mkOption { 92 type = lib.types.package; 93 default = self.packages.${pkgs.system}.default; 94 description = "The strings package to use"; 95 }; 96 97 port = lib.mkOption { 98 type = lib.types.port; 99 default = 3000; 100 description = "Port to listen on"; 101 }; 102 103 username = lib.mkOption { 104 type = lib.types.str; 105 default = "admin"; 106 description = "Username for basic auth"; 107 }; 108 109 password = lib.mkOption { 110 type = lib.types.nullOr lib.types.str; 111 default = null; 112 description = "Password for basic auth (not recommended, use passwordFile)"; 113 }; 114 115 passwordFile = lib.mkOption { 116 type = lib.types.nullOr lib.types.path; 117 default = null; 118 description = "File containing AUTH_PASSWORD=<password>"; 119 }; 120 121 baseUrl = lib.mkOption { 122 type = lib.types.str; 123 description = "Public URL for the service (e.g., https://paste.example.com)"; 124 }; 125 126 dataDir = lib.mkOption { 127 type = lib.types.path; 128 default = "/var/lib/strings-${name}"; 129 description = "Directory to store the database"; 130 }; 131 }; 132 }; 133 134 enabledInstances = lib.filterAttrs (_: inst: inst.enable) cfg.instances; 135 in 136 { 137 options.services.strings = { 138 instances = lib.mkOption { 139 type = lib.types.attrsOf (lib.types.submodule instanceOptions); 140 default = { }; 141 description = "Strings pastebin instances"; 142 example = lib.literalExpression '' 143 { 144 main = { 145 enable = true; 146 baseUrl = "https://paste.example.com"; 147 port = 3000; 148 username = "admin"; 149 passwordFile = config.age.secrets.strings-main.path; 150 }; 151 secondary = { 152 enable = true; 153 baseUrl = "https://paste2.example.com"; 154 port = 3001; 155 username = "user"; 156 passwordFile = config.age.secrets.strings-secondary.path; 157 }; 158 } 159 ''; 160 }; 161 }; 162 163 config = lib.mkIf (enabledInstances != { }) { 164 assertions = lib.mapAttrsToList (name: inst: { 165 assertion = inst.password != null || inst.passwordFile != null; 166 message = "services.strings.instances.${name}: either password or passwordFile must be set"; 167 }) enabledInstances; 168 169 users.users = lib.mapAttrs' (name: inst: { 170 name = "strings-${name}"; 171 value = { 172 isSystemUser = true; 173 group = "strings-${name}"; 174 home = inst.dataDir; 175 createHome = true; 176 }; 177 }) enabledInstances; 178 179 users.groups = lib.mapAttrs' (name: _: { 180 name = "strings-${name}"; 181 value = { }; 182 }) enabledInstances; 183 184 systemd.services = lib.mapAttrs' (name: inst: { 185 name = "strings-${name}"; 186 value = { 187 description = "strings pastebin (${name})"; 188 after = [ "network.target" ]; 189 wantedBy = [ "multi-user.target" ]; 190 191 serviceConfig = { 192 Type = "simple"; 193 User = "strings-${name}"; 194 Group = "strings-${name}"; 195 WorkingDirectory = inst.dataDir; 196 ExecStart = "${inst.package}/bin/strings"; 197 Restart = "on-failure"; 198 RestartSec = 5; 199 200 # Hardening 201 NoNewPrivileges = true; 202 PrivateTmp = true; 203 ProtectSystem = "strict"; 204 ProtectHome = true; 205 ReadWritePaths = [ inst.dataDir ]; 206 } // lib.optionalAttrs (inst.passwordFile != null) { 207 EnvironmentFile = inst.passwordFile; 208 }; 209 210 environment = { 211 PORT = toString inst.port; 212 BASE_URL = inst.baseUrl; 213 DB_PATH = "${inst.dataDir}/strings.db"; 214 AUTH_USERNAME = inst.username; 215 } // lib.optionalAttrs (inst.password != null) { 216 AUTH_PASSWORD = inst.password; 217 }; 218 }; 219 }) enabledInstances; 220 }; 221 }; 222 }; 223}