This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

superconfig / configs / radicale.nix
5.9 kB 185 lines
1{ 2 config, 3 lib, 4 pkgs, 5 self, 6 ... 7}: 8let 9 # The one and only calendar, lass/calendar: own events (phone via DAVx⁵, 10 # calendar.lassul.us), booking requests and bookings mirrored by tools/book 11 # (configs/book.nix), and whatever Hermes adds. The rights below make it the 12 # only writable collection: nobody can create, delete or replace calendars. 13 calendar = "lass/calendar"; 14 15 # lass: phone, web client and tools/book. 16 # hermes: the agent on coaxmetal (machines/coaxmetal/hermes.nix). Its var is 17 # shared (and declared there too) so coaxmetal's hermes-env can read the 18 # password without radicale running there. 19 users = [ 20 "lass" 21 "hermes" 22 ]; 23 sharedUsers = [ "hermes" ]; 24 25 # One var per user with a random password. 26 userGenerators = lib.listToAttrs ( 27 map (user: { 28 name = "radicale-${user}"; 29 value = import ./radicale-user-generator.nix pkgs user // { 30 share = lib.elem user sharedUsers; 31 }; 32 }) users 33 ); 34 35 loadCredentials = map ( 36 user: 37 "radicale-${user}-htpasswd:${ 38 config.clan.core.vars.generators."radicale-${user}".files."htpasswd-line".path 39 }" 40 ) users; 41 42 assembleHtpasswd = pkgs.writeShellScript "radicale-htpasswd" ( 43 lib.concatMapStringsSep "\n" (user: "cat \${CREDENTIALS_DIRECTORY}/radicale-${user}-htpasswd") users 44 ); 45 46 # Calino (static, local-first CalDAV web client) on its own origin; accounts 47 # and settings stay in the browser, so nothing is stored server-side. Log in 48 # with server https://cal.lassul.us/ and a radicale user. 49 webClient = "calendar.lassul.us"; 50 calino = self.packages.${pkgs.stdenv.hostPlatform.system}.calino; 51 52 proxyHeaders = '' 53 proxy_set_header X-Script-Name /; 54 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 55 proxy_set_header Host $host; 56 ''; 57 58 # CORS for the web client; see the maps in appendHttpConfig below. 59 cors = '' 60 add_header Access-Control-Allow-Origin $radicale_cors_origin always; 61 add_header Access-Control-Allow-Methods "GET, PUT, DELETE, PROPFIND, PROPPATCH, REPORT, OPTIONS, MKCOL, MKCALENDAR, MOVE" always; 62 add_header Access-Control-Allow-Headers "Authorization, Content-Type, Depth, Prefer, If-Match, If-None-Match" always; 63 add_header Access-Control-Expose-Headers "ETag, Location, DAV, Allow, X-Sync-Token" always; 64 add_header Access-Control-Max-Age 86400 always; 65 add_header Vary Origin always; 66 if ($radicale_cors_preflight) { 67 return 204; 68 } 69 ''; 70in 71{ 72 services.radicale = { 73 enable = true; 74 settings = { 75 server = { 76 hosts = [ "127.0.0.1:5232" ]; 77 }; 78 auth = { 79 type = "htpasswd"; 80 htpasswd_filename = "/run/radicale/htpasswd"; 81 htpasswd_encryption = "bcrypt"; 82 }; 83 storage = { 84 filesystem_folder = "/var/lib/radicale/collections"; 85 }; 86 web = { 87 type = "internal"; 88 }; 89 }; 90 # Whitelist, first match wins (sections are emitted sorted by name, hence 91 # the numeric prefixes); anything unmatched is denied. Every user reads and 92 # writes the events of the one calendar (d/o: but may not delete or 93 # overwrite the collection itself) and may only read the root and lass's 94 # home, which is enough for CalDAV discovery. No W/w anywhere else, so no 95 # MKCALENDAR and no auto-created principals. 96 rights = 97 let 98 anyUser = lib.concatStringsSep "|" users; 99 in 100 { 101 "10-calendar" = { 102 user = anyUser; 103 collection = calendar; 104 permissions = "rwdo"; 105 }; 106 "20-root" = { 107 user = anyUser; 108 collection = ""; 109 permissions = "R"; 110 }; 111 "21-home" = { 112 user = anyUser; 113 collection = lib.head (lib.splitString "/" calendar); 114 permissions = "R"; 115 }; 116 }; 117 }; 118 119 systemd.services.radicale.serviceConfig = { 120 RuntimeDirectory = "radicale"; 121 LoadCredential = loadCredentials; 122 }; 123 systemd.services.radicale.preStart = lib.mkBefore '' 124 ${assembleHtpasswd} > /run/radicale/htpasswd 125 ''; 126 127 clan.core.vars.generators = userGenerators; 128 129 # Let the web client's origin talk CalDAV to radicale. Preflights carry no 130 # credentials and are answered here; a plain DAV OPTIONS (no 131 # Access-Control-Request-Method) still reaches radicale. 132 services.nginx.appendHttpConfig = '' 133 map $http_origin $radicale_cors_origin { 134 default ""; 135 "https://${webClient}" $http_origin; 136 } 137 map "$request_method:$http_access_control_request_method" $radicale_cors_preflight { 138 default 0; 139 "~^OPTIONS:." 1; 140 } 141 # Top-level navigations send no Origin; the web client's GET / probes 142 # (CalDAV discovery via /.well-known/caldav) do and must reach radicale. 143 map "$request_method:$http_origin" $radicale_root_redirect { 144 default 0; 145 "GET:" 1; 146 } 147 ''; 148 149 services.nginx.virtualHosts."cal.lassul.us" = { 150 forceSSL = true; 151 enableACME = true; 152 # Browsers opening the bare domain get the web client; CalDAV discovery 153 # (PROPFIND /, or the web client's cross-origin GET /) still reaches 154 # radicale. 155 locations."= /" = { 156 proxyPass = "http://127.0.0.1:5232"; 157 extraConfig = 158 cors 159 + '' 160 if ($radicale_root_redirect) { 161 return 302 https://${webClient}/; 162 } 163 '' 164 + proxyHeaders; 165 }; 166 locations."/" = { 167 proxyPass = "http://127.0.0.1:5232"; 168 extraConfig = cors + proxyHeaders; 169 }; 170 }; 171 172 services.nginx.virtualHosts.${webClient} = { 173 forceSSL = true; 174 enableACME = true; 175 root = calino; 176 locations."/".extraConfig = '' 177 try_files $uri /index.html; 178 add_header Cache-Control no-cache; 179 ''; 180 # Vite puts content-hashed bundles here. 181 locations."/assets/".extraConfig = '' 182 add_header Cache-Control "public, max-age=31536000, immutable"; 183 ''; 184 }; 185}