diff --git a/home/apps/vicinae.nix b/home/apps/vicinae.nix index ae6a2a29..7ec9ce03 100644 --- a/home/apps/vicinae.nix +++ b/home/apps/vicinae.nix @@ -13,6 +13,7 @@ }; }; home.packages = with pkgs; [ - pulseaudio + # pulseaudio + playerctl ]; } diff --git a/modules/nixos/affine.nix b/modules/nixos/affine.nix new file mode 100644 index 00000000..7125cd07 --- /dev/null +++ b/modules/nixos/affine.nix @@ -0,0 +1,166 @@ +{ + config, + lib, + ... +}: +with lib; let + cfg = config.services.affine; + dbName = "affine"; + dbUser = "affine"; +in { + options.services.affine = { + enable = mkEnableOption "AFFiNE self-hosted workspace"; + + port = mkOption { + type = types.port; + default = 3010; + description = "Port for the AFFiNE server to listen on"; + }; + + domain = mkOption { + type = types.str; + description = "Public domain for AFFiNE (e.g. notes.darksailor.dev)"; + }; + + imageTag = mkOption { + type = types.str; + default = "stable"; + description = "Docker image tag for AFFiNE (stable, beta, canary)"; + }; + + dataDir = mkOption { + type = types.str; + default = "/var/lib/affine"; + description = "Base data directory for AFFiNE storage"; + }; + + environmentFiles = mkOption { + type = types.listOf types.path; + default = []; + description = "Environment files containing secrets (DB password, etc.)"; + }; + }; + + config = mkIf cfg.enable { + # Create data directories + systemd.tmpfiles.rules = [ + "d ${cfg.dataDir} 0755 root root -" + "d ${cfg.dataDir}/storage 0755 root root -" + "d ${cfg.dataDir}/config 0755 root root -" + "d ${cfg.dataDir}/postgres 0700 root root -" + "d ${cfg.dataDir}/redis 0755 root root -" + ]; + + virtualisation.oci-containers = { + backend = "docker"; + containers = { + affine-postgres = { + image = "pgvector/pgvector:pg16"; + volumes = [ + "${cfg.dataDir}/postgres:/var/lib/postgresql/data" + ]; + environment = { + POSTGRES_USER = dbUser; + POSTGRES_DB = dbName; + POSTGRES_INITDB_ARGS = "--data-checksums"; + POSTGRES_HOST_AUTH_METHOD = "trust"; + }; + environmentFiles = cfg.environmentFiles; + extraOptions = [ + "--network=affine-net" + "--health-cmd=pg_isready -U ${dbUser} -d ${dbName}" + "--health-interval=10s" + "--health-timeout=5s" + "--health-retries=5" + ]; + }; + + affine-redis = { + image = "redis:7"; + volumes = [ + "${cfg.dataDir}/redis:/data" + ]; + extraOptions = [ + "--network=affine-net" + "--health-cmd=redis-cli --raw incr ping" + "--health-interval=10s" + "--health-timeout=5s" + "--health-retries=5" + ]; + }; + + affine = { + image = "ghcr.io/toeverything/affine:${cfg.imageTag}"; + ports = ["127.0.0.1:${toString cfg.port}:3010"]; + dependsOn = [ + "affine-postgres" + "affine-redis" + "affine-migration" + ]; + volumes = [ + "${cfg.dataDir}/storage:/root/.affine/storage" + "${cfg.dataDir}/config:/root/.affine/config" + ]; + environment = { + AFFINE_SERVER_PORT = "3010"; + AFFINE_SERVER_HOST = cfg.domain; + AFFINE_SERVER_HTTPS = "true"; + AFFINE_SERVER_EXTERNAL_URL = "https://${cfg.domain}"; + REDIS_SERVER_HOST = "affine-redis"; + DATABASE_URL = "postgresql://${dbUser}:$${AFFINE_DB_PASSWORD:-affine}@affine-postgres:5432/${dbName}"; + AFFINE_INDEXER_ENABLED = "false"; + }; + environmentFiles = cfg.environmentFiles; + extraOptions = [ + "--network=affine-net" + ]; + }; + + affine-migration = { + image = "ghcr.io/toeverything/affine:${cfg.imageTag}"; + dependsOn = [ + "affine-postgres" + "affine-redis" + ]; + volumes = [ + "${cfg.dataDir}/storage:/root/.affine/storage" + "${cfg.dataDir}/config:/root/.affine/config" + ]; + cmd = ["sh" "-c" "node ./scripts/self-host-predeploy.js"]; + environment = { + REDIS_SERVER_HOST = "affine-redis"; + DATABASE_URL = "postgresql://${dbUser}:$${AFFINE_DB_PASSWORD:-affine}@affine-postgres:5432/${dbName}"; + AFFINE_INDEXER_ENABLED = "false"; + }; + environmentFiles = cfg.environmentFiles; + extraOptions = [ + "--network=affine-net" + ]; + }; + }; + }; + + # Create the Docker network + systemd.services.affine-network = { + description = "Create AFFiNE Docker network"; + after = ["docker.service"]; + wantedBy = ["multi-user.target"]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = "${config.virtualisation.docker.package}/bin/docker network create affine-net"; + ExecStop = "${config.virtualisation.docker.package}/bin/docker network remove affine-net"; + }; + }; + + # Ensure containers start after the network is created + systemd.services.docker-affine.after = ["affine-network.service"]; + systemd.services.docker-affine.requires = ["affine-network.service"]; + systemd.services.docker-affine-postgres.after = ["affine-network.service"]; + systemd.services.docker-affine-postgres.requires = ["affine-network.service"]; + systemd.services.docker-affine-redis.after = ["affine-network.service"]; + systemd.services.docker-affine-redis.requires = ["affine-network.service"]; + systemd.services.docker-affine-migration.after = ["affine-network.service"]; + systemd.services.docker-affine-migration.requires = ["affine-network.service"]; + }; +} diff --git a/nixos/ryu/configuration.nix b/nixos/ryu/configuration.nix index 4ced3983..fd713f5d 100644 --- a/nixos/ryu/configuration.nix +++ b/nixos/ryu/configuration.nix @@ -2,6 +2,7 @@ pkgs, lib, device, + config, ... }: { imports = [ @@ -47,6 +48,7 @@ auto-optimise-store = true; extra-experimental-features = "nix-command flakes auto-allocate-uids"; trusted-users = [device.user]; + extra-sandbox-paths = [config.programs.ccache.cacheDir]; }; extraOptions = '' build-users-group = nixbld diff --git a/nixos/ryu/programs/ccache.nix b/nixos/ryu/programs/ccache.nix new file mode 100644 index 00000000..8776f90d --- /dev/null +++ b/nixos/ryu/programs/ccache.nix @@ -0,0 +1,6 @@ +{...}: { + programs.ccache = { + enable = true; + packageNames = ["ollama" "orca-slicer" "opencv" "onnxruntime" "obs-studio" "llama-cpp"]; + }; +} diff --git a/nixos/tako/services/affine.nix b/nixos/tako/services/affine.nix new file mode 100644 index 00000000..4c35195b --- /dev/null +++ b/nixos/tako/services/affine.nix @@ -0,0 +1,89 @@ +{config, ...}: let + domain = "notes.darksailor.dev"; +in { + imports = [ + ../../../modules/nixos/affine.nix + ]; + + # SOPS secrets + sops = { + secrets = { + "affine/db_password" = {}; + "authelia/oidc/affine/client_id" = { + owner = config.systemd.services.authelia-darksailor.serviceConfig.User; + mode = "0440"; + restartUnits = ["authelia-darksailor.service"]; + }; + "authelia/oidc/affine/client_secret" = { + owner = config.systemd.services.authelia-darksailor.serviceConfig.User; + mode = "0440"; + restartUnits = ["authelia-darksailor.service"]; + }; + }; + templates."affine.env".content = '' + AFFINE_DB_PASSWORD=${config.sops.placeholder."affine/db_password"} + POSTGRES_PASSWORD=${config.sops.placeholder."affine/db_password"} + AFFINE_SERVER_EXTERNAL_URL=https://${domain} + ''; + }; + + # Enable AFFiNE service + services.affine = { + enable = true; + inherit domain; + environmentFiles = [ + config.sops.templates."affine.env".path + ]; + }; + + # Caddy reverse proxy with SSO forward auth + services.caddy.virtualHosts."${domain}".extraConfig = '' + reverse_proxy localhost:${toString config.services.affine.port} + ''; + + # Authelia access control rules + services.authelia.instances.darksailor.settings = { + access_control.rules = [ + { + inherit domain; + policy = "bypass"; + resources = [ + "^/api/(sync|awareness)([/?].*)?$" + "^/socket\\.io([/?].*)?$" + ]; + } + { + inherit domain; + policy = "one_factor"; + } + ]; + # OIDC client for AFFiNE + identity_providers.oidc.clients = [ + { + client_name = "AFFiNE: Darksailor"; + client_id = ''{{ secret "${config.sops.secrets."authelia/oidc/affine/client_id".path}" }}''; + client_secret = ''{{ secret "${config.sops.secrets."authelia/oidc/affine/client_secret".path}" }}''; + public = false; + authorization_policy = "one_factor"; + require_pkce = false; + redirect_uris = [ + "https://${domain}/oauth/callback" + ]; + scopes = [ + "openid" + "email" + "profile" + ]; + response_types = ["code"]; + grant_types = ["authorization_code"]; + userinfo_signed_response_alg = "none"; + token_endpoint_auth_method = "client_secret_post"; + } + ]; + }; + + # Ensure containers start after secrets are available + systemd.services.docker-affine.after = ["sops-install-secrets.service"]; + systemd.services.docker-affine-migration.after = ["sops-install-secrets.service"]; + systemd.services.docker-affine-postgres.after = ["sops-install-secrets.service"]; +} diff --git a/nixos/tako/services/default.nix b/nixos/tako/services/default.nix index a12eb479..1c8e45f5 100644 --- a/nixos/tako/services/default.nix +++ b/nixos/tako/services/default.nix @@ -1,5 +1,6 @@ {...}: { imports = [ + ./affine.nix ./attic.nix ./atuin.nix ./authelia.nix diff --git a/secrets/secrets.yaml b/secrets/secrets.yaml index b40842dd..68f7fdd5 100644 --- a/secrets/secrets.yaml +++ b/secrets/secrets.yaml @@ -53,6 +53,9 @@ authelia: excalidraw: client_id: ENC[AES256_GCM,data:ANaCFTiPnR/bP51lSMfiTRX7ZGZ2pmX3Guamsyj7KRzD34G18E+UUgXi0YdbDfmFxcEj+nvoerf7wWhtvIzO1Q==,iv:CyNiLA0PH0p1Zwdf8B6/Ysb6GODClnXkPctbtZnoddw=,tag:X3kAkBKD407QFg/Se33Flg==,type:str] client_secret: ENC[AES256_GCM,data:VHIbKjHWXfQCUp3wh2dsMpMaDdCabmVlLMHcMnTCXPr5ZNIS1zpyGD6keapoOYywwvDFenICf73vpHun5aFhLw==,iv:HjRTwREC2jMsW1VrVYe4iywGc9apWZWLwh5aHOjvde0=,tag:Jl9kDI8C9VjSm6SiePk7Ow==,type:str] + affine: + client_id: ENC[AES256_GCM,data:riwC/9GuHamkV7U80+qCXP/F2wP0volVmRXtkVOQu7NfBb9OlSnamMvVh1pRNHj4c3AZWHpYIIHpkhuNKUuD+A==,iv:gi70CRZqRYfvHq7JhY+N3rX3Trb7eLqgmUlFqLk7p+4=,tag:M6MzyKRTJpZsznXzvxQINQ==,type:str] + client_secret: ENC[AES256_GCM,data:/4+606H4s50+E25LOaVRi5Vt0rpzg9mAHSonYaLASPW/jEwAxwTNunWGvOg4ydjF+D9VBUbYcp1krZnlrhDp9sRR81vf+1G9aA69mWVPF7aMjBx5wA44tBszjLc/KKx804qbopG1m4y828aPrFvknx2d7Rox/YkvhLoq5jPjyVU=,iv:9s4OQNTtPBcN+7kvtH9u9kkPadFG7MG0LdlHAhMR5sw=,tag:BV0G1f/5qGAfrdRMshHzYw==,type:str] lldap: jwt: ENC[AES256_GCM,data:61dwC1ElOOGaf0CmalzXZnxImEyufKjUUWcNaEcOuv3TEODhQyHK7g==,iv:CVEJVuaCc2gDmSYWHS3fPL8FjbvblF6IladAzGoGb0o=,tag:OMm/OdKjliHjsGqJripLbg==,type:str] seed: ENC[AES256_GCM,data:jJPutPkhFVFxLbbQNZznHHiilP/cN2r+/vT4ArQVRQSqPMnkkwgc3LNk4sUTrT9V,iv:LD1IJ1CgtDfYf1gSyyaU+hir0InuDEq0u7ppMmwGJRY=,tag:cK4l4Evr7V9WEUEL7V9jtQ==,type:str] @@ -101,6 +104,8 @@ livekit: key_secret: ENC[AES256_GCM,data:n0SH6SLYXOwdjJ5nNku0Pg6/DZxmYG7iLfNNDbKlvF9DiCDxfn2ag1fToxWGz0qrAvqV6b3PdD123BURDzJ+gQ==,iv:1Pix12Pxr/kX/SqRWHOaSdccIZTQsSoFVGXYNyx2Rfo=,tag:QG1qDJPBUUtlih+TcXXsRQ==,type:str] coturn: static_auth_secret: ENC[AES256_GCM,data:osEBYgWGZl+SnqVV1G9IxMys/qDm6WTtj4nILYVw0klDjiB6vd21yA0ik/rLv9E6Y539uMCk3oB0NS7I72U1hQ==,iv:jruS3vfe0fVHY67qNhEgaCEp/9cR57UIu8a/LhdTC1o=,tag:vhxXhh9u4bOSu/lxINjvew==,type:str] +affine: + db_password: ENC[AES256_GCM,data:AbpoEbmeihtVIoRaWxVL8+v3oCk5iiia9qZLKgyy98qTuNZruiaV3kQN6clYvWgHbzJta5/H9e+xocrEtw8C/A==,iv:2vPeDAJuVujPgM+kr6AFAvat2MCJnsblebx23Ey7YNA=,tag:ukuK30aZ//MKabhSRtLNXw==,type:str] sops: age: - recipient: age1pw7kluxp7872c63ne4jecq75glj060jkmqwzkk6esatuyck9egfswufdpk @@ -112,7 +117,7 @@ sops: VGZKdHpVeFRpQUxtSEkyaEhLMlBJcGsKLb0DvPNZosPBUuiX6qz1s5IO5INQh8CK ZtXTVClwMSmaUYhdSB2gKFrKVZHXTJZ4oAL5t/BpC0pOHyr+o96T3Q== -----END AGE ENCRYPTED FILE----- - lastmodified: "2026-02-12T13:19:01Z" - mac: ENC[AES256_GCM,data:IVU1PbDwH1JKG3qPOtmfMZr7BJ7zR/UGQ167Tyf62w5V1gaiVoeqjD8/MR6OSvhMDNYxjJXRKg9E9N8q4JRxok34v5zOfqWchnXEP9wIS39kgsYJ1Hra7hOryd5n49/Xkwyen6f1VSe1nkKtldWS9XHwDBZRrSE+kaXcZTQmKIY=,iv:WLRVIEzR0MFsY6EAgyXZCHQz/xD4cSaeikA8nZqHy38=,tag:+TyOvwWkHa4fHYonKBfxyg==,type:str] + lastmodified: "2026-02-18T10:39:02Z" + mac: ENC[AES256_GCM,data:fz+15A9C6G4x3OUzoKY3yvUt75dx2aql2GEmnuJcPM1YC9KN083PodKQR1axr23w7C9S2/iTXEnYJhx3X+dfoJbTRWKVvraGHWQ9w5jbjVMVfU+97JxrtykdwXKmwlzTbF4lakHd4dWRv5e9aR7vN2JX1NUd9EuazQ0/xPeIVOQ=,iv:l4/r/poWY5IdKrM0IxbdWfg6JB7r+tssh9LIZDSNr8w=,tag:HE3C7LzWrzCKE91VdZnqXg==,type:str] unencrypted_suffix: _unencrypted version: 3.11.0