1{
2 lib,
3 pkgs,
4 config,
5 ...
6}:
7let
8 cfg = config.services.wakapi;
9
10 settingsFormat = pkgs.formats.yaml { };
11 settingsFile = settingsFormat.generate "wakapi-settings" cfg.settings;
12
13 inherit (lib)
14 getExe
15 mkOption
16 mkEnableOption
17 mkPackageOption
18 types
19 mkIf
20 optional
21 mkMerge
22 singleton
23 ;
24in
25{
26 options.services.wakapi = {
27 enable = mkEnableOption "Wakapi";
28 package = mkPackageOption pkgs "wakapi" { };
29 stateDir = mkOption {
30 type = types.path;
31 default = "/var/lib/wakapi";
32 description = ''
33 The state directory where data is stored. Will also be used as the
34 working directory for the wakapi service.
35 '';
36 };
37
38 settings = mkOption {
39 inherit (settingsFormat) type;
40 default = { };
41 description = ''
42 Settings for Wakapi.
43
44 See [config.default.yml](https://github.com/muety/wakapi/blob/master/config.default.yml) for a list of all possible options.
45 '';
46 };
47
48 passwordSalt = mkOption {
49 type = types.nullOr types.str;
50 default = null;
51 description = ''
52 The password salt to use for Wakapi.
53 '';
54 };
55 passwordSaltFile = mkOption {
56 type = types.nullOr types.path;
57 default = null;
58 description = ''
59 The path to a file containing the password salt to use for Wakapi.
60 '';
61 };
62
63 smtpPassword = mkOption {
64 type = types.nullOr types.str;
65 default = null;
66 description = ''
67 The password used for the smtp mailed to used by Wakapi.
68 '';
69 };
70 smtpPasswordFile = mkOption {
71 type = types.nullOr types.path;
72 default = null;
73 description = ''
74 The path to a file containing the password for the smtp mailer used by Wakapi.
75 '';
76 };
77
78 database = {
79 createLocally = mkEnableOption ''
80 automatic database configuration.
81
82 ::: {.note}
83 Only PostgreSQL is supported for the time being.
84 :::
85 '';
86
87 dialect = mkOption {
88 type = types.nullOr (
89 types.enum [
90 "postgres"
91 "sqlite3"
92 "mysql"
93 "cockroach"
94 "mssql"
95 ]
96 );
97 default = cfg.settings.db.dialect or null; # handle case where dialect is not set
98 defaultText = ''
99 Database dialect from settings if {option}`services.wakatime.settings.db.dialect`
100 is set, or `null` otherwise.
101 '';
102 description = ''
103 The database type to use for Wakapi.
104 '';
105 };
106
107 name = mkOption {
108 type = types.str;
109 default = cfg.settings.db.name or "wakapi";
110 defaultText = ''
111 Database name from settings if {option}`services.wakatime.settings.db.name`
112 is set, or "wakapi" otherwise.
113 '';
114 description = ''
115 The name of the database to use for Wakapi.
116 '';
117 };
118
119 user = mkOption {
120 type = types.str;
121 default = cfg.settings.db.user or "wakapi";
122 defaultText = ''
123 User from settings if {option}`services.wakatime.settings.db.user`
124 is set, or "wakapi" otherwise.
125 '';
126 description = ''
127 The name of the user to use for Wakapi.
128 '';
129 };
130 };
131 };
132
133 config = mkIf cfg.enable {
134 systemd.services.wakapi = {
135 description = "Wakapi (self-hosted WakaTime-compatible backend)";
136 wants = [
137 "network-online.target"
138 ] ++ optional (cfg.database.dialect == "postgres") "postgresql.service";
139 after = [
140 "network-online.target"
141 ] ++ optional (cfg.database.dialect == "postgres") "postgresql.service";
142 wantedBy = [ "multi-user.target" ];
143
144 script = ''
145 exec ${getExe cfg.package} -config ${settingsFile}
146 '';
147
148 serviceConfig = {
149 Environment = mkMerge [
150 (mkIf (cfg.passwordSalt != null) "WAKAPI_PASSWORD_SALT=${cfg.passwordSalt}")
151 (mkIf (cfg.smtpPassword != null) "WAKAPI_MAIL_SMTP_PASS=${cfg.smtpPassword}")
152 ];
153
154 EnvironmentFile =
155 (lib.optional (cfg.passwordSaltFile != null) cfg.passwordSaltFile)
156 ++ (lib.optional (cfg.smtpPasswordFile != null) cfg.smtpPasswordFile);
157
158 User = config.users.users.wakapi.name;
159 Group = config.users.users.wakapi.group;
160
161 DynamicUser = true;
162 PrivateTmp = true;
163 PrivateUsers = true;
164 PrivateDevices = true;
165 ProtectHome = true;
166 ProtectHostname = true;
167 ProtectClock = true;
168 ProtectKernelLogs = true;
169 ProtectKernelModules = true;
170 ProtectKernelTunables = true;
171 ProtectControlGroups = true;
172 NoNewPrivileges = true;
173 ProtectProc = "invisible";
174 ProtectSystem = "full";
175 RestrictAddressFamilies = [
176 "AF_INET"
177 "AF_INET6"
178 "AF_UNIX"
179 ];
180 CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";
181 RestrictNamespaces = true;
182 RestrictRealtime = true;
183 RestrictSUIDSGID = true;
184 WorkingDirectory = cfg.stateDir;
185 RuntimeDirectory = "wakapi";
186 StateDirectory = "wakapi";
187 StateDirectoryMode = "0700";
188 Restart = "always";
189 };
190 };
191
192 services.wakapi.settings = {
193 env = lib.mkDefault "production";
194 };
195
196 assertions = [
197 {
198 assertion = cfg.passwordSalt != null || cfg.passwordSaltFile != null;
199 message = "Either `services.wakapi.passwordSalt` or `services.wakapi.passwordSaltFile` must be set.";
200 }
201 {
202 assertion = !(cfg.passwordSalt != null && cfg.passwordSaltFile != null);
203 message = "Both `services.wakapi.passwordSalt` and `services.wakapi.passwordSaltFile` should not be set at the same time.";
204 }
205 {
206 assertion = !(cfg.smtpPassword != null && cfg.smtpPasswordFile != null);
207 message = "Both `services.wakapi.smtpPassword` and `services.wakapi.smtpPasswordFile` should not be set at the same time.";
208 }
209 {
210 assertion = cfg.database.createLocally -> cfg.settings.db.dialect != null;
211 message = "`services.wakapi.database.createLocally` is true, but a database dialect is not set!";
212 }
213 ];
214
215 warnings = [
216 (lib.optionalString (cfg.database.createLocally && cfg.settings.db.dialect != "postgres") ''
217 You have enabled automatic database configuration, but the database dialect is not set to "posgres".
218
219 The Wakapi module only supports PostgreSQL. Please set `services.wakapi.database.createLocally`
220 to `false`, or switch to "postgres" as your database dialect.
221 '')
222 ];
223
224 users = {
225 users.wakapi = {
226 group = "wakapi";
227 createHome = false;
228 isSystemUser = true;
229 };
230 groups.wakapi = { };
231 };
232
233 services.postgresql = mkIf (cfg.database.createLocally && cfg.database.dialect == "postgres") {
234 enable = true;
235
236 ensureDatabases = singleton cfg.database.name;
237 ensureUsers = singleton {
238 name = cfg.settings.db.user;
239 ensureDBOwnership = true;
240 };
241
242 authentication = ''
243 host ${cfg.settings.db.name} ${cfg.settings.db.user} 127.0.0.1/32 trust
244 '';
245 };
246 };
247
248 meta.maintainers = with lib.maintainers; [
249 isabelroses
250 NotAShelf
251 ];
252}