refactor: move ark into dentritic pattern

This commit is contained in:
2026-05-21 22:50:15 -04:00
parent 8043068885
commit f3b32776f6
48 changed files with 586 additions and 1562 deletions

View File

@@ -1,7 +1,7 @@
{ self, ... }: {
flake.homeModules.commonFeatureWallpaper = { ... }:
let
wallpaper = builtins.toString "${self}/assets/wallpaper2.jpg";
wallpaper = builtins.toString "${self}/assets/wallpaper.jpg";
in {
services.hyprpaper = {
enable = true;

View File

@@ -0,0 +1,39 @@
# Do not modify this file! It was generated by nixos-generate-config
# and may be overwritten by future invocations. Please make changes
# to /etc/nixos/configuration.nix instead.
{ config, lib, pkgs, modulesPath, ... }:
{
imports = [ (modulesPath + "/installer/scan/not-detected.nix") ];
boot.initrd.availableKernelModules =
[ "xhci_pci" "ahci" "usbhid" "usb_storage" "sd_mod" ];
boot.initrd.kernelModules = [ ];
boot.kernelModules = [ ];
boot.extraModulePackages = [ ];
fileSystems."/" = {
device = "/dev/disk/by-uuid/56034a80-72ea-4e67-be41-2e8cb642fb44";
fsType = "ext4";
};
fileSystems."/boot" = {
device = "/dev/disk/by-uuid/DE8B-01DE";
fsType = "vfat";
options = [ "fmask=0022" "dmask=0022" ];
};
swapDevices = [ ];
# Enables DHCP on each ethernet and wireless interface. In case of scripted networking
# (the default) this is the recommended approach. When using systemd-networkd it's
# still possible to use this option, but it's recommended to use it in conjunction
# with explicit per-interface declarations with `networking.interfaces.<interface>.useDHCP`.
networking.useDHCP = lib.mkDefault true;
# networking.interfaces.eno1.useDHCP = lib.mkDefault true;
# networking.interfaces.wlp1s0.useDHCP = lib.mkDefault true;
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.intel.updateMicrocode =
lib.mkDefault config.hardware.enableRedistributableFirmware;
}

View File

@@ -0,0 +1,77 @@
{ self, ... }: {
flake.nixosModules.koonArkConfiguration = { config, ... }: {
imports = [
./_hardware-configuration.nix
self.nixosModules.koonArkUser
self.nixosModules.koonArkSops
self.nixosModules.koonArkServiceAudio
self.nixosModules.koonArkServiceAuth
self.nixosModules.koonArkServiceGit
self.nixosModules.koonArkServiceHome
self.nixosModules.koonArkServicePhotos
self.nixosModules.koonArkServiceRadicale
self.nixosModules.koonArkServiceWakapi
];
# Use the systemd-boot EFI boot loader.
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
networking.hostName = "ark";
networking.nameservers = [ "8.8.8.8" "1.1.1.1" ];
services.tailscale = {
enable = true;
useRoutingFeatures = "both";
extraUpFlags = [ "--accept-dns=false" ];
};
# oauth.name = "KoonFamily";
# oauth.secrets = import ./oauth-secrets.nix;
security.sudo.wheelNeedsPassword = false;
services.cloudflared = {
enable = true;
tunnels = {
"91d31395-fbc7-45a1-ae13-148957b32ecd" = {
credentialsFile = config.sops.secrets.tunnel-credentials.path;
ingress = {
"auth.koon.us" = "http://localhost:1411";
"photos.koon.us" = "http://localhost:2283";
"home.koon.us" = "http://localhost:8123";
"docs.koon.us" = "http://localhost:3004";
"git.koon.us" = "http://localhost:3000";
"ssh.koon.us" = "ssh://localhost:2222";
"audio.koon.us" = "http://localhost:8021";
"radicale.koon.us" = "http://localhost:5232";
"waka.koon.us" = "http://localhost:3006";
# "ride.koon.us" = "http://localhost:3007";
# "ride-api.koon.us" = "http://localhost:8080";
"money.koon.us" = "http://localhost:3160";
"zero.koon.us" = "http://localhost:4848";
"money-api.koon.us" = "http://localhost:3161";
};
default = "http_status:404";
};
};
};
fileSystems."/mnt/hdd" = {
device = "/dev/sdb";
fsType = "ext4";
};
services.openssh.enable = true;
networking.firewall.allowedTCPPorts = [ 8123 22 ];
time.timeZone = "America/New_York";
nix.settings.experimental-features = [ "nix-command" "flakes" ];
system.stateVersion = "25.05"; # Did you read the comment?
};
}

View File

@@ -0,0 +1,8 @@
{ self, inputs, ... }: {
flake.nixosConfigurations.koonArk = inputs.nixpkgs-unstable.lib.nixosSystem {
modules = [
self.inputs.sops-nix.nixosModules.sops
self.nixosModules.koonArkConfiguration
];
};
}

View File

@@ -0,0 +1,8 @@
{ ... }: {
flake.nixosModules.koonArkServiceAudio = { ... }: {
services.audiobookshelf = {
enable = true;
port = 8021;
};
};
}

View File

@@ -0,0 +1,21 @@
{ ... }: {
flake.nixosModules.koonArkServiceAuth = { config, ... }: {
services.pocket-id = {
enable = true;
settings = {
APP_URL = "https://auth.koon.us";
TRUST_PROXY = true;
ANALYTICS_DISABLED = true;
UI_CONFIG_DISABLED = true;
APP_NAME = "KoonFamily";
};
credentials = {
ENCRYPTION_KEY = config.sops.secrets."pocket-id-encryption-key".path;
};
};
};
}

View File

@@ -0,0 +1,193 @@
{ ... }: {
flake.nixosModules.koonArkServiceGit = { config, lib, pkgs, ... }:
let
oauthName = "KoonFamily";
in {
services.openssh = {
enable = true;
# hostKeys = [
# { path = "/etc/ssh/ssh_host_ed25519_key"; type = "ed25519"; }
# { path = "/etc/ssh/ssh_host_rsa_key"; type = "rsa"; bits = 4096; }
# ];
settings = {
# explicitly allow post-quantum KEX
KexAlgorithms = [
"mlkem768x25519-sha256"
"sntrup761x25519-sha512"
"curve25519-sha256"
];
};
};
services.gitea = {
enable = true;
user = "git";
group = "git";
database = {
user = "git";
name = "git";
type = "postgres";
};
settings = {
server = {
DOMAIN = "git.koon.us";
ROOT_URL = "https://git.koon.us";
HTTP_PORT = 3000;
LANDING_PAGE = "/max";
SSH_DOMAIN = "ssh.koon.us";
SSH_PORT = 2222;
START_SSH_SERVER = true;
};
oauth2_client = {
ACCOUNT_LINKING = "auto";
ENABLE_AUTO_REGISTRATION = true;
UPDATE_AVATAR = true;
USERNAME = "email";
};
service = {
DISABLE_REGISTRATION = true;
ENABLE_PASSWORD_SIGNIN_FORM = false;
ENABLE_PASSKEY_AUTHENTICATION = false;
SHOW_MILESTONES_DASHBOARD_PAGE = false;
};
"service.explore" = {
DISABLE_USERS_PAGE = true;
DISABLE_ORGANIZATIONS_PAGE = true;
DISABLE_CODE_PAGE = true;
};
};
};
users.users.git = {
isSystemUser = true;
group = "git";
home = "/var/lib/gitea";
description = "Git server (Gitea)";
createHome = true;
};
users.groups.git = { };
systemd.services.gitea = {
serviceConfig = {
RestartSec = "60"; # Retry every minute
};
preStart = let
exe = lib.getExe config.services.gitea.package;
clientIdPath = config.sops.secrets."oauth/git/clientId".path;
clientSecretPath = config.sops.secrets."oauth/git/clientSecret".path;
args = lib.escapeShellArgs (lib.concatLists [
[ "--name" oauthName ]
[ "--provider" "openidConnect" ]
# [ "--key" config.oauth.secrets.git.clientId ]
[
"--auto-discover-url"
"https://auth.koon.us/.well-known/openid-configuration"
]
[ "--scopes" "email" ]
[ "--scopes" "profile" ]
[ "--group-claim-name" "groups" ]
[ "--admin-group" "admin" ]
[ "--skip-local-2fa" ]
]);
in lib.mkAfter ''
CLIENT_ID=$(cat ${clientIdPath})
CLIENT_SECRET=$(cat ${clientSecretPath})
provider_id=$(${exe} admin auth list | ${pkgs.gnugrep}/bin/grep -w '${oauthName}' | cut -f1)
if [[ -z "$provider_id" ]]; then
${exe} admin auth add-oauth ${args} --key "$CLIENT_ID" --secret "$CLIENT_SECRET"
else
${exe} admin auth update-oauth --id "$provider_id" ${args} --key "$CLIENT_ID" --secret "$CLIENT_SECRET"
fi
mkdir -p /var/lib/gitea/custom/public/assets/img/
ln -sf ${
./git/assets/img/logo.svg
} /var/lib/gitea/custom/public/assets/img/logo.svg
ln -sf ${
./git/assets/img/favicon.svg
} /var/lib/gitea/custom/public/assets/img/favicon.svg
mkdir -p /var/lib/gitea/custom/templates/base/
ln -sf ${
./git/templates/base/head_navbar.tmpl
} /var/lib/gitea/custom/templates/base/head_navbar.tmpl
ln -sf ${
./git/templates/base/footer_content.tmpl
} /var/lib/gitea/custom/templates/base/footer_content.tmpl
mkdir -p /var/lib/gitea/custom/templates/custom/
ln -sf ${
./git/templates/custom/header.tmpl
} /var/lib/gitea/custom/templates/custom/header.tmpl
'';
};
services.restic.backups = {
git-local = {
repository = "/mnt/hdd/restic/git";
passwordFile = config.sops.secrets.restic-password.path;
initialize = true;
paths = [ "/var/lib/gitea/repositories" "/var/backup/git" ];
backupPrepareCommand = ''
mkdir -p /var/backup/git
${pkgs.sudo}/bin/sudo ${pkgs.systemd}/bin/systemctl stop gitea
${pkgs.sudo}/bin/sudo -u postgres ${pkgs.postgresql}/bin/pg_dump \
--clean \
--if-exists \
--dbname=git > /var/backup/git/postgres.sql
'';
backupCleanupCommand = ''
${pkgs.sudo}/bin/sudo ${pkgs.systemd}/bin/systemctl start gitea
'';
};
git-remote = {
repository = "rest:http://m1:8000/git";
passwordFile = config.sops.secrets.restic-password.path;
initialize = true;
paths = [ "/var/lib/gitea/repositories" "/var/backup/git" ];
backupPrepareCommand = ''
mkdir -p /var/backup/git
${pkgs.sudo}/bin/sudo ${pkgs.systemd}/bin/systemctl stop gitea
${pkgs.sudo}/bin/sudo -u postgres ${pkgs.postgresql}/bin/pg_dump \
--clean \
--if-exists \
--dbname=git > /var/backup/git/postgres.sql
'';
backupCleanupCommand = ''
${pkgs.sudo}/bin/sudo ${pkgs.systemd}/bin/systemctl start gitea
'';
};
};
environment.systemPackages = with pkgs;
let
scripts = with pkgs; {
restore_git_pg = writeShellScriptBin "restore_git_pg" ''
${pkgs.sudo}/bin/sudo -u postgres psql --dbname=git < /var/backup/git/postgres.sql
'';
restore_git = writeShellScriptBin "restore_git" ''
${pkgs.sudo}/bin/sudo ${pkgs.systemd}/bin/systemctl stop gitea
${pkgs.sudo}/bin/sudo ${restic}/bin/restic -r /mnt/hdd/restic/git restore latest --target /
${scripts.restore_git_pg}/bin/restore_git_pg
${pkgs.sudo}/bin/sudo ${pkgs.systemd}/bin/systemctl start gitea
'';
};
in [ scripts.restore_git_pg scripts.restore_git ];
};
}

View File

@@ -0,0 +1,15 @@
/* custom.css */
:root {
--color-primary: #2e7d32;
--color-primary-dark: #1b5e20;
}
/* Hide "Powered by Gitea" */
.footer .ui.container .ui.left {
display: none;
}
/* Custom header background */
.ui.top.menu {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}

View File

@@ -0,0 +1,6 @@
<svg width="321" height="524" viewBox="0 0 321 524" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M321 249C267.137 251.03 192 393.5 192 524L192 249L321 249Z" fill="black"/>
<path d="M0 190C54.6982 188.598 131 85 131 0V190H0Z" fill="black"/>
<path d="M70 249H131V310H70V249Z" fill="black"/>
<path d="M192 129H253V190H192V129Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 359 B

View File

@@ -0,0 +1,6 @@
<svg width="321" height="524" viewBox="0 0 321 524" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M321 249C267.137 251.03 192 393.5 192 524L192 249L321 249Z" fill="black"/>
<path d="M0 190C54.6982 188.598 131 85 131 0V190H0Z" fill="black"/>
<path d="M70 249H131V310H70V249Z" fill="black"/>
<path d="M192 129H253V190H192V129Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 359 B

View File

@@ -0,0 +1,3 @@
<footer class="page-footer" role="group" aria-label="{{ctx.Locale.Tr "aria.footer"}}">
© MMXXV Koon Family
</footer>

View File

@@ -0,0 +1,190 @@
{{$notificationUnreadCount := 0}}
{{if and .IsSigned .NotificationUnreadCount}}
{{$notificationUnreadCount = call .NotificationUnreadCount ctx}}
{{end}}
{{$activeStopwatch := NIL}}
{{if and .IsSigned EnableTimetracking .GetActiveStopwatch}}
{{$activeStopwatch = call .GetActiveStopwatch ctx}}
{{end}}
<nav id="navbar" aria-label="{{ctx.Locale.Tr "aria.navbar"}}">
<div class="navbar-left">
<!-- the logo -->
<a class="item" id="navbar-logo" href="{{AppSubUrl}}/" aria-label="{{if .IsSigned}}{{ctx.Locale.Tr "dashboard"}}{{else}}{{ctx.Locale.Tr "home"}}{{end}}">
<img width="30" height="30" src="{{AssetUrlPrefix}}/img/logo.svg" alt="{{ctx.Locale.Tr "logo"}}" aria-hidden="true">
</a>
<!-- mobile right menu, it must be here because in mobile view, each item is a flex column, the first item is a full row column -->
<div class="ui secondary menu navbar-mobile-right only-mobile">
{{if $activeStopwatch}}
<a id="mobile-stopwatch-icon" class="active-stopwatch item" href="{{$activeStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}" data-seconds="{{$activeStopwatch.Seconds}}">
<div class="tw-relative">
{{svg "octicon-stopwatch"}}
<span class="header-stopwatch-dot"></span>
</div>
</a>
{{end}}
{{if .IsSigned}}
<a id="mobile-notifications-icon" class="item" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}">
<div class="tw-relative">
{{svg "octicon-bell"}}
<span class="notification_count{{if not $notificationUnreadCount}} tw-hidden{{end}}">{{$notificationUnreadCount}}</span>
</div>
</a>
{{end}}
<button class="item ui icon mini button tw-m-0" id="navbar-expand-toggle" aria-label="{{ctx.Locale.Tr "home.nav_menu"}}">{{svg "octicon-three-bars"}}</button>
</div>
</div>
<!-- the full dropdown menus -->
<div class="navbar-right">
{{if and .IsSigned .MustChangePassword}}
<div class="ui dropdown jump item" data-tooltip-content="{{ctx.Locale.Tr "user_profile_and_more"}}">
<span>
{{ctx.AvatarUtils.Avatar .SignedUser 24 "tw-mr-1"}}
<span class="only-mobile">{{.SignedUser.Name}}</span>
<span class="not-mobile">{{svg "octicon-triangle-down"}}</span>
</span>
<div class="menu user-menu">
<div class="header">
{{ctx.Locale.Tr "signed_in_as"}} <strong>{{.SignedUser.Name}}</strong>
</div>
<div class="divider"></div>
<a class="item link-action" href data-url="{{AppSubUrl}}/user/logout">
{{svg "octicon-sign-out"}}
{{ctx.Locale.Tr "sign_out"}}
</a>
</div><!-- end content avatar menu -->
</div><!-- end dropdown avatar menu -->
{{else if .IsSigned}}
{{if $activeStopwatch}}
<a class="item not-mobile active-stopwatch" href="{{$activeStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}" data-seconds="{{$activeStopwatch.Seconds}}">
<div class="tw-relative">
{{svg "octicon-stopwatch"}}
<span class="header-stopwatch-dot"></span>
</div>
</a>
{{end}}
<a class="item not-mobile" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}">
<div class="tw-relative">
{{svg "octicon-bell"}}
<span class="notification_count{{if not $notificationUnreadCount}} tw-hidden{{end}}">{{$notificationUnreadCount}}</span>
</div>
</a>
<div class="ui dropdown jump item" data-tooltip-content="{{ctx.Locale.Tr "create_new"}}">
<span class="text">
{{svg "octicon-plus"}}
<span class="not-mobile">{{svg "octicon-triangle-down"}}</span>
<span class="only-mobile">{{ctx.Locale.Tr "create_new"}}</span>
</span>
<div class="menu">
<a class="item" href="{{AppSubUrl}}/repo/create">
{{svg "octicon-plus"}} {{ctx.Locale.Tr "new_repo"}}
</a>
{{if not .DisableMigrations}}
<a class="item" href="{{AppSubUrl}}/repo/migrate">
{{svg "octicon-repo-push"}} {{ctx.Locale.Tr "new_migrate"}}
</a>
{{end}}
{{if .SignedUser.CanCreateOrganization}}
<a class="item" href="{{AppSubUrl}}/org/create">
{{svg "octicon-organization"}} {{ctx.Locale.Tr "new_org"}}
</a>
{{end}}
</div><!-- end content create new menu -->
</div><!-- end dropdown menu create new -->
<div class="ui dropdown jump item" data-tooltip-content="{{ctx.Locale.Tr "user_profile_and_more"}}">
<span>
{{ctx.AvatarUtils.Avatar .SignedUser 24 "tw-mr-1"}}
<span class="only-mobile">{{.SignedUser.Name}}</span>
<span class="not-mobile">{{svg "octicon-triangle-down"}}</span>
</span>
<div class="menu user-menu">
<div class="header">
{{ctx.Locale.Tr "signed_in_as"}} <strong>{{.SignedUser.Name}}</strong>
</div>
<div class="divider"></div>
<a class="item" href="{{.SignedUser.HomeLink}}">
{{svg "octicon-person"}}
{{ctx.Locale.Tr "your_profile"}}
</a>
{{if not .DisableStars}}
<a class="item" href="{{.SignedUser.HomeLink}}?tab=stars">
{{svg "octicon-star"}}
{{ctx.Locale.Tr "your_starred"}}
</a>
{{end}}
<a class="item" href="{{AppSubUrl}}/notifications/subscriptions">
{{svg "octicon-bell"}}
{{ctx.Locale.Tr "notification.subscriptions"}}
</a>
<a class="{{if .PageIsUserSettings}}active {{end}}item" href="{{AppSubUrl}}/user/settings">
{{svg "octicon-tools"}}
{{ctx.Locale.Tr "your_settings"}}
</a>
<a class="item" target="_blank" rel="noopener noreferrer" href="https://docs.gitea.com">
{{svg "octicon-question"}}
{{ctx.Locale.Tr "help"}}
</a>
{{if .IsAdmin}}
<div class="divider"></div>
<a class="{{if .PageIsAdmin}}active {{end}}item" href="{{AppSubUrl}}/-/admin">
{{svg "octicon-server"}}
{{ctx.Locale.Tr "admin_panel"}}
</a>
{{end}}
<div class="divider"></div>
<a class="item link-action" href data-url="{{AppSubUrl}}/user/logout">
{{svg "octicon-sign-out"}}
{{ctx.Locale.Tr "sign_out"}}
</a>
</div><!-- end content avatar menu -->
</div><!-- end dropdown avatar menu -->
{{else}}
{{if .ShowRegistrationButton}}
<a class="item{{if .PageIsSignUp}} active{{end}}" href="{{AppSubUrl}}/user/sign_up">
{{svg "octicon-person"}}
<span class="tw-ml-1">{{ctx.Locale.Tr "register"}}</span>
</a>
{{end}}
<a class="item{{if .PageIsSignIn}} active{{end}}" rel="nofollow" href="{{AppSubUrl}}/user/oauth2/KoonFamily{{if not .PageIsSignIn}}?redirect_to={{.CurrentURL}}{{end}}">
{{svg "octicon-sign-in"}}
<span class="tw-ml-1">{{ctx.Locale.Tr "sign_in"}}</span>
</a>
{{end}}
</div><!-- end full right menu -->
{{if $activeStopwatch}}
<div class="active-stopwatch-popup tippy-target">
<div class="tw-flex tw-items-center tw-gap-2 tw-p-3">
<a class="stopwatch-link tw-flex tw-items-center tw-gap-2 muted" href="{{$activeStopwatch.IssueLink}}">
{{svg "octicon-issue-opened" 16}}
<span class="stopwatch-issue">{{$activeStopwatch.RepoSlug}}#{{$activeStopwatch.IssueIndex}}</span>
</a>
<div class="tw-flex tw-gap-1">
<form class="stopwatch-commit form-fetch-action" method="post" action="{{$activeStopwatch.IssueLink}}/times/stopwatch/toggle">
{{.CsrfTokenHtml}}
<button
type="submit"
class="ui button mini compact basic icon tw-mr-0"
data-tooltip-content="{{ctx.Locale.Tr "repo.issues.stop_tracking"}}"
>{{svg "octicon-square-fill"}}</button>
</form>
<form class="stopwatch-cancel form-fetch-action" method="post" action="{{$activeStopwatch.IssueLink}}/times/stopwatch/cancel">
{{.CsrfTokenHtml}}
<button
type="submit"
class="ui button mini compact basic icon tw-mr-0"
data-tooltip-content="{{ctx.Locale.Tr "repo.issues.cancel_tracking"}}"
>{{svg "octicon-trash"}}</button>
</form>
</div>
</div>
</div>
{{end}}
</nav>

View File

@@ -0,0 +1,10 @@
<style>
:root {
--color-primary: #a68746;
--color-primary-dark: #a68746;
}
img.ui.avatar {
border-radius: 100%;
}
</style>

View File

@@ -0,0 +1,38 @@
{ ... }: {
flake.nixosModules.koonArkServiceHome = { pkgs, ... }: {
virtualisation.oci-containers = let
hass_config = pkgs.writeText "configuration.yaml" ''
# Discovery
default_config:
# Web Server configuration
http:
server_host: 127.0.0.1
use_x_forwarded_for: true
trusted_proxies: 127.0.0.1
sonos:
media_player:
hosts:
- 10.0.0.77
- 10.0.0.186
'';
in {
backend = "podman";
containers.homeassistant = {
volumes = [
"home-assistant:/config"
# "/data/docker/hass:/config"
"${hass_config}:/config/configuration.yaml"
# "/run/secrets/home-assistant:/config/secrets.yaml"
];
environment.TZ = "America/New_York";
image =
"ghcr.io/home-assistant/home-assistant:stable"; # Warning: if the tag does not change, the image will not be updated
extraOptions = [ "--network=host" ];
};
};
networking.firewall.allowedTCPPorts = [ 1400 ];
};
}

View File

@@ -0,0 +1,123 @@
{ ... }: {
flake.nixosModules.koonArkServicePhotos = { config, lib, pkgs, ... }:
let
oauthName = "KoonFamily";
in {
sops = {
templates = {
"immich-config.json" = {
content = builtins.toJSON {
passwordLogin.enabled = false;
# We will do this ourselves
backup.database.enabled = false;
oauth = {
enabled = true;
autoLaunch = true;
autoRegister = true;
buttonText =
lib.strings.concatStrings [ "Login To " oauthName ];
clientId = config.sops.placeholder."oauth/photos/clientId";
clientSecret = config.sops.placeholder."oauth/photos/clientSecret";
issuerUrl = "https://auth.koon.us/.well-known/openid-configuration";
};
};
owner = config.users.users.immich.name;
mode = "0400";
restartUnits = [ "immich-server.service" "pocket-id.service" ];
};
};
};
services.immich = {
enable = true;
port = 2283;
environment.IMMICH_CONFIG_FILE = config.sops.templates."immich-config.json".path;
accelerationDevices = null;
machine-learning.environment = {
HF_XET_CACHE = "/var/cache/immich/huggingface-xet";
};
};
users.users.immich = {
home = "/var/lib/immich";
createHome = true;
extraGroups = [ "video" "render" ];
};
hardware.graphics = {
enable = true;
extraPackages = with pkgs; [ intel-media-driver ];
};
environment.sessionVariables = { LIBVA_DRIVER_NAME = "iHD"; };
services.restic.backups = {
immich-local = {
repository = "/mnt/hdd/restic/immich";
passwordFile = config.sops.secrets.restic-password.path;
initialize = true;
paths = [ "/var/lib/immich/upload" "/var/backup/immich" ];
backupPrepareCommand = ''
mkdir -p /var/backup/immich
${pkgs.sudo}/bin/sudo ${pkgs.systemd}/bin/systemctl stop immich-server
${pkgs.sudo}/bin/sudo ${pkgs.systemd}/bin/systemctl stop immich-machine-learning
${pkgs.sudo}/bin/sudo -u postgres ${pkgs.postgresql}/bin/pg_dump \
--clean \
--if-exists \
--dbname=immich > /var/backup/immich/postgres.sql
'';
backupCleanupCommand = ''
${pkgs.sudo}/bin/sudo ${pkgs.systemd}/bin/systemctl start immich-server
${pkgs.sudo}/bin/sudo ${pkgs.systemd}/bin/systemctl start immich-machine-learning
'';
};
immich-remote = {
repository = "rest:http://m1:8000/immich";
passwordFile = config.sops.secrets.restic-password.path;
initialize = true;
paths = [ "/var/lib/immich/upload" "/var/backup/immich" ];
backupPrepareCommand = ''
mkdir -p /var/backup/immich
${pkgs.sudo}/bin/sudo ${pkgs.systemd}/bin/systemctl stop immich-server
${pkgs.sudo}/bin/sudo ${pkgs.systemd}/bin/systemctl stop immich-machine-learning
${pkgs.sudo}/bin/sudo -u postgres ${pkgs.postgresql}/bin/pg_dump \
--clean \
--if-exists \
--dbname=immich > /var/backup/immich/postgres.sql
'';
backupCleanupCommand = ''
${pkgs.sudo}/bin/sudo ${pkgs.systemd}/bin/systemctl start immich-server
${pkgs.sudo}/bin/sudo ${pkgs.systemd}/bin/systemctl start immich-machine-learning
'';
};
};
environment.systemPackages = with pkgs;
let
scripts = with pkgs; {
restore_immich_pg = writeShellScriptBin "restore_immich_pg" ''
${pkgs.sudo}/bin/sudo -u postgres psql --dbname=immich < /var/backup/immich/postgres.sql
'';
restore_immich = writeShellScriptBin "restore_immich" ''
${pkgs.sudo}/bin/sudo ${pkgs.systemd}/bin/systemctl stop immich-server
${pkgs.sudo}/bin/sudo ${pkgs.systemd}/bin/systemctl stop immich-machine-learning
${pkgs.sudo}/bin/sudo ${restic}/bin/restic -r /mnt/hdd/restic/immich restore latest --target /
${scripts.restore_immich_pg}/bin/restore_immich_pg
${pkgs.sudo}/bin/sudo ${pkgs.systemd}/bin/systemctl start immich-server
${pkgs.sudo}/bin/sudo ${pkgs.systemd}/bin/systemctl start immich-machine-learning
'';
};
in [ scripts.restore_immich_pg scripts.restore_immich ];
};
}

View File

@@ -0,0 +1,11 @@
{ ... }: {
flake.nixosModules.koonArkServiceRadicale = { ... }: {
services.radicale = {
enable = true;
settings = {
auth.type = "none";
server.hosts = [ "0.0.0.0:5232" ];
};
};
};
}

View File

@@ -0,0 +1,50 @@
{ ... }: {
flake.nixosModules.koonArkServiceWakapi = { config, ... }: {
nixpkgs.overlays = [
(final: prev: let
version = "2.15.0";
in {
wakapi = (prev.buildGoModule.override { go = prev.go_1_25; }) {
pname = "wakapi";
version = version;
src = final.fetchFromGitHub {
owner = "k2on";
repo = "wakapi";
rev = "koon-fork";
hash = "sha256-FYGtoJmbqUD02/JKvON1RqpjkrDkAOkfPwMAUZ2MSE4=";
};
vendorHash = "sha256-912x6LwitYXdjWpP75Xoc56JXadeLQZuESSyLoaJcU0=";
excludedPackages = [ "scripts" ];
postPatch = ''echo ${version} > version.txt'';
ldflags = [ "-s" "-w" ];
passthru = {
nixos = prev.nixosTests.wakapi;
updateScript = prev.nix-update-script { };
};
meta = prev.wakapi.meta // {
version = version;
mainProgram = "wakapi";
};
};
})
];
services.wakapi = {
enable = true;
# passwordSaltFile = config.sops.secrets."waka-password-salt".path;
settings = {
server.port = 3006;
app.avatar_url_template = "https://auth.koon.us/api/users/fbffa48a-faf7-4230-a89f-0da184f5948c/profile-picture.png";
};
};
};
}

View File

@@ -0,0 +1,34 @@
{ self, ... }: {
flake.nixosModules.koonArkSops = { config, ... }: {
sops = {
age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
defaultSopsFile = "${self}/secrets/koon/ark/default.yaml";
validateSopsFiles = false;
secrets = {
"restic-password" = {};
"tunnel-credentials" = {};
"admin-password" = {};
"pocket-id-encryption-key" = {
owner = config.services.pocket-id.user;
};
"waka-password-salt" = {
owner = config.users.users.wakapi.name;
};
"oauth/photos/clientId" = {};
"oauth/photos/clientSecret" = {};
"oauth/git/clientId" = {
owner = config.services.gitea.user;
};
"oauth/git/clientSecret" = {
owner = config.services.gitea.user;
};
};
};
};
}

View File

@@ -0,0 +1,13 @@
{ ... }: {
flake.nixosModules.koonArkUser = { pkgs, config, ... }: {
sops.secrets.admin-password.neededForUsers = true;
users.mutableUsers = false;
users.users.admin = {
isNormalUser = true;
extraGroups = [ "wheel" ];
hashedPasswordFile = config.sops.secrets.admin-password.path;
packages = with pkgs; [ tree vim tmux restic ];
};
};
}

View File

@@ -8,4 +8,3 @@
];
};
}