From 945846e0b56326bc772d99dc69d5e262134af354 Mon Sep 17 00:00:00 2001 From: Alois Date: Sat, 22 Aug 2026 11:11:34 +0200 Subject: [PATCH] Add flake --- LICENSE | 18 ++ README.md | 56 +++- flake.nix | 756 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 829 insertions(+), 1 deletion(-) create mode 100644 LICENSE create mode 100644 flake.nix diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cb45b99 --- /dev/null +++ b/LICENSE @@ -0,0 +1,18 @@ +MIT License + +Copyright (c) 2026 Methanium + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index e1fb7d8..53c61ee 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,56 @@ -# nextcloud +# Nextcloud +Example: + +```nix +{ config, ... }: +{ + sops.secrets = { + nextcloud-jwt = { }; + nextcloud-oidc = { }; + nextcloud-admin-password = { }; + }; + + custom.nextcloud = { + enable = true; + + hostName = "cloud.example.com"; + adminUser = "administrator"; + useAuthentikSSO = true; + + secrets = { + adminPasswordFile = config.sops.secrets.nextcloud-admin-password.path; + jwtFile = config.sops.secrets.nextcloud-jwt.path; + oidcClientSecretFile = config.sops.secrets.nextcloud-oidc.path; + }; + + nginx.useACMEHost = "example.com"; + + anubis.enable = true; + + authentik = { + hostName = "auth.example.com"; + providerName = "My authentik provider"; + blueprint.enable = true; + }; + + addons = { + apps = [ + "mail" + "deck" + ]; + + whiteboard = { + enable = true; + hostName = "whiteboad.example.com"; + disableRecording = true; + }; + + eurooffice = { + enable = true; + hostName = "eurooffice.example.com"; + }; + }; + }; +} +``` diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..55079d1 --- /dev/null +++ b/flake.nix @@ -0,0 +1,756 @@ +{ + description = "Nextcloud"; + + inputs.nixpkgs.url = "nixpkgs/nixos-unstable"; + + outputs = inputs: { + nixosModules.default = + { + config, + lib, + options, + pkgs, + ... + }: + let + inherit (lib) + concatStringsSep + hasAttrByPath + mkEnableOption + mkIf + mkMerge + mkOption + optional + optionalAttrs + setAttrByPath + types + unique + ; + + cfg = config.custom.nextcloud; + + nextcloudUrl = "https://${cfg.hostName}"; + whiteboardUrl = + if cfg.addons.whiteboard.externalUrl != null then + cfg.addons.whiteboard.externalUrl + else + "https://${cfg.addons.whiteboard.hostName}"; + euroofficeUrl = + if cfg.addons.eurooffice.externalUrl != null then + cfg.addons.eurooffice.externalUrl + else + "https://${cfg.addons.eurooffice.hostName}"; + + appSet = cfg.package.packages.apps; + packagedAppNames = unique ( + cfg.addons.apps + ++ optional cfg.useAuthentikSSO "user_oidc" + ++ optional cfg.addons.whiteboard.enable "whiteboard" + ); + packagedApps = lib.genAttrs packagedAppNames ( + name: + if builtins.hasAttr name appSet then + builtins.getAttr name appSet + else + throw "custom.nextcloud: Nextcloud app '${name}' is not packaged for ${ + cfg.package.name or "the selected Nextcloud package" + }" + ); + + euroofficeApp = pkgs.fetchNextcloudApp { + url = cfg.addons.eurooffice.app.url; + hash = cfg.addons.eurooffice.app.hash; + license = cfg.addons.eurooffice.app.license; + }; + + allExtraApps = + packagedApps + // optionalAttrs cfg.addons.eurooffice.enable { + eurooffice = euroofficeApp; + } + // cfg.addons.extraApps; + + nginxTls = { + forceSSL = cfg.nginx.forceSSL; + enableACME = cfg.nginx.enableACME; + useACMEHost = cfg.nginx.useACMEHost; + }; + + publicProxyTarget = + if cfg.anubis.enable then + "http://${cfg.bindAddress}:${toString cfg.anubis.port}" + else + "http://${cfg.bindAddress}:${toString cfg.rawPort}"; + + authentikDiscoveryUrl = + if cfg.authentik.discoveryUrl != null then + cfg.authentik.discoveryUrl + else + "https://${cfg.authentik.hostName}/application/o/${cfg.authentik.applicationSlug}/.well-known/openid-configuration"; + + authentikBlueprint = '' + version: 1 + metadata: + name: Nextcloud + labels: + blueprints.goauthentik.io/instantiate: "true" + + entries: + - model: authentik_core.group + identifiers: + name: ${cfg.authentik.groupName} + attrs: + is_superuser: false + + - model: authentik_providers_oauth2.oauth2provider + identifiers: + name: ${cfg.authentik.providerName} + attrs: + name: ${cfg.authentik.providerName} + client_type: confidential + client_id: ${cfg.authentik.clientId} + client_secret: !Env ${cfg.authentik.blueprint.secretEnvName} + grant_types: + - authorization_code + - refresh_token + authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]] + invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]] + redirect_uris: + - matching_mode: strict + url: ${nextcloudUrl}/apps/user_oidc/code + signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]] + property_mappings: + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]] + - !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]] + + - model: authentik_core.application + identifiers: + slug: ${cfg.authentik.applicationSlug} + attrs: + name: Nextcloud + slug: ${cfg.authentik.applicationSlug} + provider: !Find [authentik_providers_oauth2.oauth2provider, [name, ${cfg.authentik.providerName}]] + + - model: authentik_policies.policybinding + identifiers: + target: !Find [authentik_core.application, [slug, ${cfg.authentik.applicationSlug}]] + group: !Find [authentik_core.group, [name, ${cfg.authentik.groupName}]] + order: 0 + attrs: + enabled: true + ''; + + whiteboardModuleAvailable = hasAttrByPath [ "services" "nextcloud-whiteboard-server" ] options; + anubisModuleAvailable = hasAttrByPath [ "services" "anubis" "instances" ] options; + authentikTargetAvailable = hasAttrByPath cfg.authentik.blueprint.targetOption options; + + whiteboardStart = pkgs.writeShellScript "nextcloud-whiteboard-with-secret" '' + set -euo pipefail + export JWT_SECRET_KEY="$(<"$CREDENTIALS_DIRECTORY/jwt")" + exec ${lib.getExe pkgs.nextcloud-whiteboard-server} + ''; + + configScript = pkgs.writeShellScript "nextcloud-stack-config" '' + set -euo pipefail + + OCC=${lib.escapeShellArg "${config.services.nextcloud.occ}/bin/nextcloud-occ"} + + "$OCC" config:app:set files default_quota --value ${lib.escapeShellArg cfg.defaultQuota} + + ${lib.optionalString cfg.addons.whiteboard.enable '' + jwt_secret="$(<"$CREDENTIALS_DIRECTORY/jwt")" + "$OCC" config:app:set whiteboard collabBackendUrl --value=${lib.escapeShellArg "${whiteboardUrl}/"} + "$OCC" config:app:set whiteboard jwt_secret_key --value="$jwt_secret" + ''} + + ${lib.optionalString cfg.addons.eurooffice.enable '' + jwt_secret="$(<"$CREDENTIALS_DIRECTORY/jwt")" + "$OCC" config:app:set eurooffice StorageUrl --value=${lib.escapeShellArg "${nextcloudUrl}/"} + "$OCC" config:app:set eurooffice DocumentServerUrl --value=${lib.escapeShellArg "${euroofficeUrl}/"} + "$OCC" config:app:set eurooffice jwt_secret --value="$jwt_secret" + "$OCC" config:app:set eurooffice jwt_header --value=${lib.escapeShellArg cfg.addons.eurooffice.jwtHeader} + "$OCC" config:app:set eurooffice editFormats --value=${lib.escapeShellArg cfg.addons.eurooffice.editFormats} + ''} + + ${lib.optionalString cfg.useAuthentikSSO '' + oidc_secret="$(<"$CREDENTIALS_DIRECTORY/oidc")" + "$OCC" user_oidc:provider ${lib.escapeShellArg cfg.authentik.providerName} \ + --clientid=${lib.escapeShellArg cfg.authentik.clientId} \ + --clientsecret="$oidc_secret" \ + --discoveryuri=${lib.escapeShellArg authentikDiscoveryUrl} \ + --unique-uid=${if cfg.authentik.uniqueUid then "1" else "0"} + ''} + + ${concatStringsSep "\n" cfg.addons.occCommands} + ''; + in + { + options.custom.nextcloud = { + enable = mkEnableOption "Nextcloud"; + + hostName = mkOption { + type = types.str; + description = "Public Nextcloud hostname."; + example = "cloud.example.com"; + }; + + bindAddress = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "Loopback address used by internal services."; + }; + + rawPort = mkOption { + type = types.port; + default = 5003; + description = "Port for the raw Nextcloud nginx vhost."; + }; + + package = mkOption { + type = types.package; + default = pkgs.nextcloud34; + defaultText = lib.literalExpression "pkgs.nextcloud34"; + description = "Nextcloud package."; + }; + + adminUser = mkOption { + type = types.nullOr types.str; + default = "admin"; + description = "Initial admin username. Set to null to disable initial admin creation on Nextcloud 32+."; + }; + + maxUploadSize = mkOption { + type = types.str; + default = "1G"; + }; + + defaultQuota = mkOption { + type = types.str; + default = "10 GB"; + }; + + appstoreEnable = mkOption { + type = types.bool; + default = false; + }; + + settings = mkOption { + type = types.attrs; + default = { }; + description = "Extra services.nextcloud.settings values."; + }; + + useAuthentikSSO = mkOption { + type = types.bool; + default = false; + description = "Configure the user_oidc app for Authentik."; + }; + + secrets = { + adminPasswordFile = mkOption { + type = types.nullOr types.str; + default = null; + description = "Runtime file containing the initial Nextcloud admin password."; + }; + + jwtFile = mkOption { + type = types.nullOr types.str; + default = null; + description = "Runtime file containing the shared JWT secret for addons."; + }; + + oidcClientSecretFile = mkOption { + type = types.nullOr types.str; + default = null; + description = "Runtime file containing the Authentik OIDC client secret."; + }; + + nextcloud = mkOption { + type = types.attrsOf types.str; + default = { }; + description = "Extra secret config.php entries passed to services.nextcloud.secrets."; + }; + }; + + database = { + type = mkOption { + type = types.enum [ + "mysql" + "pgsql" + "sqlite" + ]; + default = "mysql"; + }; + + createLocally = mkOption { + type = types.bool; + default = true; + }; + + name = mkOption { + type = types.str; + default = "nextcloud"; + }; + + user = mkOption { + type = types.str; + default = "nextcloud"; + }; + + host = mkOption { + type = types.nullOr types.str; + default = null; + description = "Database host. Leave null when createLocally is true."; + }; + + passwordFile = mkOption { + type = types.nullOr types.str; + default = null; + }; + }; + + nginx = { + forceSSL = mkOption { + type = types.bool; + default = true; + }; + + enableACME = mkOption { + type = types.bool; + default = false; + }; + + useACMEHost = mkOption { + type = types.nullOr types.str; + default = null; + example = "example.com"; + }; + + extraPublicConfig = mkOption { + type = types.lines; + default = ""; + }; + }; + + anubis = { + enable = mkOption { + type = types.bool; + default = false; + }; + + port = mkOption { + type = types.port; + default = 7003; + }; + + settings = mkOption { + type = types.attrs; + default = { }; + }; + }; + + authentik = { + hostName = mkOption { + type = types.str; + default = "auth.example.com"; + }; + + discoveryUrl = mkOption { + type = types.nullOr types.str; + default = null; + }; + + clientId = mkOption { + type = types.str; + default = "nextcloud"; + }; + + providerName = mkOption { + type = types.str; + default = "Authentik"; + }; + + applicationSlug = mkOption { + type = types.str; + default = "nextcloud"; + }; + + groupName = mkOption { + type = types.str; + default = "nextcloud-users"; + }; + + uniqueUid = mkOption { + type = types.bool; + default = false; + }; + + hideLoginForm = mkOption { + type = types.bool; + default = true; + }; + + blueprint = { + enable = mkOption { + type = types.bool; + default = false; + description = "Generate an Authentik blueprint under /etc/nextcloud."; + }; + + autoAttach = mkOption { + type = types.bool; + default = true; + description = "Also attach the blueprint to targetOption when that option exists."; + }; + + targetOption = mkOption { + type = types.listOf types.str; + default = [ + "methanium" + "authentik" + "blueprints" + "nextcloud" + ]; + }; + + secretEnvName = mkOption { + type = types.str; + default = "NEXTCLOUD_OIDC_SECRET"; + }; + }; + }; + + addons = { + apps = mkOption { + type = types.listOf types.str; + default = [ + "mail" + "deck" + ]; + description = "App names from the selected Nextcloud package app set."; + }; + + extraApps = mkOption { + type = types.attrsOf types.package; + default = { }; + description = "Additional app packages merged into services.nextcloud.extraApps."; + }; + + occCommands = mkOption { + type = types.listOf types.lines; + default = [ ]; + description = "Extra idempotent shell commands run after nextcloud-setup."; + }; + + whiteboard = { + enable = mkEnableOption "Nextcloud Whiteboard"; + + hostName = mkOption { + type = types.str; + default = "whiteboard.example.com"; + }; + + externalUrl = mkOption { + type = types.nullOr types.str; + default = null; + description = "External Whiteboard URL. Defaults to https://hostName."; + }; + + port = mkOption { + type = types.port; + default = 5004; + }; + + nginx.enable = mkOption { + type = types.bool; + default = true; + }; + + disableRecording = mkOption { + type = types.bool; + default = false; + description = "Apply the custom DISABLE_RECORDING patch from the original configuration."; + }; + + settings = mkOption { + type = types.attrsOf types.str; + default = { }; + }; + }; + + eurooffice = { + enable = mkEnableOption "EuroOffice document server integration"; + + hostName = mkOption { + type = types.str; + default = "office.example.com"; + }; + + externalUrl = mkOption { + type = types.nullOr types.str; + default = null; + description = "External EuroOffice URL. Defaults to https://hostName."; + }; + + port = mkOption { + type = types.port; + default = 5005; + }; + + nginx.enable = mkOption { + type = types.bool; + default = true; + }; + + image = mkOption { + type = types.str; + default = "ghcr.io/euro-office/documentserver:latest"; + }; + + jwtHeader = mkOption { + type = types.str; + default = "Authorization"; + }; + + editFormats = mkOption { + type = types.str; + default = ''{"docm":true,"docx":true,"dotm":true,"dotx":true,"xlsm":true,"xlsx":true,"pptm":true,"pptx":true,"pdf":true}''; + }; + + environment = mkOption { + type = types.attrsOf types.str; + default = { }; + }; + + app = { + url = mkOption { + type = types.str; + default = "https://github.com/nextcloud-releases/eurooffice/releases/download/v11.0.1/eurooffice-v11.0.1.tar.gz"; + }; + + hash = mkOption { + type = types.str; + default = "sha256-HXpvyCNhlxAvrxSEu6/5u0mpg7TrTsaS2gii9mf74ns="; + }; + + license = mkOption { + type = types.str; + default = "asl20"; + }; + }; + }; + }; + }; + + config = mkIf cfg.enable (mkMerge [ + { + assertions = [ + { + assertion = cfg.adminUser == null || cfg.secrets.adminPasswordFile != null; + message = "custom.nextcloud.secrets.adminPasswordFile is required when adminUser is not null."; + } + { + assertion = + !(cfg.addons.whiteboard.enable || cfg.addons.eurooffice.enable) || cfg.secrets.jwtFile != null; + message = "custom.nextcloud.secrets.jwtFile is required by Whiteboard and EuroOffice."; + } + { + assertion = !cfg.useAuthentikSSO || cfg.secrets.oidcClientSecretFile != null; + message = "custom.nextcloud.secrets.oidcClientSecretFile is required when useAuthentikSSO is true."; + } + { + assertion = !cfg.addons.whiteboard.enable || whiteboardModuleAvailable; + message = "The selected nixpkgs does not provide services.nextcloud-whiteboard-server."; + } + { + assertion = !cfg.anubis.enable || anubisModuleAvailable; + message = "The selected nixpkgs does not provide services.anubis.instances."; + } + { + assertion = !cfg.authentik.blueprint.enable || cfg.useAuthentikSSO; + message = "authentik.blueprint.enable requires useAuthentikSSO = true."; + } + ]; + + services.nextcloud = { + enable = true; + package = cfg.package; + hostName = "raw-" + cfg.hostName; + appstoreEnable = cfg.appstoreEnable; + maxUploadSize = cfg.maxUploadSize; + configureRedis = true; + + database.createLocally = cfg.database.createLocally; + + config = { + adminuser = cfg.adminUser; + adminpassFile = cfg.secrets.adminPasswordFile; + dbtype = cfg.database.type; + dbname = cfg.database.name; + dbuser = cfg.database.user; + } + // optionalAttrs (cfg.database.host != null) { + dbhost = cfg.database.host; + } + // optionalAttrs (cfg.database.passwordFile != null) { + dbpassFile = cfg.database.passwordFile; + }; + + https = true; + + settings = mkMerge [ + { + trusted_domains = [ cfg.hostName ]; + trusted_proxies = unique [ + cfg.bindAddress + "127.0.0.1" + "::1" + ]; + overwritehost = cfg.hostName; + overwriteprotocol = "https"; + } + (mkIf cfg.useAuthentikSSO { + hide_login_form = cfg.authentik.hideLoginForm; + }) + cfg.settings + ]; + + secrets = cfg.secrets.nextcloud; + extraAppsEnable = true; + extraApps = allExtraApps; + }; + + services.nginx.virtualHosts.${"raw-" + cfg.hostName}.listen = lib.mkForce [ + { + addr = cfg.bindAddress; + port = cfg.rawPort; + } + ]; + + services.nginx.virtualHosts.${cfg.hostName} = nginxTls // { + extraConfig = cfg.nginx.extraPublicConfig; + locations."/" = { + proxyPass = publicProxyTarget; + proxyWebsockets = true; + }; + }; + + systemd.services.nextcloud-stack-config = { + description = "Declarative Nextcloud stack configuration"; + after = [ "nextcloud-setup.service" ]; + requires = [ "nextcloud-setup.service" ]; + wantedBy = [ "multi-user.target" ]; + restartTriggers = [ configScript ]; + serviceConfig = { + Type = "oneshot"; + User = "nextcloud"; + LoadCredential = + optional (cfg.secrets.jwtFile != null) "jwt:${cfg.secrets.jwtFile}" + ++ optional (cfg.secrets.oidcClientSecretFile != null) "oidc:${cfg.secrets.oidcClientSecretFile}" + ++ optional (cfg.database.passwordFile != null) "dbpass:${cfg.database.passwordFile}" + ++ lib.mapAttrsToList (name: path: "${name}:${path}") cfg.secrets.nextcloud; + ExecStart = configScript; + }; + }; + } + + (mkIf (cfg.anubis.enable && anubisModuleAvailable) { + services.anubis.instances.nextcloud.settings = { + BIND = "${cfg.bindAddress}:${toString cfg.anubis.port}"; + TARGET = "http://${cfg.bindAddress}:${toString cfg.rawPort}"; + } + // cfg.anubis.settings; + }) + + (mkIf (cfg.addons.whiteboard.enable && whiteboardModuleAvailable) { + services.nextcloud-whiteboard-server = { + enable = true; + secrets = [ ]; + settings = { + NEXTCLOUD_URL = nextcloudUrl; + HOST = cfg.bindAddress; + PORT = toString cfg.addons.whiteboard.port; + } + // cfg.addons.whiteboard.settings; + }; + + systemd.services.nextcloud-whiteboard-server.serviceConfig = { + LoadCredential = optional (cfg.secrets.jwtFile != null) "jwt:${cfg.secrets.jwtFile}"; + ExecStart = lib.mkForce whiteboardStart; + }; + }) + + (mkIf (cfg.addons.whiteboard.enable && cfg.addons.whiteboard.nginx.enable) { + services.nginx.virtualHosts.${cfg.addons.whiteboard.hostName} = nginxTls // { + locations."/" = { + proxyPass = "http://${cfg.bindAddress}:${toString cfg.addons.whiteboard.port}"; + proxyWebsockets = true; + }; + }; + }) + + (mkIf (cfg.addons.whiteboard.enable && cfg.addons.whiteboard.disableRecording) { + nixpkgs.overlays = [ + (final: prev: { + nextcloud-whiteboard-server = prev.nextcloud-whiteboard-server.overrideAttrs (old: { + postPatch = (old.postPatch or "") + '' + substituteInPlace websocket_server/SocketManager.js \ + --replace-fail $'const support = await checkPuppeteerAvailability({ force: false })\n\t\t\tsocket.emit(\'recording-availability\', {\n\t\t\t\tavailable: support.available,\n\t\t\t\treason: support.reason || null,\n\t\t\t})' $'if (process.env.DISABLE_RECORDING === "1") {\n\t\t\t\tsocket.emit(\'recording-availability\', {\n\t\t\t\t\tavailable: false,\n\t\t\t\t\treason: null,\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst support = await checkPuppeteerAvailability({ force: false })\n\t\t\tsocket.emit(\'recording-availability\', {\n\t\t\t\tavailable: support.available,\n\t\t\t\treason: support.reason || null,\n\t\t\t})' + ''; + }); + }) + ]; + + services.nextcloud-whiteboard-server.settings.DISABLE_RECORDING = "1"; + }) + + (mkIf cfg.addons.eurooffice.enable { + virtualisation.oci-containers.containers.nextcloud-eurooffice = { + serviceName = "nextcloud-eurooffice"; + image = cfg.addons.eurooffice.image; + ports = [ "${cfg.bindAddress}:${toString cfg.addons.eurooffice.port}:80" ]; + environmentFiles = [ "/run/nextcloud-eurooffice/eurooffice.env" ]; + environment = { + JWT_ENABLED = "true"; + JWT_HEADER = cfg.addons.eurooffice.jwtHeader; + ALLOW_PRIVATE_IP_ADDRESS = "true"; + } + // cfg.addons.eurooffice.environment; + }; + + systemd.services.nextcloud-eurooffice = { + serviceConfig = { + LoadCredential = optional (cfg.secrets.jwtFile != null) "jwt:${cfg.secrets.jwtFile}"; + RuntimeDirectory = "nextcloud-eurooffice"; + RuntimeDirectoryMode = "0700"; + }; + preStart = '' + set -euo pipefail + jwt_secret="$(<"$CREDENTIALS_DIRECTORY/jwt")" + umask 077 + printf 'JWT_SECRET=%s\n' "$jwt_secret" > /run/nextcloud-eurooffice/eurooffice.env + ''; + }; + }) + + (mkIf (cfg.addons.eurooffice.enable && cfg.addons.eurooffice.nginx.enable) { + services.nginx.virtualHosts.${cfg.addons.eurooffice.hostName} = nginxTls // { + locations."/" = { + proxyPass = "http://${cfg.bindAddress}:${toString cfg.addons.eurooffice.port}"; + proxyWebsockets = true; + }; + }; + }) + + (mkIf cfg.authentik.blueprint.enable { + environment.etc."nextcloud/authentik-blueprint.yaml".text = authentikBlueprint; + }) + + (mkIf ( + cfg.authentik.blueprint.enable && cfg.authentik.blueprint.autoAttach && authentikTargetAvailable + ) (setAttrByPath cfg.authentik.blueprint.targetOption authentikBlueprint)) + ]); + }; + }; +}