1{
2 pkgs,
3 config,
4 lib,
5 ...
6}: let
7 inherit
8 (lib)
9 boolToString
10 concatMapAttrs
11 concatStrings
12 isBool
13 mapAttrsToList
14 mkEnableOption
15 mkIf
16 mkOption
17 mkPackageOption
18 optionalAttrs
19 types
20 mkDefault
21 ;
22 cfg = config.services.your_spotify;
23
24 configEnv = concatMapAttrs (name: value:
25 optionalAttrs (value != null) {
26 ${name} =
27 if isBool value
28 then boolToString value
29 else toString value;
30 })
31 cfg.settings;
32
33 configFile = pkgs.writeText "your_spotify.env" (concatStrings (mapAttrsToList (name: value: "${name}=${value}\n") configEnv));
34in {
35 options.services.your_spotify = let
36 inherit (types) nullOr port str path package;
37 in {
38 enable = mkEnableOption "your_spotify";
39
40 enableLocalDB = mkEnableOption "a local mongodb instance";
41 nginxVirtualHost = mkOption {
42 type = nullOr str;
43 default = null;
44 description = ''
45 If set creates an nginx virtual host for the client.
46 In most cases this should be the CLIENT_ENDPOINT without
47 protocol prefix.
48 '';
49 };
50
51 package = mkPackageOption pkgs "your_spotify" {};
52
53 clientPackage = mkOption {
54 type = package;
55 description = "Client package to use.";
56 };
57
58 spotifySecretFile = mkOption {
59 type = path;
60 description = ''
61 A file containing the secret key of your Spotify application.
62 Refer to: [Creating the Spotify Application](https://github.com/Yooooomi/your_spotify#creating-the-spotify-application).
63 '';
64 };
65
66 settings = mkOption {
67 description = ''
68 Your Spotify Configuration. Refer to [Your Spotify](https://github.com/Yooooomi/your_spotify) for definitions and values.
69 '';
70 example = lib.literalExpression ''
71 {
72 CLIENT_ENDPOINT = "https://example.com";
73 API_ENDPOINT = "https://api.example.com";
74 SPOTIFY_PUBLIC = "spotify_client_id";
75 }
76 '';
77 type = types.submodule {
78 freeformType = types.attrsOf types.str;
79 options = {
80 CLIENT_ENDPOINT = mkOption {
81 type = str;
82 description = ''
83 The endpoint of your web application.
84 Has to include a protocol Prefix (e.g. `http://`)
85 '';
86 example = "https://your_spotify.example.org";
87 };
88 API_ENDPOINT = mkOption {
89 type = str;
90 description = ''
91 The endpoint of your server
92 This api has to be reachable from the device you use the website from not from the server.
93 This means that for example you may need two nginx virtual hosts if you want to expose this on the
94 internet.
95 Has to include a protocol Prefix (e.g. `http://`)
96 '';
97 example = "https://localhost:3000";
98 };
99 SPOTIFY_PUBLIC = mkOption {
100 type = str;
101 description = ''
102 The public client ID of your Spotify application.
103 Refer to: [Creating the Spotify Application](https://github.com/Yooooomi/your_spotify#creating-the-spotify-application)
104 '';
105 };
106 MONGO_ENDPOINT = mkOption {
107 type = str;
108 description = ''The endpoint of the Mongo database.'';
109 default = "mongodb://localhost:27017/your_spotify";
110 };
111 PORT = mkOption {
112 type = port;
113 description = "The port of the api server";
114 default = 3000;
115 };
116 };
117 };
118 };
119 };
120
121 config = mkIf cfg.enable {
122 services.your_spotify.clientPackage = mkDefault (cfg.package.client.override {apiEndpoint = cfg.settings.API_ENDPOINT;});
123 systemd.services.your_spotify = {
124 after = ["network.target"];
125 script = ''
126 export SPOTIFY_SECRET=$(< "$CREDENTIALS_DIRECTORY/SPOTIFY_SECRET")
127 ${lib.getExe' cfg.package "your_spotify_migrate"}
128 exec ${lib.getExe cfg.package}
129 '';
130 serviceConfig = {
131 User = "your_spotify";
132 Group = "your_spotify";
133 DynamicUser = true;
134 EnvironmentFile = [configFile];
135 StateDirectory = "your_spotify";
136 LimitNOFILE = "1048576";
137 PrivateTmp = true;
138 PrivateDevices = true;
139 StateDirectoryMode = "0700";
140 Restart = "always";
141
142 LoadCredential = ["SPOTIFY_SECRET:${cfg.spotifySecretFile}"];
143
144 # Hardening
145 CapabilityBoundingSet = "";
146 LockPersonality = true;
147 #MemoryDenyWriteExecute = true; # Leads to coredump because V8 does JIT
148 PrivateUsers = true;
149 ProtectClock = true;
150 ProtectControlGroups = true;
151 ProtectHome = true;
152 ProtectHostname = true;
153 ProtectKernelLogs = true;
154 ProtectKernelModules = true;
155 ProtectKernelTunables = true;
156 ProtectProc = "invisible";
157 ProcSubset = "pid";
158 ProtectSystem = "strict";
159 RestrictAddressFamilies = [
160 "AF_INET"
161 "AF_INET6"
162 "AF_NETLINK"
163 ];
164 RestrictNamespaces = true;
165 RestrictRealtime = true;
166 SystemCallArchitectures = "native";
167 SystemCallFilter = [
168 "@system-service"
169 "@pkey"
170 ];
171 UMask = "0077";
172 };
173 wantedBy = ["multi-user.target"];
174 };
175 services.nginx = mkIf (cfg.nginxVirtualHost != null) {
176 enable = true;
177 virtualHosts.${cfg.nginxVirtualHost} = {
178 root = cfg.clientPackage;
179 locations."/".extraConfig = ''
180 add_header Content-Security-Policy "frame-ancestors 'none';" ;
181 add_header X-Content-Type-Options "nosniff" ;
182 try_files = $uri $uri/ /index.html ;
183 '';
184 };
185 };
186 services.mongodb = mkIf cfg.enableLocalDB {
187 enable = true;
188 };
189 };
190 meta.maintainers = with lib.maintainers; [patrickdag];
191}