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