This repository has no description
4.8 kB
159 lines
1{
2 config,
3 lib,
4 pkgs,
5 self,
6 ...
7}:
8let
9 # lass: personal calendars; tools/book reads free/busy from them and writes
10 # booking requests into lass/bookings.
11 users = [
12 "opencrow"
13 "lass"
14 ];
15
16 # Generate one var per user with random password
17 userGenerators = lib.listToAttrs (
18 map (user: {
19 name = "radicale-${user}";
20 value = {
21 files."htpasswd-line" = { };
22 files."password" = { };
23 runtimeInputs = with pkgs; [
24 apacheHttpd
25 coreutils
26 ];
27 script = ''
28 password=$(head -c 32 /dev/urandom | base64 | tr -dc 'a-zA-Z0-9' | head -c 24)
29 echo "$password" > "$out/password"
30 htpasswd -nbB ${user} "$password" > "$out/htpasswd-line"
31 '';
32 };
33 }) users
34 );
35
36 loadCredentials = map (
37 user:
38 "radicale-${user}-htpasswd:${
39 config.clan.core.vars.generators."radicale-${user}".files."htpasswd-line".path
40 }"
41 ) users;
42
43 assembleHtpasswd = pkgs.writeShellScript "radicale-htpasswd" (
44 lib.concatMapStringsSep "\n" (user: "cat \${CREDENTIALS_DIRECTORY}/radicale-${user}-htpasswd") users
45 );
46
47 # Calino (static, local-first CalDAV web client) on its own origin; accounts
48 # and settings stay in the browser, so nothing is stored server-side. Log in
49 # with server https://cal.lassul.us/ and a radicale user.
50 webClient = "calendar.lassul.us";
51 calino = self.packages.${pkgs.stdenv.hostPlatform.system}.calino;
52
53 proxyHeaders = ''
54 proxy_set_header X-Script-Name /;
55 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
56 proxy_set_header Host $host;
57 '';
58
59 # CORS for the web client; see the maps in appendHttpConfig below.
60 cors = ''
61 add_header Access-Control-Allow-Origin $radicale_cors_origin always;
62 add_header Access-Control-Allow-Methods "GET, PUT, DELETE, PROPFIND, PROPPATCH, REPORT, OPTIONS, MKCOL, MKCALENDAR, MOVE" always;
63 add_header Access-Control-Allow-Headers "Authorization, Content-Type, Depth, Prefer, If-Match, If-None-Match" always;
64 add_header Access-Control-Expose-Headers "ETag, Location, DAV, Allow, X-Sync-Token" always;
65 add_header Access-Control-Max-Age 86400 always;
66 add_header Vary Origin always;
67 if ($radicale_cors_preflight) {
68 return 204;
69 }
70 '';
71in
72{
73 services.radicale = {
74 enable = true;
75 settings = {
76 server = {
77 hosts = [ "127.0.0.1:5232" ];
78 };
79 auth = {
80 type = "htpasswd";
81 htpasswd_filename = "/run/radicale/htpasswd";
82 htpasswd_encryption = "bcrypt";
83 };
84 storage = {
85 filesystem_folder = "/var/lib/radicale/collections";
86 };
87 web = {
88 type = "internal";
89 };
90 };
91 };
92
93 systemd.services.radicale.serviceConfig = {
94 RuntimeDirectory = "radicale";
95 LoadCredential = loadCredentials;
96 };
97 systemd.services.radicale.preStart = lib.mkBefore ''
98 ${assembleHtpasswd} > /run/radicale/htpasswd
99 '';
100
101 clan.core.vars.generators = userGenerators;
102
103 # Let the web client's origin talk CalDAV to radicale. Preflights carry no
104 # credentials and are answered here; a plain DAV OPTIONS (no
105 # Access-Control-Request-Method) still reaches radicale.
106 services.nginx.appendHttpConfig = ''
107 map $http_origin $radicale_cors_origin {
108 default "";
109 "https://${webClient}" $http_origin;
110 }
111 map "$request_method:$http_access_control_request_method" $radicale_cors_preflight {
112 default 0;
113 "~^OPTIONS:." 1;
114 }
115 # Top-level navigations send no Origin; the web client's GET / probes
116 # (CalDAV discovery via /.well-known/caldav) do and must reach radicale.
117 map "$request_method:$http_origin" $radicale_root_redirect {
118 default 0;
119 "GET:" 1;
120 }
121 '';
122
123 services.nginx.virtualHosts."cal.lassul.us" = {
124 forceSSL = true;
125 enableACME = true;
126 # Browsers opening the bare domain get the web client; CalDAV discovery
127 # (PROPFIND /, or the web client's cross-origin GET /) still reaches
128 # radicale.
129 locations."= /" = {
130 proxyPass = "http://127.0.0.1:5232";
131 extraConfig =
132 cors
133 + ''
134 if ($radicale_root_redirect) {
135 return 302 https://${webClient}/;
136 }
137 ''
138 + proxyHeaders;
139 };
140 locations."/" = {
141 proxyPass = "http://127.0.0.1:5232";
142 extraConfig = cors + proxyHeaders;
143 };
144 };
145
146 services.nginx.virtualHosts.${webClient} = {
147 forceSSL = true;
148 enableACME = true;
149 root = calino;
150 locations."/".extraConfig = ''
151 try_files $uri /index.html;
152 add_header Cache-Control no-cache;
153 '';
154 # Vite puts content-hashed bundles here.
155 locations."/assets/".extraConfig = ''
156 add_header Cache-Control "public, max-age=31536000, immutable";
157 '';
158 };
159}