Something went wrong. Try again.
Tea journaling on ATProto (alpha)
Something went wrong. Try again.
Nix
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348{ config, lib, pkgs, ...}:
let cfg = config.services.arabica;
moderatorUserType = lib.types.submodule { options = { did = lib.mkOption { type = lib.types.str; description = "AT Protocol DID of the moderator."; example = "did:plc:abc123xyz"; }; handle = lib.mkOption { type = lib.types.str; default = ""; description = "Optional handle for the moderator (for readability)."; example = "alice.bsky.social"; }; role = lib.mkOption { type = lib.types.enum [ "admin" "moderator" ]; description = "The moderation role assigned to this user."; }; note = lib.mkOption { type = lib.types.str; default = ""; description = "Optional note about this moderator."; }; }; };
# Build the moderators JSON config file from Nix settings moderatorsConfigFile = pkgs.writeText "moderators.json" ( builtins.toJSON { roles = { admin = { description = "Full platform control"; permissions = [ "hide_record" "unhide_record" "blacklist_user" "unblacklist_user" "view_reports" "dismiss_report" "view_audit_log" "reset_autohide" ]; }; moderator = { description = "Content moderation"; permissions = [ "hide_record" "unhide_record" "view_reports" "dismiss_report" ]; }; }; users = map ( u: { inherit (u) did role; } // lib.optionalAttrs (u.handle != "") { inherit (u) handle; } // lib.optionalAttrs (u.note != "") { inherit (u) note; } ) cfg.moderation.moderators; } );
# Resolve the config path: explicit file takes priority, then generated from moderators list effectiveConfigPath = if cfg.moderation.configFile != null then cfg.moderation.configFile else if cfg.moderation.moderators != [ ] then moderatorsConfigFile else null;in{ options.services.arabica = { enable = lib.mkEnableOption "Arabica coffee brew tracking service";
package = lib.mkOption { type = lib.types.package; default = pkgs.callPackage ./default.nix { }; defaultText = lib.literalExpression "pkgs.callPackage ./default.nix { }"; description = "The arabica package to use."; };
settings = { port = lib.mkOption { type = lib.types.port; default = 18910; description = "Port on which the arabica server listens."; };
logLevel = lib.mkOption { type = lib.types.enum [ "debug" "info" "warn" "error" ]; default = "info"; description = "Log level for the arabica server."; };
logFormat = lib.mkOption { type = lib.types.enum [ "pretty" "json" ]; default = "json"; description = "Log format. Use 'json' for production, 'pretty' for development."; };
secureCookies = lib.mkOption { type = lib.types.bool; default = true; description = "Whether to set the Secure flag on cookies. Should be true when using HTTPS."; };
publicUrl = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; description = '' Public-facing URL of the server (e.g. https://arabica.social). Used for absolute URLs in OpenGraph metadata. If not set, the server derives it from the Host header at request time. ''; example = "https://arabica.social"; }; };
moderation = { configFile = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; description = '' Path to a moderators JSON config file. If set, this takes priority over the `moderators` list option. See the project README for the expected format. ''; example = "/etc/arabica/moderators.json"; };
moderators = lib.mkOption { type = lib.types.listOf moderatorUserType; default = [ ]; description = '' List of moderator users. When set, a config file is generated automatically with the standard admin and moderator roles. Ignored if `configFile` is set. ''; example = lib.literalExpression '' [ { did = "did:plc:abc123"; role = "admin"; handle = "alice.bsky.social"; note = "Platform owner"; } { did = "did:plc:def456"; role = "moderator"; handle = "bob.bsky.social"; } ] ''; }; };
smtp = { enable = lib.mkOption { type = lib.types.bool; default = false; description = '' Enable SMTP email notifications for join requests. SMTP credentials (SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_FROM) can be provided via environmentFiles. ''; };
host = lib.mkOption { type = lib.types.str; default = ""; description = "SMTP server hostname. Can also be set via SMTP_HOST in an environment file."; example = "smtp.example.com"; };
port = lib.mkOption { type = lib.types.nullOr lib.types.port; default = null; description = "SMTP server port. Can also be set via SMTP_PORT in an environment file."; };
from = lib.mkOption { type = lib.types.str; default = ""; description = "Sender address for outgoing email. Can also be set via SMTP_FROM in an environment file."; example = "noreply@arabica.example.com"; }; };
environmentFiles = lib.mkOption { type = lib.types.listOf lib.types.path; default = [ ]; description = '' List of environment files to load into the systemd service. Useful for secrets like SMTP_USER and SMTP_PASS that should not be stored in the Nix store. ''; example = lib.literalExpression ''[ "/run/secrets/arabica.env" ]''; };
oauth = { clientId = lib.mkOption { type = lib.types.str; description = '' OAuth client ID. This should be the URL to your client metadata endpoint. For example: https://arabica.example.com/.well-known/oauth-client-metadata.json ''; example = "https://arabica.example.com/.well-known/oauth-client-metadata.json"; };
redirectUri = lib.mkOption { type = lib.types.str; description = '' OAuth redirect URI. This is where users are redirected after authentication. For example: https://arabica.example.com/oauth/callback ''; example = "https://arabica.example.com/oauth/callback"; }; };
dataDir = lib.mkOption { type = lib.types.path; default = "/var/lib/arabica"; description = "Directory where arabica stores its data (OAuth sessions, etc.)."; };
user = lib.mkOption { type = lib.types.str; default = "arabica"; description = "User account under which arabica runs."; };
group = lib.mkOption { type = lib.types.str; default = "arabica"; description = "Group under which arabica runs."; };
otelEndpoint = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; description = "OTLP HTTP endpoint for OpenTelemetry traces (e.g. localhost:4318)."; example = "localhost:4318"; };
openFirewall = lib.mkOption { type = lib.types.bool; default = false; description = "Whether to open the firewall for the arabica port."; }; };
config = lib.mkIf cfg.enable { users.users.${cfg.user} = lib.mkIf (cfg.user == "arabica") { isSystemUser = true; group = cfg.group; description = "Arabica service user"; home = cfg.dataDir; createHome = true; };
users.groups.${cfg.group} = lib.mkIf (cfg.group == "arabica") { };
systemd.services.arabica = { description = "Arabica Coffee Brew Tracking Service"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ];
serviceConfig = { Type = "simple"; User = cfg.user; Group = cfg.group; ExecStart = "${cfg.package}/bin/arabica"; Restart = "on-failure"; RestartSec = "10s";
EnvironmentFile = cfg.environmentFiles;
# Security hardening NoNewPrivileges = true; PrivateTmp = true; ProtectSystem = "strict"; ProtectHome = true; ReadWritePaths = [ cfg.dataDir ]; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectControlGroups = true; RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; RestrictNamespaces = true; LockPersonality = true; RestrictRealtime = true; RestrictSUIDSGID = true; MemoryDenyWriteExecute = true; SystemCallArchitectures = "native"; CapabilityBoundingSet = ""; };
environment = { LOG_LEVEL = cfg.settings.logLevel; LOG_FORMAT = cfg.settings.logFormat; SECURE_COOKIES = lib.boolToString cfg.settings.secureCookies; # Arabica per-app env (uppercase prefix matches app.Name). ARABICA_PORT = toString cfg.settings.port; ARABICA_OAUTH_CLIENT_ID = cfg.oauth.clientId; ARABICA_OAUTH_REDIRECT_URI = cfg.oauth.redirectUri; ARABICA_DATA_DIR = cfg.dataDir; } // lib.optionalAttrs (cfg.settings.publicUrl != null) { ARABICA_PUBLIC_URL = cfg.settings.publicUrl; } // lib.optionalAttrs (effectiveConfigPath != null) { ARABICA_MODERATORS_CONFIG = toString effectiveConfigPath; } // lib.optionalAttrs (cfg.smtp.enable && cfg.smtp.host != "") { SMTP_HOST = cfg.smtp.host; } // lib.optionalAttrs (cfg.smtp.enable && cfg.smtp.port != null) { SMTP_PORT = toString cfg.smtp.port; } // lib.optionalAttrs (cfg.smtp.enable && cfg.smtp.from != "") { SMTP_FROM = cfg.smtp.from; } // lib.optionalAttrs (cfg.otelEndpoint != null) { OTEL_EXPORTER_OTLP_ENDPOINT = cfg.otelEndpoint; }; };
networking.firewall.allowedTCPPorts = lib.optional cfg.openFirewall cfg.settings.port; };}