simple strings server for the wentworth coding club / my personal use
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}