Your one-stop-cake-shop for everything Freshly Baked has to offer

feat(pm/wiki): factor into ingredient

I want to have a private copy of the wiki on umber and a public copy on
teal. It would be good to share configs between them, so we should use
an ingredient for this

+2
packetmix/systems/default.nix
··· 90 90 ingredients = [ 91 91 "freshlybakedcake" 92 92 "server" 93 + "wiki" 93 94 ]; 94 95 args = { 95 96 system = "x86_64-linux"; ··· 101 102 ingredients = [ 102 103 "freshlybakedcake" 103 104 "server" 105 + "wiki" 104 106 ]; 105 107 args = { 106 108 system = "x86_64-linux";
+6
packetmix/systems/teal/headscale.nix
··· 125 125 type = "A"; 126 126 value = "100.64.0.37"; 127 127 } 128 + { 129 + # wiki.starrysky.fyi -> umber 130 + name = "wiki.starrysky.fyi"; 131 + type = "A"; 132 + value = "100.64.0.48"; 133 + } 128 134 ]; 129 135 nameservers.global = [ 130 136 "1.1.1.1"
+6 -348
packetmix/systems/teal/wiki.nix
··· 1 1 # SPDX-FileCopyrightText: 2025 FreshlyBakedCake 2 + # SPDX-FileCopyrightText: 2026 Collabora Productivity Limited 2 3 # 3 4 # SPDX-License-Identifier: MIT 4 5 5 6 { 6 - project, 7 - system, 8 - config, 9 - pkgs, 10 - lib, 11 - ... 12 - }: 13 - { 14 - clicks.storage.impermanence.persist.directories = [ 15 - { 16 - directory = "/var/lib/mediawiki"; 17 - mode = "0700"; 18 - user = "mediawiki"; 19 - defaultPerms.mode = "0700"; 20 - } 21 - { 22 - directory = "/var/lib/private/opensearch"; 23 - mode = "0700"; 24 - user = "opensearch"; 25 - defaultPerms.mode = "0700"; 26 - } 27 - ]; 28 - 29 - services.mediawiki = { 30 - enable = true; 31 - package = project.inputs.nixos-unstable.result.${system}.mediawiki; # header auth master requires mediawiki unstable - header auth stable is broken on missing Hooks (recently removed in stable MW version) 32 - phpPackage = pkgs.php83.withExtensions ({ enabled, all }: enabled ++ [ all.luasandbox ]); 33 - database.type = "postgres"; 34 - path = [ 35 - pkgs.diffutils 36 - pkgs.imagemagick 37 - pkgs.python3Packages.pygments 38 - ]; 39 - extensions = { 40 - AdvancedSearch = project.inputs.AdvancedSearch.src; 41 - Auth_remoteuser = project.inputs.Auth_remoteuser.src; # header auth 42 - AutoCreateCategoryPages = project.inputs.AutoCreateCategoryPages.src; 43 - Cargo = project.inputs.Cargo.src; # queries and soforth 44 - CategoryTree = null; 45 - CheckUser = null; 46 - Cite = null; 47 - CiteThisPage = null; 48 - CirrusSearch = "${ 49 - pkgs.php.buildComposerProject { 50 - pname = "CirrusSearch"; 51 - version = "0.0.3665"; 52 - src = project.inputs.CirrusSearch.src; 53 - vendorHash = "sha256-MLD/3hvzX1aqR4knajJ1amb6K5SVtxlfy+UZWoSi1Bk="; 54 - composerLock = ./wiki/CirrusSearch.composer.lock; 55 - } 56 - }/share/php/CirrusSearch"; # needed for advancedsearch 57 - CodeEditor = null; 58 - DiscussionTools = null; 59 - Echo = null; 60 - EditNotify = project.inputs.EditNotify.src; 61 - Elastica = "${ 62 - pkgs.php.buildComposerProject { 63 - pname = "Elastica"; 64 - version = "0.0.3665"; 65 - src = project.inputs.Elastica.src; 66 - vendorHash = "sha256-4kp8njLTqPeFCREnGharCB/pmYBnXLJR4TdD6EH6WCI="; 67 - composerLock = ./wiki/Elastica.composer.lock; 68 - } 69 - }/share/php/Elastica"; # needed for cirrussearch 70 - Linter = null; 71 - Math = null; 72 - MobileFrontend = project.inputs.MobileFrontend.src; 73 - NamespacePreload = project.inputs.NamespacePreload.src; 74 - Network = "${ 75 - config.services.phpfpm.pools.mediawiki.phpPackage.buildComposerProject { 76 - pname = "Network"; 77 - version = "0.0.3665"; 78 - src = project.inputs.Network.src; 79 - vendorHash = "sha256-JHa6PW5xO3pcwn/2jbGXM0wGhr6UmtqFdxaGCgpaYb0="; 80 - composerLock = ./wiki/Network.composer.lock; 81 - } 82 - }/share/php/Network"; # for page connection graphs 83 - OpenIDConnect = "${ 84 - pkgs.php.buildComposerProject { 85 - pname = "OpenIDConnect"; 86 - version = "0.0.3665"; 87 - src = project.inputs.OpenIDConnect.src; 88 - vendorHash = "sha256-DjxyOK21tbBEj6hFfhVNDxeNu4a26hvMRHgD/u24ZT0="; 89 - composerLock = ./wiki/OpenIDConnect.composer.lock; 90 - 91 - postInstall = '' 92 - cat sql/postgres/ChangePrimaryKey.sql | sed 's/DROP INDEX "primary"/ALTER TABLE openid_connect DROP CONSTRAINT openid_connect_pkey/' > $out/share/php/OpenIDConnect/sql/postgres/ChangePrimaryKey.sql 93 - ''; 94 - } 95 - }/share/php/OpenIDConnect"; 96 - ParserFunctions = null; 97 - PluggableAuth = project.inputs.PluggableAuth.src; # needed for OIDC 98 - Poem = null; 99 - ReplaceText = null; 100 - Scribunto = null; 101 - SecureLinkFixer = null; 102 - SimpleTooltip = project.inputs.SimpleTooltip.src; 103 - SyntaxHighlight_GeSHi = null; 104 - TemplateData = null; 105 - TemplateStyles = null; 106 - Thanks = null; 107 - UserMerge = project.inputs.UserMerge.src; 108 - VisualEditor = null; 109 - WikiEditor = null; 110 - }; 111 - extraConfig = '' 112 - $wgMaxUploadSize = 1024*1024*1024*8; 113 - $wgGroupPermissions['autoconfirmed']['upload_by_url'] = true; 114 - $wgGroupPermissions['autoconfirmed']['interwiki'] = true; // https://wiki.freshly.space/wiki/Special:Interwiki - edit shortlink prefixes, crazy-strong permission but we trust our friends 115 - $wgAllowCopyUploads = true; 116 - $wgCopyUploadsFromSpecialUpload = true; 117 - 118 - $wgSMTP = [ 119 - 'host' => 'ssl://mail.freshly.space', 120 - 'IDHost' => 'wiki.freshly.space', 121 - 'localhost' => 'wiki.freshly.space', 122 - 'port' => 465, 123 - 'auth' => true, 124 - 'username' => 'automated@freshly.space', 125 - 'password' => trim(file_get_contents('/secrets/mediawiki/mail_password.txt')) 126 - ]; 127 - $wgLocalInterwikis = [ 128 - 'fbc' 129 - ]; 130 - 131 - $wgWhitelistReadRegexp = [ 132 - '/^Main Page$/', 133 - '/^Public:/', 134 - '/^User:/' 135 - ]; 136 - $wgGroupPermissions['*']['read'] = false; 137 - $wgGroupPermissions['*']['edit'] = false; 138 - $wgGroupPermissions['*']['createaccount'] = false; 139 - $wgGroupPermissions['*']['autocreateaccount'] = true; 140 - 141 - $wgGroupPermissions['bureaucrat']['usermerge'] = true; 142 - 143 - $wgAuthRemoteuserUserName = function () { 144 - if (!isset($_SERVER['HTTP_X_WEBAUTH_LOGIN'])) { 145 - return ""; 146 - } 147 - 148 - if ($_SERVER['HTTP_X_WEBAUTH_LOGIN'] === 'hyperneutrino') { 149 - return 'HyperNeutrino'; 150 - } 151 - 152 - return $_SERVER['HTTP_X_WEBAUTH_LOGIN']; 153 - }; 154 - $wgAuthRemoteuserPriority = MediaWiki\Session\SessionInfo::MAX_PRIORITY; 155 - 156 - $wgUseCdn = true; 157 - $wgCdnServersNoPurge = [ 158 - '127.0.0.1' 159 - ]; 160 - $wgUsePrivateIPs = true; 161 - 162 - $wgUseInstantCommons = true; 163 - $wgPingback = false; 164 - 165 - $wgPluggableAuth_Config = [ 166 - 'Freshly Baked Cake Kanidm' => [ 167 - 'plugin' => 'OpenIDConnect', 168 - 'data' => [ 169 - 'providerURL' => 'https://idm.freshly.space/oauth2/openid/mediawiki', 170 - 'clientID' => 'mediawiki', 171 - 'clientsecret' => trim(file_get_contents('/secrets/mediawiki/oidc_client_secret.txt')), 172 - 'codeChallengeMethod' => 'S256' 173 - ] 174 - ] 175 - ]; 176 - 177 - $wgOpenIDConnect_MigrateUsersByUserName = true; 178 - 179 - $wgLogos = [ 180 - 'icon' => '/icon.svg', 181 - 'svg' => '/icon.svg' 182 - ]; 183 - 184 - $wgPygmentizePath = '${pkgs.python3Packages.pygments}/bin/pygmentize'; 185 - 186 - $wgScribuntoDefaultEngine = 'luasandbox'; 187 - 188 - $wgNamespacesWithSubpages[NS_MAIN] = true; 189 - 190 - $wgNamespacePreloadDoExpansion = false; // This can't expand {{PAGENAME}} (or like) correctly, making it very nearly useless 191 - 192 - $wgCirrusSearchServers = [ 193 - [ 194 - "host" => '127.0.0.1', 195 - "port" => 1037 196 - ] 197 - ]; 198 - $wgSearchType = 'CirrusSearch'; 199 - $wgNamespacesToBeSearchedDefault[NS_CATEGORY] = true; 200 - 201 - $wgUrlProtocols[] = "rad:"; 202 - 203 - $wgSVGNativeRendering = true; 204 - 205 - $wgRCWatchCategoryMembership = true; 206 - 207 - $wgCargoPageDataColumns[] = 'creationDate'; 208 - $wgCargoPageDataColumns[] = 'modificationDate'; 209 - $wgCargoPageDataColumns[] = 'creator'; 210 - $wgCargoPageDataColumns[] = 'lastEditor'; 211 - $wgCargoPageDataColumns[] = 'displayTitle'; 212 - $wgCargoPageDataColumns[] = 'categories'; 213 - $wgCargoPageDataColumns[] = 'numRevisions'; 214 - $wgCargoPageDataColumns[] = 'outgoingLinks'; 215 - $wgCargoPageDataColumns[] = 'isRedirect'; 216 - $wgCargoPageDataColumns[] = 'pageNameOrRedirect'; 217 - $wgCargoPageDataColumns[] = 'pageIDOrRedirect'; 218 - 219 - $wgCargoFileDataColumns[] = 'mediaType'; 220 - $wgCargoFileDataColumns[] = 'path'; 221 - $wgCargoFileDataColumns[] = 'lastUploadDate'; 222 - 223 - $wgFixDoubleRedirects = true; 224 - 225 - $wgMFAutodetectMobileView = true; 226 - $wgMFEnableMobilePreferences = true; 227 - wfLoadSkin( 'MinervaNeue' ); 228 - 229 - $wgShowExceptionDetails = true; 230 - $wgDevelopmentWarnings = true; 231 - ''; 232 - webserver = "nginx"; 233 - url = "https://wiki.freshly.space"; 234 - nginx.hostName = "wiki.freshly.space"; 235 - name = "Freshly Wiki"; 236 - database.createLocally = true; 237 - 238 - passwordSender = "wiki@freshly.space"; 239 - 240 - passwordFile = "/secrets/mediawiki/initial_admin_password.txt"; 241 - }; 242 - 243 - systemd.timers.mediawiki-maintenance = { 244 - wantedBy = [ "timers.target" ]; 245 - timerConfig = { 246 - OnUnitActiveSec = "30"; 247 - OnBootSec = "30"; 248 - Persistent = false; 249 - Unit = "mediawiki-maintenance.service"; 250 - }; 251 - }; 252 - 253 - systemd.services.mediawiki-maintenance = { 254 - script = '' 255 - ${config.services.phpfpm.pools.mediawiki.phpPackage}/bin/php ${config.services.mediawiki.finalPackage}/share/mediawiki/maintenance/run.php runJobs --memory-limit 1G --maxtime 30 256 - ''; 257 - serviceConfig = { 258 - Type = "oneshot"; 259 - RemainAfterExit = false; 260 - User = "mediawiki"; 261 - Group = "nginx"; 262 - PrivateTmp = true; 263 - Environment = "MEDIAWIKI_CONFIG=${config.services.phpfpm.pools.mediawiki.phpEnv.MEDIAWIKI_CONFIG}"; 264 - }; 265 - }; 266 - 267 - services.opensearch = { 268 - # needed for cirrussearch 269 - enable = true; 270 - package = project.packages.opensearch.result.${system}; 271 - settings = { 272 - "http.port" = 1037; 273 - "path.data" = "/var/lib/private/opensearch/data"; 274 - "path.logs" = "/var/lib/private/opensearch/logs"; 275 - }; 276 - }; 277 - 278 - services.nginx.enable = true; 279 7 services.headscale.settings.dns.extra_records = [ 280 8 { 281 9 # wiki.freshly.space -> teal ··· 284 12 value = "100.64.0.5"; 285 13 } 286 14 ]; 287 - services.nginx.virtualHosts."wiki.freshly.space" = { 288 - listen = [ 289 - { 290 - addr = "127.0.0.1"; 291 - port = 1036; 292 - } 293 - ]; 294 15 295 - locations = { 296 - "= /" = lib.mkForce { 297 - extraConfig = '' 298 - return 301 https://wiki.freshly.space/wiki/; 299 - ''; # overriding nixpkgs /wiki/ redirect since as our double-proxy makes it redirect to :1036 300 - }; 301 - "= /favicon.ico".alias = ./wiki/favicon.ico; 302 - "= /icon.svg".alias = ./wiki/icon.svg; 303 - }; 304 - 305 - extraConfig = '' 306 - client_max_body_size 1024M; 307 - ''; 308 - }; 309 - services.nginx.virtualHosts."external.wiki.freshly.space" = { 310 - listenAddresses = [ 311 - "0.0.0.0" 312 - "[::0]" 313 - ]; 314 - 315 - serverName = "wiki.freshly.space"; 316 - 317 - addSSL = true; 318 - enableACME = true; 319 - acmeRoot = null; 320 - 321 - locations."/" = { 322 - proxyPass = "http://127.0.0.1:1036"; 323 - recommendedProxySettings = true; 324 - proxyWebsockets = true; 325 - 326 - extraConfig = '' 327 - proxy_set_header X-Webauth-Login ""; 328 - proxy_cache off; 329 - ''; 330 - }; 331 - 332 - extraConfig = '' 333 - client_max_body_size 1024M; 334 - ''; 335 - }; 336 - services.nginx.virtualHosts."internal.wiki.freshly.space" = { 337 - listenAddresses = [ "localhost.tailscale" ]; 338 - 339 - serverName = "wiki.freshly.space"; 340 - 341 - addSSL = true; 342 - enableACME = true; 343 - acmeRoot = null; 344 - 345 - locations."/" = { 346 - proxyPass = "http://127.0.0.1:1036"; 347 - recommendedProxySettings = true; 348 - proxyWebsockets = true; 349 - 350 - extraConfig = '' 351 - proxy_cache off; 352 - ''; 353 - }; 354 - 355 - extraConfig = '' 356 - client_max_body_size 1024M; 357 - ''; 358 - }; 359 - 360 - services.nginx.tailscaleAuth = { 361 - enable = true; 362 - virtualHosts = [ "internal.wiki.freshly.space" ]; 16 + ingredient.wiki.wiki = { 17 + hostname = "wiki.freshly.space"; 18 + email = "wiki@freshly.space"; 19 + enablePublicInternet = true; 20 + enableAutoRegistration = true; 363 21 }; 364 22 }
packetmix/systems/teal/wiki/CirrusSearch.composer.lock packetmix/systems/wiki/wiki/CirrusSearch.composer.lock
packetmix/systems/teal/wiki/CirrusSearch.composer.lock.license packetmix/systems/wiki/wiki/CirrusSearch.composer.lock.license
packetmix/systems/teal/wiki/Elastica.composer.lock packetmix/systems/wiki/wiki/Elastica.composer.lock
packetmix/systems/teal/wiki/Elastica.composer.lock.license packetmix/systems/wiki/wiki/Elastica.composer.lock.license
packetmix/systems/teal/wiki/Network.composer.lock packetmix/systems/wiki/wiki/Network.composer.lock
packetmix/systems/teal/wiki/Network.composer.lock.license packetmix/systems/wiki/wiki/Network.composer.lock.license
packetmix/systems/teal/wiki/OpenIDConnect.composer.lock packetmix/systems/wiki/wiki/OpenIDConnect.composer.lock
packetmix/systems/teal/wiki/OpenIDConnect.composer.lock.license packetmix/systems/wiki/wiki/OpenIDConnect.composer.lock.license
packetmix/systems/teal/wiki/favicon.ico packetmix/systems/wiki/wiki/favicon.ico
packetmix/systems/teal/wiki/favicon.ico.license packetmix/systems/wiki/wiki/favicon.ico.license
packetmix/systems/teal/wiki/icon.svg packetmix/systems/wiki/wiki/icon.svg
packetmix/systems/teal/wiki/icon.svg.license packetmix/systems/wiki/wiki/icon.svg.license
+9
packetmix/systems/umber/postgresql.nix
··· 1 + # SPDX-FileCopyrightText: 2025 FreshlyBakedCake 2 + # 3 + # SPDX-License-Identifier: MIT 4 + 5 + { 6 + clicks.storage.impermanence.persist.directories = [ 7 + "/var/lib/postgresql" 8 + ]; 9 + }
+11
packetmix/systems/umber/wiki.nix
··· 1 + # SPDX-FileCopyrightText: 2026 Collabora Productivity Limited 2 + # 3 + # SPDX-License-Identifier: MIT 4 + 5 + { 6 + ingredient.wiki.wiki = { 7 + name = "Starry Sky Wiki"; 8 + hostname = "wiki.starrysky.fyi"; 9 + email = "wiki@starrysky.fyi"; 10 + }; 11 + }
+407
packetmix/systems/wiki/wiki.nix
··· 1 + # SPDX-FileCopyrightText: 2025 FreshlyBakedCake 2 + # SPDX-FileCopyrightText: 2026 Collabora Productivity Limited 3 + # 4 + # SPDX-License-Identifier: MIT 5 + 6 + # This ingredient has some default values (including OIDC config/etc.) that make it probably unsuitable for use outside of Freshly Baked. Sorry. 7 + { 8 + project, 9 + system, 10 + config, 11 + pkgs, 12 + lib, 13 + ... 14 + }: 15 + { 16 + options.ingredient.wiki.wiki = { 17 + name = lib.mkOption { 18 + type = lib.types.str; 19 + description = "What should your wiki be called"; 20 + default = "Freshly Wiki"; 21 + }; 22 + hostname = lib.mkOption { 23 + type = lib.types.str; 24 + description = "Where your wiki should be hosted"; 25 + }; 26 + email = lib.mkOption { 27 + type = lib.types.str; 28 + description = "What email should notifications/password resets/etc. come from"; 29 + }; 30 + enablePublicInternet = lib.mkEnableOption "Allow access from the public internet with authentication via OIDC"; 31 + enableAutoRegistration = lib.mkEnableOption "Allow unregistered users to automatically register via OIDC or Tailscale"; 32 + }; 33 + 34 + config = { 35 + clicks.storage.impermanence.persist.directories = [ 36 + { 37 + directory = "/var/lib/mediawiki"; 38 + mode = "0700"; 39 + user = "mediawiki"; 40 + defaultPerms.mode = "0700"; 41 + } 42 + { 43 + directory = "/var/lib/private/opensearch"; 44 + mode = "0700"; 45 + user = "opensearch"; 46 + defaultPerms.mode = "0700"; 47 + } 48 + ]; 49 + 50 + services.mediawiki = { 51 + enable = true; 52 + package = project.inputs.nixos-unstable.result.${system}.mediawiki; # header auth master requires mediawiki unstable - header auth stable is broken on missing Hooks (recently removed in stable MW version) 53 + phpPackage = pkgs.php83.withExtensions ({ enabled, all }: enabled ++ [ all.luasandbox ]); 54 + database.type = "postgres"; 55 + path = [ 56 + pkgs.diffutils 57 + pkgs.imagemagick 58 + pkgs.python3Packages.pygments 59 + ]; 60 + extensions = { 61 + AdvancedSearch = project.inputs.AdvancedSearch.src; 62 + Auth_remoteuser = project.inputs.Auth_remoteuser.src; # header auth 63 + AutoCreateCategoryPages = project.inputs.AutoCreateCategoryPages.src; 64 + Cargo = project.inputs.Cargo.src; # queries and soforth 65 + CategoryTree = null; 66 + CheckUser = null; 67 + Cite = null; 68 + CiteThisPage = null; 69 + CirrusSearch = "${ 70 + pkgs.php.buildComposerProject { 71 + pname = "CirrusSearch"; 72 + version = "0.0.3665"; 73 + src = project.inputs.CirrusSearch.src; 74 + vendorHash = "sha256-MLD/3hvzX1aqR4knajJ1amb6K5SVtxlfy+UZWoSi1Bk="; 75 + composerLock = ./wiki/CirrusSearch.composer.lock; 76 + } 77 + }/share/php/CirrusSearch"; # needed for advancedsearch 78 + CodeEditor = null; 79 + DiscussionTools = null; 80 + Echo = null; 81 + EditNotify = project.inputs.EditNotify.src; 82 + Elastica = "${ 83 + pkgs.php.buildComposerProject { 84 + pname = "Elastica"; 85 + version = "0.0.3665"; 86 + src = project.inputs.Elastica.src; 87 + vendorHash = "sha256-4kp8njLTqPeFCREnGharCB/pmYBnXLJR4TdD6EH6WCI="; 88 + composerLock = ./wiki/Elastica.composer.lock; 89 + } 90 + }/share/php/Elastica"; # needed for cirrussearch 91 + Linter = null; 92 + Math = null; 93 + MobileFrontend = project.inputs.MobileFrontend.src; 94 + NamespacePreload = project.inputs.NamespacePreload.src; 95 + Network = "${ 96 + config.services.phpfpm.pools.mediawiki.phpPackage.buildComposerProject { 97 + pname = "Network"; 98 + version = "0.0.3665"; 99 + src = project.inputs.Network.src; 100 + vendorHash = "sha256-JHa6PW5xO3pcwn/2jbGXM0wGhr6UmtqFdxaGCgpaYb0="; 101 + composerLock = ./wiki/Network.composer.lock; 102 + } 103 + }/share/php/Network"; # for page connection graphs 104 + OpenIDConnect = lib.mkIf config.ingredient.wiki.wiki.enablePublicInternet "${ 105 + pkgs.php.buildComposerProject { 106 + pname = "OpenIDConnect"; 107 + version = "0.0.3665"; 108 + src = project.inputs.OpenIDConnect.src; 109 + vendorHash = "sha256-DjxyOK21tbBEj6hFfhVNDxeNu4a26hvMRHgD/u24ZT0="; 110 + composerLock = ./wiki/OpenIDConnect.composer.lock; 111 + 112 + postInstall = '' 113 + cat sql/postgres/ChangePrimaryKey.sql | sed 's/DROP INDEX "primary"/ALTER TABLE openid_connect DROP CONSTRAINT openid_connect_pkey/' > $out/share/php/OpenIDConnect/sql/postgres/ChangePrimaryKey.sql 114 + ''; 115 + } 116 + }/share/php/OpenIDConnect"; 117 + ParserFunctions = null; 118 + PluggableAuth = lib.mkIf config.ingredient.wiki.wiki.enablePublicInternet project.inputs.PluggableAuth.src; # needed for OIDC 119 + Poem = null; 120 + ReplaceText = null; 121 + Scribunto = null; 122 + SecureLinkFixer = null; 123 + SimpleTooltip = project.inputs.SimpleTooltip.src; 124 + SyntaxHighlight_GeSHi = null; 125 + TemplateData = null; 126 + TemplateStyles = null; 127 + Thanks = null; 128 + UserMerge = project.inputs.UserMerge.src; 129 + VisualEditor = null; 130 + WikiEditor = null; 131 + }; 132 + extraConfig = '' 133 + $wgMaxUploadSize = 1024*1024*1024*8; 134 + $wgGroupPermissions['autoconfirmed']['upload_by_url'] = true; 135 + $wgGroupPermissions['autoconfirmed']['interwiki'] = true; // Special:Interwiki - edit shortlink prefixes, crazy-strong permission but we trust our friends 136 + $wgAllowCopyUploads = true; 137 + $wgCopyUploadsFromSpecialUpload = true; 138 + 139 + $wgSMTP = [ 140 + 'host' => 'ssl://mail.freshly.space', 141 + 'IDHost' => '${config.ingredient.wiki.wiki.hostname}', 142 + 'localhost' => '${config.ingredient.wiki.wiki.hostname}', 143 + 'port' => 465, 144 + 'auth' => true, 145 + 'username' => 'automated@freshly.space', 146 + 'password' => trim(file_get_contents('/secrets/mediawiki/mail_password.txt')) 147 + ]; 148 + $wgLocalInterwikis = [ 149 + 'fbc' 150 + ]; 151 + 152 + $wgWhitelistReadRegexp = [ 153 + '/^Main Page$/', 154 + '/^Public:/', 155 + '/^User:/' 156 + ]; 157 + $wgGroupPermissions['*']['read'] = false; 158 + $wgGroupPermissions['*']['edit'] = false; 159 + $wgGroupPermissions['*']['createaccount'] = false; 160 + $wgGroupPermissions['*']['autocreateaccount'] = ${ 161 + if config.ingredient.wiki.wiki.enableAutoRegistration then "true" else "false" 162 + }; 163 + 164 + $wgGroupPermissions['bureaucrat']['usermerge'] = true; 165 + 166 + $wgAuthRemoteuserUserName = function () { 167 + if (!isset($_SERVER['HTTP_X_WEBAUTH_LOGIN'])) { 168 + return ""; 169 + } 170 + 171 + if ($_SERVER['HTTP_X_WEBAUTH_LOGIN'] === 'hyperneutrino') { 172 + return 'HyperNeutrino'; 173 + } 174 + 175 + return $_SERVER['HTTP_X_WEBAUTH_LOGIN']; 176 + }; 177 + $wgAuthRemoteuserPriority = MediaWiki\Session\SessionInfo::MAX_PRIORITY; 178 + 179 + $wgUseCdn = true; 180 + $wgCdnServersNoPurge = [ 181 + '127.0.0.1' 182 + ]; 183 + $wgUsePrivateIPs = true; 184 + 185 + $wgUseInstantCommons = true; 186 + $wgPingback = false; 187 + 188 + ${ 189 + if config.ingredient.wiki.wiki.enablePublicInternet then 190 + '' 191 + $wgPluggableAuth_Config = [ 192 + 'Freshly Baked Cake Kanidm' => [ 193 + 'plugin' => 'OpenIDConnect', 194 + 'data' => [ 195 + 'providerURL' => 'https://idm.freshly.space/oauth2/openid/mediawiki', 196 + 'clientID' => 'mediawiki', 197 + 'clientsecret' => trim(file_get_contents('/secrets/mediawiki/oidc_client_secret.txt')), 198 + 'codeChallengeMethod' => 'S256' 199 + ] 200 + ] 201 + ]; 202 + '' 203 + else 204 + "" 205 + } 206 + 207 + $wgOpenIDConnect_MigrateUsersByUserName = true; 208 + 209 + $wgLogos = [ 210 + 'icon' => '/icon.svg', 211 + 'svg' => '/icon.svg' 212 + ]; 213 + 214 + $wgPygmentizePath = '${pkgs.python3Packages.pygments}/bin/pygmentize'; 215 + 216 + $wgScribuntoDefaultEngine = 'luasandbox'; 217 + 218 + $wgNamespacesWithSubpages[NS_MAIN] = true; 219 + 220 + $wgNamespacePreloadDoExpansion = false; // This can't expand {{PAGENAME}} (or like) correctly, making it very nearly useless 221 + 222 + $wgCirrusSearchServers = [ 223 + [ 224 + "host" => '127.0.0.1', 225 + "port" => 1037 226 + ] 227 + ]; 228 + $wgSearchType = 'CirrusSearch'; 229 + $wgNamespacesToBeSearchedDefault[NS_CATEGORY] = true; 230 + 231 + $wgUrlProtocols[] = "rad:"; 232 + 233 + $wgSVGNativeRendering = true; 234 + 235 + $wgRCWatchCategoryMembership = true; 236 + 237 + $wgCargoPageDataColumns[] = 'creationDate'; 238 + $wgCargoPageDataColumns[] = 'modificationDate'; 239 + $wgCargoPageDataColumns[] = 'creator'; 240 + $wgCargoPageDataColumns[] = 'lastEditor'; 241 + $wgCargoPageDataColumns[] = 'displayTitle'; 242 + $wgCargoPageDataColumns[] = 'categories'; 243 + $wgCargoPageDataColumns[] = 'numRevisions'; 244 + $wgCargoPageDataColumns[] = 'outgoingLinks'; 245 + $wgCargoPageDataColumns[] = 'isRedirect'; 246 + $wgCargoPageDataColumns[] = 'pageNameOrRedirect'; 247 + $wgCargoPageDataColumns[] = 'pageIDOrRedirect'; 248 + 249 + $wgCargoFileDataColumns[] = 'mediaType'; 250 + $wgCargoFileDataColumns[] = 'path'; 251 + $wgCargoFileDataColumns[] = 'lastUploadDate'; 252 + 253 + $wgFixDoubleRedirects = true; 254 + 255 + $wgMFAutodetectMobileView = true; 256 + $wgMFEnableMobilePreferences = true; 257 + wfLoadSkin( 'MinervaNeue' ); 258 + 259 + $wgShowExceptionDetails = true; 260 + $wgDevelopmentWarnings = true; 261 + ''; 262 + webserver = "nginx"; 263 + url = "https://${config.ingredient.wiki.wiki.hostname}"; 264 + nginx.hostName = config.ingredient.wiki.wiki.hostname; 265 + inherit (config.ingredient.wiki.wiki) name; 266 + database = { 267 + passwordFile = builtins.toFile "unused-mediawiki-postgress-password" "userpass"; # This isn't actually needed for running a wiki, but some of the initialization scripts do require it. It's not a real password. 268 + createLocally = false; # We can't use createLocally with passwordFile, which is needed during initialization... we'll ensure the database ourself below :( 269 + 270 + socket = "/run/postgresql"; 271 + }; 272 + 273 + passwordSender = config.ingredient.wiki.wiki.email; 274 + 275 + passwordFile = "/secrets/mediawiki/initial_admin_password.txt"; 276 + }; 277 + 278 + services.postgresql = { 279 + enable = true; 280 + ensureDatabases = [ config.services.mediawiki.database.name ]; 281 + ensureUsers = [ 282 + { 283 + name = config.services.mediawiki.database.user; 284 + ensureDBOwnership = true; 285 + } 286 + ]; 287 + }; 288 + systemd.services.mediawiki-init.after = [ "postgresql.target" ]; 289 + systemd.services.httpd.after = [ "postgresql.target" ]; 290 + 291 + systemd.timers.mediawiki-maintenance = { 292 + wantedBy = [ "timers.target" ]; 293 + timerConfig = { 294 + OnUnitActiveSec = "30"; 295 + OnBootSec = "30"; 296 + Persistent = false; 297 + Unit = "mediawiki-maintenance.service"; 298 + }; 299 + }; 300 + 301 + systemd.services.mediawiki-maintenance = { 302 + script = '' 303 + ${config.services.phpfpm.pools.mediawiki.phpPackage}/bin/php ${config.services.mediawiki.finalPackage}/share/mediawiki/maintenance/run.php runJobs --memory-limit 1G --maxtime 30 304 + ''; 305 + serviceConfig = { 306 + Type = "oneshot"; 307 + RemainAfterExit = false; 308 + User = "mediawiki"; 309 + Group = "nginx"; 310 + PrivateTmp = true; 311 + Environment = "MEDIAWIKI_CONFIG=${config.services.phpfpm.pools.mediawiki.phpEnv.MEDIAWIKI_CONFIG}"; 312 + }; 313 + }; 314 + 315 + services.opensearch = { 316 + # needed for cirrussearch 317 + enable = true; 318 + package = project.packages.opensearch.result.${system}; 319 + settings = { 320 + "http.port" = 1037; 321 + "path.data" = "/var/lib/private/opensearch/data"; 322 + "path.logs" = "/var/lib/private/opensearch/logs"; 323 + }; 324 + }; 325 + 326 + services.nginx.enable = true; 327 + services.nginx.virtualHosts.${config.ingredient.wiki.wiki.hostname} = { 328 + listen = [ 329 + { 330 + addr = "127.0.0.1"; 331 + port = 1036; 332 + } 333 + ]; 334 + 335 + locations = { 336 + "= /" = lib.mkForce { 337 + extraConfig = '' 338 + return 301 https://${config.ingredient.wiki.wiki.hostname}/wiki/; 339 + ''; # overriding nixpkgs /wiki/ redirect since as our double-proxy makes it redirect to :1036 340 + }; 341 + "= /favicon.ico".alias = ./wiki/favicon.ico; 342 + "= /icon.svg".alias = ./wiki/icon.svg; 343 + }; 344 + 345 + extraConfig = '' 346 + client_max_body_size 1024M; 347 + ''; 348 + }; 349 + services.nginx.virtualHosts."external.${config.ingredient.wiki.wiki.hostname}" = 350 + lib.mkIf config.ingredient.wiki.wiki.enablePublicInternet 351 + { 352 + listenAddresses = [ 353 + "0.0.0.0" 354 + "[::0]" 355 + ]; 356 + 357 + serverName = config.ingredient.wiki.wiki.hostname; 358 + 359 + addSSL = true; 360 + enableACME = true; 361 + acmeRoot = null; 362 + 363 + locations."/" = { 364 + proxyPass = "http://127.0.0.1:1036"; 365 + recommendedProxySettings = true; 366 + proxyWebsockets = true; 367 + 368 + extraConfig = '' 369 + proxy_set_header X-Webauth-Login ""; 370 + proxy_cache off; 371 + ''; 372 + }; 373 + 374 + extraConfig = '' 375 + client_max_body_size 1024M; 376 + ''; 377 + }; 378 + services.nginx.virtualHosts."internal.${config.ingredient.wiki.wiki.hostname}" = { 379 + listenAddresses = [ "localhost.tailscale" ]; 380 + 381 + serverName = config.ingredient.wiki.wiki.hostname; 382 + 383 + addSSL = true; 384 + enableACME = true; 385 + acmeRoot = null; 386 + 387 + locations."/" = { 388 + proxyPass = "http://127.0.0.1:1036"; 389 + recommendedProxySettings = true; 390 + proxyWebsockets = true; 391 + 392 + extraConfig = '' 393 + proxy_cache off; 394 + ''; 395 + }; 396 + 397 + extraConfig = '' 398 + client_max_body_size 1024M; 399 + ''; 400 + }; 401 + 402 + services.nginx.tailscaleAuth = { 403 + enable = true; 404 + virtualHosts = [ "internal.${config.ingredient.wiki.wiki.hostname}" ]; 405 + }; 406 + }; 407 + }