9f7431bb36
- Update virtualcam website and API to latest repository revisions. - Add Google OAuth client ID and secrets decryption for Google auth. - Enable Shkeeper BTC/USD payment processing (`BYPASS_PAYMENTS=false`) and add service dependencies. - Update Caddy `admin_gate` IP access rules and remove redundant `admin_gate` import from app proxy. - Configure weekly automatic Nix garbage collection (`--delete-older-than 14d`) and nix store optimization.
288 lines
12 KiB
Nix
288 lines
12 KiB
Nix
{
|
|
pkgs,
|
|
lib,
|
|
unstablePkgs,
|
|
...
|
|
}: let
|
|
rev = "6e351ddc732421eb775eb9843ccd2389d6525edd";
|
|
apiRev = "390878d126185b21a14479f85c70134a155c61c6";
|
|
|
|
# Private repositories are fetched over SSH (port 2222). nix-daemon runs as
|
|
# root and uses /root/.ssh (identity materialized by the git-ssh-key unit),
|
|
# so the source ends up in the store without any Nix-native credentials.
|
|
src = builtins.fetchGit {
|
|
url = "ssh://git@git.severijnse.eu:2222/jory/virtualcam-website.git";
|
|
rev = rev;
|
|
};
|
|
|
|
srcPatched = pkgs.applyPatches {
|
|
name = "virtualcam-website-patched";
|
|
src = src;
|
|
patches = [./virtualcam-checkout.patch];
|
|
};
|
|
|
|
apiSrc = builtins.fetchGit {
|
|
url = "ssh://git@git.severijnse.eu:2222/jory/virtualcam-api.git";
|
|
rev = apiRev;
|
|
};
|
|
|
|
# Build the Next.js app entirely in Nix (offline npm deps from the lockfile).
|
|
app = unstablePkgs.buildNpmPackage {
|
|
pname = "virtualcam-website";
|
|
version = "0.1.0";
|
|
src = srcPatched;
|
|
npmDepsHash = "sha256-GodWQKtOtsLOjjiwzxun+wTPhtvjR2uCV91n+wiZHw4=";
|
|
nodejs = unstablePkgs.nodejs;
|
|
|
|
buildPhase = ''
|
|
runHook preBuild
|
|
npx prisma generate
|
|
npm run build
|
|
runHook postBuild
|
|
'';
|
|
|
|
installPhase = ''
|
|
runHook preInstall
|
|
mkdir -p $out
|
|
cp -r .next node_modules public prisma src package.json package-lock.json \
|
|
prisma.config.ts next.config.ts tsconfig.json postcss.config.mjs $out/
|
|
runHook postInstall
|
|
'';
|
|
|
|
APP_URL = "https://virtualcam.severijnse.eu";
|
|
GOOGLE_CLIENT_ID = "754775011707-c699m092tv3icmovhk5qa106v3q6eh7c.apps.googleusercontent.com";
|
|
# Only used to satisfy prisma generate / next build metadata resolution.
|
|
DATABASE_URL = "postgresql://virtualcam@localhost/virtualcam?host=/run/postgresql&schema=public";
|
|
# Use the nixpkgs-bundled Prisma engine so the sandboxed offline build does
|
|
# not need to download it from binaries.prisma.sh. Version matches ^7.8.0.
|
|
PRISMA_SCHEMA_ENGINE_BINARY = "${unstablePkgs.prisma-engines}/bin/schema-engine";
|
|
NEXT_TELEMETRY_DISABLED = "1";
|
|
CI = "true";
|
|
};
|
|
|
|
# License validation / admin API (Go/Fiber), serving on host port 3004.
|
|
# go.mod demands go 1.26.5 but the pinned nixpkgs only has 1.26.4; the code
|
|
# uses nothing newer, so the directive is relaxed to match the toolchain.
|
|
api = unstablePkgs.buildGoModule {
|
|
pname = "virtualcam-api";
|
|
version = "0.1.0";
|
|
src = apiSrc;
|
|
vendorHash = "sha256-uvHClXHw9ycoIf6qBZmV2O3CSyIxCgnSPCSALM07qg8=";
|
|
go = unstablePkgs.go_1_26;
|
|
postPatch = ''
|
|
sed -i 's/^go 1\.26\.5$/go 1.26.4/' go.mod
|
|
'';
|
|
};
|
|
|
|
dbUrl = "postgresql://virtualcam@localhost/virtualcam?host=/run/postgresql&schema=public";
|
|
# lib/pq parses the DSN differently from node-postgres: a hostname in the URL
|
|
# authority wins over a `host=` query param (so it would go over TCP and fail
|
|
# password auth), and lib/pq rejects unknown URL params like `schema`. Use a
|
|
# keyword DSN: unix-socket + peer auth + no SSL, matching the OS user.
|
|
apiDbUrl = "host=/run/postgresql user=virtualcam dbname=virtualcam sslmode=disable";
|
|
|
|
# Root-only runtime environment file holding the secrets both services need.
|
|
secretsFile = ../../secrets/secrets.yaml;
|
|
envFile = "/var/lib/virtualcam/environment";
|
|
|
|
# Materialize the license signing key and admin token from sops into a
|
|
# root-only file (0600). systemd reads environmentFiles before dropping
|
|
# privileges, so the service users never need to read it themselves.
|
|
writeSecrets = pkgs.writeShellScript "virtualcam-write-secrets" ''
|
|
set -euo pipefail
|
|
install -d -o virtualcam -g virtualcam -m 0750 /var/lib/virtualcam
|
|
: > "${envFile}"
|
|
chmod 0600 "${envFile}"
|
|
${pkgs.sops}/bin/sops --decrypt --input-type yaml --output-type yaml ${secretsFile} |
|
|
${pkgs.gnused}/bin/sed -nE \
|
|
's/^virtualcam_license_signing_key: (.*)/LICENSE_SIGNING_KEY=\1/p; s/^virtualcam_admin_token: (.*)/ADMIN_TOKEN=\1/p; s/^virtualcam_smtp_pass: (.*)/SMTP_PASS=\1/p; s/^virtualcam_google_OAuth_secret: (.*)/GOOGLE_CLIENT_SECRET=\1/p; s/^shkeeper_api_key: (.*)/SHKEEPER_API_KEY=\1/p' \
|
|
>> "${envFile}"
|
|
'';
|
|
|
|
# Materialize the nix-daemon's SSH identity so private-repo fetches keep
|
|
# working after the one-off bootstrap copy in /root/.ssh.
|
|
writeGitSshKey = pkgs.writeShellScript "git-ssh-key" ''
|
|
set -euo pipefail
|
|
install -d -m 0700 /root/.ssh
|
|
${pkgs.sops}/bin/sops --decrypt --extract '["git_ssh_key_b64"]' \
|
|
--input-type yaml --output-type yaml ${secretsFile} |
|
|
${pkgs.coreutils}/bin/base64 -d > /root/.ssh/id_ed25519
|
|
chmod 0600 /root/.ssh/id_ed25519
|
|
cat > /root/.ssh/known_hosts <<'EOF'
|
|
git.severijnse.eu ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAzIVo9Jdp8kwnWmTn26Fj68baJjwDphYw/0HTH5BzYY
|
|
[git.severijnse.eu]:2222 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDGcPKlvdxApYRCZuQxrljEdETrxAu0JsEJ+28zFaMfA3P69J9vEkqqAZZ0Uq3rtE8ZqPBxeDh7JYn2XChb+QVGjOHVWMCWqcdD43ws26eY91aHGYnO2kW+sKSxIh803tdifIp2q8VRnaHVXcLHLx2rmwmvqIfMLnEEShxAt4Q846DianBoA7S8heoBvMLi8AAO09sVEgMPWm8ULkoRq0poHslsKkdYKlWzecP85tPo/tlCEjQq5cC5+xidH9TS4ILxVIXGUfErZzbWZq5LtaRmqiU5U1+zzdb83oJvHFtTZ8wHwZX+VE4jZzKw7wdFPk+dJZp/3ZAVva6fLSjlpyyDFNWMcf2/18Gs6YioD1n2fr1dXruZiXNMnI19Dzyp+8yWIPbAeWyVx3BoCrcjIApEDMLMo7ut0l6RfNy80hYpY91VBrpsjSRLB4uCVDsqhTLPTrFUlu+9YY9lSHEUVLRylOLK54TbY089hbHOB/UdpyCiXpI0ds3ww1UmnDQSFjwNknc+wTXRvzEz+VsX2nAzfO+5NWq2jHNn4wH6GuAinAxSVg1A7Ub9VSmPelRiTzK6ZnplPm762MxcPFmo+y/5cszumUwypcyTS8MJIC57S/kabfsnjdt97K+9eT9X2LDZ2uIQgDS8qEgvbnvfXVmMoqakgBLYG7Embqc8BDzNsQ==
|
|
EOF
|
|
chmod 0600 /root/.ssh/known_hosts
|
|
test -f /root/.ssh/id_ed25519.pub || ${pkgs.openssh}/bin/ssh-keygen -y -f /root/.ssh/id_ed25519 > /root/.ssh/id_ed25519.pub
|
|
'';
|
|
|
|
# The repo's seed uses tsx (a devDependency buildNpmPackage drops) plus the
|
|
# "@/..." path alias. nixpkgs ships tsx, which honours tsconfig paths, so we
|
|
# add it to the service path rather than working around the missing dep.
|
|
seed = pkgs.writeShellScript "virtualcam-seed" ''
|
|
set -euo pipefail
|
|
export DATABASE_URL="${dbUrl}"
|
|
export PRISMA_SCHEMA_ENGINE_BINARY="${unstablePkgs.prisma-engines}/bin/schema-engine"
|
|
export HOME=/var/lib/virtualcam
|
|
cd ${app}
|
|
tsx prisma/seed.ts
|
|
'';
|
|
|
|
# One shared PostgreSQL server (existing system postgres). Each service gets
|
|
# its own database + role. virtualcam authenticates over the Unix socket via
|
|
# peer auth: the systemd services run as OS user `virtualcam`, which matches
|
|
# the database role `virtualcam`, so no password is stored anywhere.
|
|
migrate = pkgs.writeShellScript "virtualcam-migrate" ''
|
|
set -euo pipefail
|
|
export DATABASE_URL="${dbUrl}"
|
|
# Use the local Prisma engine; no network download needed at runtime.
|
|
export PRISMA_SCHEMA_ENGINE_BINARY="${unstablePkgs.prisma-engines}/bin/schema-engine"
|
|
cd ${app}
|
|
./node_modules/.bin/prisma migrate deploy
|
|
'';
|
|
in {
|
|
users = {
|
|
users.virtualcam = {
|
|
isSystemUser = true;
|
|
group = "virtualcam";
|
|
description = "virtualcamera services user";
|
|
};
|
|
groups.virtualcam = {};
|
|
};
|
|
|
|
services.postgresql = {
|
|
ensureDatabases = ["virtualcam"];
|
|
ensureUsers = [
|
|
{
|
|
name = "virtualcam";
|
|
ensureDBOwnership = true;
|
|
}
|
|
];
|
|
};
|
|
|
|
systemd = {
|
|
services = {
|
|
git-ssh-key = {
|
|
description = "Materialize nix-daemon git SSH key from sops";
|
|
wantedBy = ["multi-user.target"];
|
|
environment.SOPS_AGE_KEY_FILE = "/etc/age/keys.txt";
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
Environment = ["HOME=/root"];
|
|
ExecStart = "${writeGitSshKey}";
|
|
};
|
|
};
|
|
|
|
virtualcam-secrets = {
|
|
description = "Materialize virtualcam secrets from sops";
|
|
wantedBy = ["multi-user.target"];
|
|
# The age key lives in /etc/age/keys.txt; the service must know where it
|
|
# is and needs a HOME for age to report its user config directory.
|
|
environment.SOPS_AGE_KEY_FILE = "/etc/age/keys.txt";
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
Environment = ["HOME=/root"];
|
|
ExecStart = "${writeSecrets}";
|
|
};
|
|
};
|
|
|
|
virtualcam-migrate = {
|
|
description = "Virtualcam Prisma migrations";
|
|
after = ["postgresql.service"];
|
|
requires = ["postgresql.service"];
|
|
wantedBy = ["multi-user.target"];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
User = "virtualcam";
|
|
Group = "virtualcam";
|
|
StateDirectory = "virtualcam";
|
|
StateDirectoryMode = "0750";
|
|
ExecStart = "${migrate}";
|
|
};
|
|
};
|
|
|
|
virtualcam-seed = {
|
|
description = "Virtualcam catalog seed";
|
|
after = ["virtualcam-migrate.service"];
|
|
requires = ["virtualcam-migrate.service"];
|
|
wantedBy = ["multi-user.target"];
|
|
path = [unstablePkgs.nodejs unstablePkgs.tsx];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
User = "virtualcam";
|
|
Group = "virtualcam";
|
|
ExecCondition = "!/var/lib/virtualcam/.seeded";
|
|
ExecStart = "${seed}";
|
|
ExecStartPost = "${pkgs.coreutils}/bin/touch /var/lib/virtualcam/.seeded";
|
|
StateDirectory = "virtualcam";
|
|
StateDirectoryMode = "0750";
|
|
};
|
|
};
|
|
|
|
virtualcam = {
|
|
description = "Virtualcamera website (Next.js)";
|
|
after = ["postgresql.service" "podman-shkeeper.service" "virtualcam-migrate.service" "virtualcam-seed.service" "virtualcam-secrets.service"];
|
|
requires = ["postgresql.service" "podman-shkeeper.service" "virtualcam-migrate.service" "virtualcam-seed.service" "virtualcam-secrets.service"];
|
|
wantedBy = ["multi-user.target"];
|
|
path = [unstablePkgs.nodejs];
|
|
serviceConfig = {
|
|
User = "virtualcam";
|
|
Group = "virtualcam";
|
|
WorkingDirectory = "${app}";
|
|
EnvironmentFile = [envFile];
|
|
ExecStart = "${app}/node_modules/.bin/next start -p 3001 -H 127.0.0.1";
|
|
Restart = "on-failure";
|
|
RestartSec = 5;
|
|
StateDirectory = "virtualcam";
|
|
StateDirectoryMode = "0750";
|
|
Environment = [
|
|
"DATABASE_URL=${dbUrl}"
|
|
"APP_URL=https://virtualcam.severijnse.eu"
|
|
"GOOGLE_CLIENT_ID=754775011707-c699m092tv3icmovhk5qa106v3q6eh7c.apps.googleusercontent.com"
|
|
"PAYMENTS_MODE=shkeeper"
|
|
"BYPASS_PAYMENTS=false"
|
|
"SHKEEPER_URL=https://pay.severijnse.eu"
|
|
"SHKEEPER_CRYPTO=BTC"
|
|
"SHKEEPER_FIAT=USD"
|
|
"ADMIN_EMAILS=jory@severijnse.eu"
|
|
"SMTP_HOST=mail.severijnse.eu"
|
|
"SMTP_PORT=587"
|
|
"SMTP_USER=no-reply@severijnse.eu"
|
|
"SMTP_FROM=no-reply@severijnse.eu"
|
|
"NODE_ENV=production"
|
|
"NEXT_TELEMETRY_DISABLED=1"
|
|
"HOME=/var/lib/virtualcam"
|
|
];
|
|
};
|
|
};
|
|
|
|
virtualcam-api = {
|
|
description = "Virtualcamera license API (Go/Fiber)";
|
|
after = ["postgresql.service" "podman-shkeeper.service" "virtualcam-migrate.service" "virtualcam-secrets.service"];
|
|
requires = ["postgresql.service" "podman-shkeeper.service" "virtualcam-migrate.service" "virtualcam-secrets.service"];
|
|
wantedBy = ["multi-user.target"];
|
|
serviceConfig = {
|
|
User = "virtualcam";
|
|
Group = "virtualcam";
|
|
WorkingDirectory = "${api}";
|
|
EnvironmentFile = [envFile];
|
|
ExecStart = "${api}/bin/virtualcam-api";
|
|
Restart = "on-failure";
|
|
RestartSec = 5;
|
|
StateDirectory = "virtualcam";
|
|
StateDirectoryMode = "0750";
|
|
Environment = [
|
|
"PORT=3004"
|
|
"DATABASE_URL=${apiDbUrl}"
|
|
"CORS_ORIGINS=https://virtualcam.severijnse.eu"
|
|
"HOME=/var/lib/virtualcam"
|
|
];
|
|
};
|
|
};
|
|
};
|
|
};
|
|
|
|
# Served behind Caddy (virtualHosts wired in caddy.nix).
|
|
networking.firewall.allowedTCPPorts = [];
|
|
}
|