Compare commits

..

No commits in common. "main" and "rememberme" have entirely different histories.

70 changed files with 143 additions and 29241 deletions

View file

@ -6,7 +6,6 @@
./../shared/applications/server/nginx.nix
./../shared/applications/server/postgresql.nix # INCLUDES DATABASE BACKUPS
./../shared/applications/server/restic.nix # EXTERNAL BACKUP
# ./../shared/applications/server/podman.nix
./../shared/applications/state/postgresql.nix
./../shared/applications/state/ssh.nix
@ -24,15 +23,9 @@
./gerd/services/wger
./gerd/services/searx.nix
./gerd/services/miniflux.nix
./gerd/services/matrix
./gerd/services/uptime-kuma.nix
./gerd/services/rallly
./gerd/services/notify
./gerd/services/drasl.nix
./gerd/services/drtvrss.nix
./gerd/services/vikunja.nix
./gerd/services/monitoring
./gerd/services/element.nix
./gerd/services/matrix-synapse.nix
];
networking.hostName = "gerd";
@ -43,15 +36,12 @@
disks = {
disk = "/dev/sda";
pools.rpool.datasets = {
# zfs create -o quota=1G rpool/safe/svcs/service-name
"safe/svcs/forgejo" = { mountpoint = "/srv/forgejo"; extra.options.quota = "5G"; };
"safe/svcs/hedgedoc" = { mountpoint = "/srv/hedgedoc"; extra.options.quota = "5G"; };
"safe/svcs/nextcloud" = { mountpoint = "/srv/nextcloud"; extra.options.quota = "5G"; };
"safe/svcs/stalwart" = { mountpoint = "/srv/stalwart"; extra.options.quota = "5G"; };
"safe/svcs/synapse" = { mountpoint = "/srv/synapse"; extra.options.quota = "5G"; };
"safe/svcs/wger" = { mountpoint = "/srv/wger"; extra.options.quota = "5G"; };
"safe/svcs/prometheus" = { mountpoint = "/srv/prometheus"; extra.options.quota = "5G"; };
"safe/svcs/postgresql" = { mountpoint = "/srv/postgresql"; extra.options.quota = "5G"; };
"backup/postgresql" = { mountpoint = "/media/backup/postgresqlbackup"; extra.options.quota = "5G"; };
};
@ -64,7 +54,7 @@
platforms.hetzner = {
enable = true;
network.address = [
"65.108.221.240"
"65.108.221.240/32"
"2a01:4f9:c012:743e::1/64"
];
};

View file

@ -10,13 +10,8 @@ let
in {
services.authelia.instances.main = {
enable = true;
package = pkgs.authelia.override {
authelia-web = pkgs.authelia.passthru.web.overrideAttrs (old: {
postPatch = old.postPatch + ''
substituteInPlace src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx \
--replace-fail "const [rememberMe, setRememberMe] = useState(false)" "const [rememberMe, setRememberMe] = useState(true)"
'';
});
package = pkgs.authelia.overrideAttrs {
patches = [ ./remember_me_by_default.patch ];
};
environmentVariables.AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE = config.age.secrets.lldap-bind-user-pass.path;
@ -34,10 +29,6 @@ in {
authelia_url = "https://${svc_domain}";
} ];
# setup redis for sessions, otherwise it's in-memory, and everyone
# has to login again each time authelia is restarted
session.redis.host = "${config.services.redis.servers.authelia.unixSocket}";
server.address = "tcp://127.0.0.1:${builtins.toString port}";
# totp - disable for now, as it requires email server
@ -93,13 +84,6 @@ in {
};
};
# setup redis for persisting session
# across reboots
services.redis.servers.authelia = {
enable = true;
user = authelia_user;
};
# setup lldap user for authelia that can send emails
services.lldap.provision.users = config.mine.shared.lib.ldap.mkScope (lconfig: llib: {
authelia = llib.mkProvisionUserSystem "authelia" config.age.secrets.authelia-smtp-password.path;

View file

@ -3,7 +3,4 @@
./authelia.nix
./authelia-nginx.nix
];
# generate new authelia client secret like so
# authelia crypto hash generate pbkdf2 --variant sha512 --random --random.length 72 --random.charset rfc3986
}

View file

@ -0,0 +1,13 @@
diff --git a/web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx b/web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx
index 6bbeda992..d08ade4c3 100644
--- a/web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx
+++ b/web/src/views/LoginPortal/FirstFactor/FirstFactorForm.tsx
@@ -42,7 +42,7 @@ const FirstFactorForm = function (props: Props) {
const loginChannel = useMemo(() => new BroadcastChannel<boolean>("login"), []);
- const [rememberMe, setRememberMe] = useState(false);
+ const [rememberMe, setRememberMe] = useState(true);
const [username, setUsername] = useState("");
const [usernameError, setUsernameError] = useState(false);
const [password, setPassword] = useState("");

View file

@ -1,153 +0,0 @@
{ config, pkgs, lib, ... }:
let
sources = import ./../../../shared/sources;
flake-compat = sources.flake-compat;
drasl = import flake-compat { src = sources.drasl; };
svc_domain = "drasl.${config.mine.shared.settings.domain}";
port = 25585;
draslOIDCName = "Authelia";
in {
imports = [
drasl.defaultNix.nixosModules.drasl
];
services.drasl = {
enable = true;
settings = {
ApplicationOwner = config.mine.shared.settings.brand;
Domain = svc_domain;
BaseURL = "https://${svc_domain}";
ListenAddress = "localhost:${builtins.toString port}";
# all ldap admins in group `drasl-admin` are default admins here
DefaultAdmins = config.mine.shared.lib.ldap.mkScope (lconfig: llib: let
admins = lib.forEach (
lib.filter
(v: lib.elem lconfig.groups.drasl_admin (v.groups or []))
(lib.attrValues lconfig.provision.users)
) (v: v.mail);
in admins);
# allow importing players
ImportExistingPlayer = {
Allow = true;
Nickname = "Mojang";
AccountURL = "https://api.mojang.com";
SessionURL = "https://sessionserver.mojang.com";
SetSkinURL = "https://www.minecraft.net/msaprofile/mygames/editskin";
RequireSkinVerification = false; # TODO: should maybe be changed to true in the future
};
RegistrationExistingPlayer.Allow = true;
# only allow loging using OIDC
CreateNewPlayer.Allow = true;
RegistrationNewPlayer.Allow = true;
AllowPasswordLogin = false;
# configure OIDC
RegistrationOIDC = [{
Name = draslOIDCName;
Issuer = "https://${config.mine.shared.settings.authelia.domain}";
ClientID = "drasl";
# ClientSecret = "<gotten-from-env>";
PKCE = true;
RequireInvite = false;
AllowChoosingPlayerName = true;
}];
};
};
# secrets
systemd.services.drasl.serviceConfig.EnvironmentFile = config.age.secrets.drasl-env.path;
systemd.services.drasl.restartTriggers = [ config.age.secrets.drasl-env.path ]; # unsure if this works
# setup for oidc
services.authelia.instances.main.settings.identity_providers.oidc.clients = [{
client_id = "drasl";
client_name = "Drasl";
client_secret = "$pbkdf2-sha512$310000$x8USzEVE/HW7/tiYtgTFaA$POg.0gZuWfHTuO0Z2Dd1GZ.T2813IAG.nWnwOarHGBz7aCGI1rdRoaS7gZ9V6bnTWWiFL/lqk5NFoqdZn94neg";
consent_mode = "implicit";
redirect_uris = [ "${config.services.drasl.settings.BaseURL}/web/oidc-callback/${draslOIDCName}" ];
scopes = [
"openid"
"profile"
"email"
];
}];
# nginx
services.nginx.virtualHosts."${svc_domain}" = let
httpListenOn = "http://localhost:${builtins.toString port}";
in config.mine.shared.lib.authelia.mkProtectedWebsite {
forceSSL = true;
enableACME = true;
locations."/" = config.mine.shared.lib.authelia.mkProtectedLocation {
proxyPass = httpListenOn;
};
# needed for clients to auth
locations."/authlib-injector".proxyPass = httpListenOn;
# needed for server to auth
locations."/auth".proxyPass = httpListenOn;
locations."/account".proxyPass = httpListenOn;
locations."/session".proxyPass = httpListenOn;
locations."/services".proxyPass = httpListenOn;
# skins
locations."/web/texture".proxyPass = httpListenOn;
};
# persistence
environment.persistence.root.directories = [
{ directory = "/var/lib/private/drasl"; mode = "0700"; }
];
# meta
mine.shared.meta.drasl = rec {
name = "Drasl";
description = ''
Yggdrasil-compatible API server for Minecraft, which can be used instead of the official Minecraft authentication server.
This means that we do not require Mojangs servers, to authenticate with any server managed by ${config.mine.shared.settings.brand}.
It is possible to login with OIDC on Drasl, and then import your Mojang player into Drasl.
'';
url = "https://${svc_domain}";
package = let
pkg = config.services.drasl.package;
in {
name = pkg.pname;
version = pkg.version;
meta = with lib; {
description = "Yggdrasil-compatible API server for Minecraft";
license = lib.licenses.gpl3Only;
homepage = "https://github.com/unmojang/drasl";
platforms = platforms.all;
};
};
};
# TODO(eyJhb): this should not be placed here
mine.shared.meta.minecraft = rec {
name = "Minecraft";
description = ''We're running a vanilla Minecraft hosted externally by a member'';
url = "mcvanilla.${config.mine.shared.settings.domain}";
package = let
pkg = pkgs.minecraft-server;
in {
name = pkg.pname;
version = "1.21.5";
meta = pkg.meta;
};
};
}

View file

@ -1,55 +0,0 @@
{ config, lib, ... }:
let
sources = import ./../../../shared/sources;
flake-compat = sources.flake-compat;
drtvrss = import flake-compat { src = sources.drtvrss; };
svc_domain = "drtv.${config.mine.shared.settings.domain}";
port = 8125;
in {
imports = [
# (builtins.trace drtvrss.defaultNix.nixosModules.x86_64-linux.default drtvrss.defaultNix.nixosModules.default)
drtvrss.defaultNix.nixosModules.x86_64-linux.default
];
services.drtvrss = {
enable = true;
host = "127.0.0.1:${builtins.toString port}";
base_url = "https://${svc_domain}";
klagemail = "rasmus@rend.al";
recommended_shows = [ "matador_130149" ];
};
systemd.services.drtvrss.confinement.enable = lib.mkForce false;
# nginx
services.nginx.virtualHosts."${svc_domain}" = {
forceSSL = true;
enableACME = true;
locations."/".proxyPass = "http://localhost:${builtins.toString port}";
};
# meta
mine.shared.meta.drtvrss = rec {
name = "DRTV RSS";
description = ''
Alternative frontend for DRTV, whithout the requirement to login + the ability to generate RSS feeds of your favorite shows.
'';
url = "https://${svc_domain}";
package = {
name = "drtvrss";
version = "v0.0.1";
meta = with lib; {
description = "DRTVRSS";
license = licenses.agpl3Only;
homepage = "https://github.com/RasmusRendal/drtvrss";
platforms = platforms.all;
};
};
};
}

View file

@ -99,7 +99,6 @@ in {
client_id = "forgejo";
client_name = "Forgejo";
client_secret = "$pbkdf2-sha512$310000$cOGtLwMHyfugAJCIiUUjfQ$ao7zC8QB1m8aTGNf1dxYbRAPivZ0G1eaJ4bNFVfJiTFZX06U5baBjT0emvoaeFHXMFbYHzorb2/8vxnY/D0b5Q";
consent_mode = "implicit";
redirect_uris = [ "https://${config.mine.shared.settings.forgejo.domain}/user/oauth2/${AUTHELIA_AUTH_NAME}/callback" ];
scopes = [
"openid"

View file

@ -176,8 +176,6 @@ in {
groupOfUniqueNames = "groupOfUniqueNames";
};
provision = config.services.lldap.provision;
users = {
admin = "admin";
# bind = "bind_user";
@ -248,39 +246,25 @@ in {
mkProvisionEmail = name: "${name}@${config.mine.shared.settings.domain}";
mkProvisionUserNormal = name: config.mine.shared.lib.ldap.mkScope (lconfig: llib: {
user_id = name;
display_name = name; # required for nextcloud
membermail = mkProvisionEmail name;
mail = "env:EMAIL_${lib.toUpper name}";
groups = [ lconfig.groups.member ];
membermaildiskquota = 100*1024*1024; # mb
nextcloudquota = 5*1024*1024; # mb
});
mkProvisionUserSystem = name: password_file: config.mine.shared.lib.ldap.mkScope (lconfig: llib: {
user_id = name;
membermail = mkProvisionEmail name;
mail = mkProvisionEmail name;
password = "file:${password_file}";
groups = [ lconfig.groups.system_mail lconfig.groups.system_service ];
membermaildiskquota = 10*1024*1024; # mb
});
mkProvisionUserSystemExt = name: password_file: custom_attrs: lib.recursiveUpdate (config.mine.shared.lib.ldap.mkScope (lconfig: llib: {
user_id = name;
membermail = mkProvisionEmail name;
password = "file:${password_file}";
groups = [ lconfig.groups.system_mail lconfig.groups.system_service ];
membermaildiskquota = 10*1024*1024; # mb
})) custom_attrs;
mkProvisionUserAdmin = name: config.mine.shared.lib.ldap.mkScope (lconfig: llib: {
user_id = name;
display_name = name; # required for nextcloud
membermail = mkProvisionEmail name;
mail = mkProvisionEmail name;
groups = with lconfig.groups; [ admin nextcloud_admin grafana_admin drasl_admin member ];
groups = [ lconfig.groups.admin lconfig.groups.member ];
membermaildiskquota = 100*1024*1024; # mb
nextcloudquota = 100*1024*1024; # mb
});
};

View file

@ -162,6 +162,5 @@ in {
${pythonEnv}/bin/python -m bootstrap.main ${configFile}
'';
};
systemd.services.lldap.restartTriggers = [ configFile ];
};
}

View file

@ -12,23 +12,27 @@
provision = config.mine.shared.lib.ldap.mkScope (lconfig: llib: {
# users
users = {
# bind user
bind = {
user_id = "bind_user";
groups = [ lconfig.groups.password_manager lconfig.groups.strict_readonly ];
# normal users
testusername = {
membermail = "env:EMAIL_EMAIL0";
groups = [ config.services.lldap.provision.groups.system_mail.display_name ];
};
# system users - defined in each service
# should not be done here
user1 = llib.mkProvisionUserNormal "thief420";
# admin users
admin = llib.mkProvisionUserAdmin "admin";
eyjhb = llib.mkProvisionUserAdmin "eyjhb";
rasmus = llib.mkProvisionUserAdmin "rasmus";
# normal users
user1 = llib.mkProvisionUserNormal "thief420";
testusername = (llib.mkProvisionUserNormal "testusername") // { mail = "testusername@fricloud.dk"; };
# system users - defined in each service
# should not be done here
# bind user
bind = {
user_id = "bind_user";
groups = [ lconfig.groups.password_manager lconfig.groups.strict_readonly ];
};
};
# groups
@ -36,9 +40,6 @@
"base_member" = {};
"system_service" = {};
"system_mail" = {};
"nextcloud_admin" = {};
"drasl_admin" = {};
"grafana_admin" = {};
};
# attributes
@ -58,9 +59,6 @@
membermaildiskquota = {
attributeType = "INTEGER";
};
nextcloudquota = {
attributeType = "INTEGER";
};
};
});

View file

@ -74,12 +74,6 @@ in {
allowed_lifetime_min = "1d";
allowed_lifetime_max = "1y";
};
# automatically forget room on leave/kick/ban, and
# purge from db after X time
forget_rooms_on_leave = true;
forgotten_room_retention_period = "28d";
};
};
@ -128,7 +122,6 @@ in {
client_id = "synapse";
client_name = "Synapse";
client_secret = "$pbkdf2-sha512$310000$SmE9y.LA9lnzxNWL6CeWQA$zcrum.Rst9xQy/MKBI5i.UiUdSjx/F0ak65Z3vYk0w7/GMWIqXaW3GnE7bJQw6nHi5eZ2uhKHtW/DKp2TDVhbQ";
consent_mode = "implicit";
redirect_uris = [ "https://${svc_domain}/_synapse/client/oidc/callback" ];
scopes = [
"openid"

View file

@ -1,7 +0,0 @@
{
imports = [
./matrix-synapse.nix
./element.nix
./housecleaning.nix
];
}

View file

@ -1,36 +0,0 @@
{ config, pkgs, ... }:
{
# delete empty directories
# - https://github.com/element-hq/synapse/issues/7690
# - https://github.com/matrix-org/synapse/issues/7690
systemd.services.matrix-synapse.preStart =
''${pkgs.findutils}/bin/find ${config.services.matrix-synapse.dataDir}/media_store -empty -type d -delete'';
systemd.timers."matrix-synapse-housecleaning-empty-dirs" = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "Mon *-*-* 04:00:00";
Unit = config.systemd.services.matrix-synapse.name;
};
};
# vacuum database
systemd.services."matrix-synapse-psql-vacuum" = let
psqlUser = config.systemd.services.postgresql.serviceConfig.User;
dbName = config.services.matrix-synapse.settings.database.args.database;
in {
serviceConfig.User = psqlUser;
serviceConfig.RemainAfterExit = "yes";
script = ''${config.services.postgresql.package}/bin/psql -d ${dbName} -c "vacuum full"'';
};
systemd.timers."matrix-synapse-housecleaning-vacuum-db" = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "Mon *-*-* 04:00:00";
Unit = config.systemd.services.matrix-synapse-psql-vacuum.name;
};
};
}

View file

@ -1,19 +0,0 @@
{
imports = [
./grafana.nix
./prometheus.nix
./mon-postgres.nix
./mon-stalwart.nix
./mon-authelia.nix
./mon-matrix-synapse.nix
./mon-zfs.nix
./mon-miniflux.nix
./mon-hedgedoc.nix
./mon-forgejo.nix
./mon-uptime-kuma.nix
./mon-searx.nix
./mon-nextcloud.nix
./mon-node-exporter.nix
];
}

View file

@ -1,92 +0,0 @@
{ config, ... }:
let
svc_domain = "grafana.${config.mine.shared.settings.domain}";
auth_domain = config.mine.shared.settings.authelia.domain;
grafana_user = config.systemd.services.grafana.serviceConfig.User;
in {
services.grafana = {
enable = true;
settings = {
server = {
http_addr = "127.0.0.1";
http_port = 3010;
root_url = "https://${svc_domain}";
};
# only allow signun with oauth
auth.disable_login_form = true;
"auth.generic_oauth" = {
enabled = true;
name = "Authelia";
icon = "signin";
client_id = "grafana";
client_secret = "$__file{${config.age.secrets.grafana-authelia-secret.path}}";
scopes = "openid profile email groups";
empty_scopes = false;
auth_url = "https://${auth_domain}/api/oidc/authorization";
token_url = "https://${auth_domain}/api/oidc/token";
api_url = "https://${auth_domain}/api/oidc/userinfo";
login_attribute_path = "preferred_username";
groups_attribute_path = "groups";
name_attribute_path = "name";
use_pkce = true;
role_attribute_path = config.mine.shared.lib.ldap.mkScope (lconfig: llib:
"contains(groups, '${lconfig.groups.grafana_admin}') && 'Admin' || contains(groups, 'editor') && 'Editor' || 'Viewer'"
);
};
};
provision = {
enable = true;
# dashboards.settings.providers = [{
# name = "my dashboards";
# options.path = "/etc/grafana-dashboards";
# }];
datasources.settings.datasources = [
{
name = "Prometheus";
type = "prometheus";
url = "http://${config.services.prometheus.listenAddress}:${toString config.services.prometheus.port}";
}
];
};
};
# authelia
services.authelia.instances.main.settings.identity_providers.oidc.clients = [{
client_id = "grafana";
client_name = "Grafana";
client_secret = "$pbkdf2-sha512$310000$81MV1.67njuS/5H2UvVsnA$vaNO3/tzVA76Jho4ngS.xFjDuYn1sDn/9qo7cD0ueMnVvzaoJj00ND5wCGzVSUnvLuxNE/enC1K5r7xKAe/Hrg";
consent_mode = "implicit";
redirect_uris = [ "https://${svc_domain}/login/generic_oauth" ];
scopes = [
"openid"
"email"
"profile"
"groups"
];
}];
environment.persistence.root.directories = [
config.services.grafana.dataDir
];
systemd.tmpfiles.rules = [
"Z ${config.services.grafana.dataDir} 0770 ${grafana_user} ${grafana_user} -"
];
age.secrets.grafana-authelia-secret.owner = grafana_user;
services.nginx.virtualHosts."${svc_domain}" = {
forceSSL = true;
enableACME = true;
locations."/".proxyPass = "http://localhost:${builtins.toString config.services.grafana.settings.server.http_port}";
};
}

View file

@ -1,18 +0,0 @@
{ config, lib, ... }:
{
services.authelia.instances.main.settings = {
telemetry.metrics = {
enabled = true;
};
};
services.prometheus.scrapeConfigs = [
{
job_name = "authelia";
static_configs = [{
targets = [ (lib.removePrefix "tcp://" config.services.authelia.instances.main.settings.telemetry.metrics.address) ];
}];
}
];
}

View file

@ -1,14 +0,0 @@
{ config, ... }:
{
services.forgejo.settings.metrics.ENABLED = true;
services.prometheus.scrapeConfigs = [
{
job_name = "forgejo";
static_configs = [{
targets = [ "localhost:${builtins.toString config.services.forgejo.settings.server.HTTPPORT}" ];
}];
}
];
}

View file

@ -1,18 +0,0 @@
{ config, ... }:
{
services.hedgedoc.settings = {
# enabled by default anyways
# TODO(eyJhb): disable exposing this to the WORLD
enableStatsApi = true;
};
services.prometheus.scrapeConfigs = [
{
job_name = "hedgedoc";
static_configs = [{
targets = [ "localhost:${builtins.toString config.services.hedgedoc.settings.port}"];
}];
}
];
}

View file

@ -1,27 +0,0 @@
let
metrics_port = 9734;
in {
services.matrix-synapse = {
settings = {
enable_metrics = true;
listeners = [
{
port = metrics_port;
type = "metrics";
bind_addresses = [ "localhost" ];
tls = false;
resources = [];
}
];
};
};
services.prometheus.scrapeConfigs = [
{
job_name = "matrix-synapse";
static_configs = [{
targets = [ "localhost:${builtins.toString metrics_port}"];
}];
}
];
}

View file

@ -1,16 +0,0 @@
{ config, ... }:
{
services.miniflux.config = {
METRICS_COLLECTOR = 1;
};
services.prometheus.scrapeConfigs = [
{
job_name = "miniflux";
static_configs = [{
targets = [ config.services.miniflux.config.LISTEN_ADDR ];
}];
}
];
}

View file

@ -1,45 +0,0 @@
{ config, lib, pkgs, ... }:
let
# occ bin
occ = config.services.nextcloud.occ + "/bin/nextcloud-occ";
nextcloudSetupServerinfoToken = pkgs.writeShellScript "nextcloud-setup-serverinfo-token.sh" ''
# set serverinfo_token
SERVERINFO_TOKEN="$(cat $CREDENTIALS_DIRECTORY/nextcloud-serverinfo-token)"
${occ} config:app:set serverinfo token --value "$SERVERINFO_TOKEN" > /dev/null 2>&1
'';
in {
systemd.services.nextcloud-setup = {
# runs this after all the main nextcloud-setup stuff
script = lib.mkAfter ''
${nextcloudSetupServerinfoToken}
'';
# setup credentials for service
serviceConfig.LoadCredential = [
"nextcloud-serverinfo-token:${config.age.secrets.nextcloud-serverinfo-token.path}"
];
};
services.prometheus.exporters.nextcloud = {
enable = true;
listenAddress = "localhost";
tokenFile = config.age.secrets.nextcloud-serverinfo-token.path;
url = let
scheme = if config.services.nextcloud.https then "https" else "http";
in "${scheme}://${config.services.nextcloud.hostName}";
};
# setup permissions
age.secrets.nextcloud-serverinfo-token.owner = config.services.prometheus.exporters.nextcloud.user;
services.prometheus.scrapeConfigs = [
{
job_name = "nextcloud";
static_configs = [{
targets = [ "localhost:${builtins.toString config.services.prometheus.exporters.nextcloud.port}" ];
}];
}
];
}

View file

@ -1,23 +0,0 @@
{ config, lib, ... }:
{
services.prometheus.exporters.node = {
enable = true;
listenAddress = "localhost";
enabledCollectors = [
"cpu"
"filesystem"
"systemd"
];
};
services.prometheus.scrapeConfigs = [
{
job_name = "node-exporter";
static_configs = [{
targets = [ "localhost:${builtins.toString config.services.prometheus.exporters.node.port}"];
}];
}
];
}

View file

@ -1,34 +0,0 @@
{ config, pkgs, ... }:
{
services.prometheus.exporters.postgres = {
enable = true;
listenAddress = "localhost";
runAsLocalSuperUser = true;
extraFlags = let
extraQuery = pkgs.writeText "prometehus-postgres-query.yaml" ''
pg_database:
query: "SELECT pg_database.datname, pg_database_size(pg_database.datname) as size FROM pg_database"
metrics:
- datname:
usage: "LABEL"
description: "Name of the database"
- size:
usage: "GAUGE"
description: "Disk space used by the database"
'';
in [
"--extend.query-path=${extraQuery}"
];
};
services.prometheus.scrapeConfigs = [
{
job_name = "postgres";
static_configs = [{
targets = [ "localhost:${toString config.services.prometheus.exporters.postgres.port}" ];
}];
}
];
}

View file

@ -1,16 +0,0 @@
{ config, ... }:
{
services.searx.settings.general.open_metrics = "thisreallydoesnotmatterasitisnotaccessiblefromoutsideofthisserver";
services.prometheus.scrapeConfigs = [
{
job_name = "searx";
basic_auth.username = "canbeanything";
basic_auth.password = config.services.searx.settings.general.open_metrics;
static_configs = [{
targets = [ config.services.searx.uwsgiConfig.http ];
}];
}
];
}

View file

@ -1,22 +0,0 @@
{ config, ... }:
{
services.stalwart-mail.settings = {
metrics.prometheus.enable = true;
};
services.prometheus.scrapeConfigs = [
{
job_name = "stalwart";
metrics_path = "/metrics/prometheus";
static_configs = [{
targets = [ "localhost:${toString config.mine.shared.settings.mail.ports.http_management}" ];
}];
metric_relabel_configs = [{
source_labels = [ "__name__" ];
target_label = "__name__";
replacement = "stalwart_$1";
}];
}
];
}

View file

@ -1,12 +0,0 @@
{ config, ... }:
{
services.prometheus.scrapeConfigs = [
{
job_name = "uptime-kuma";
static_configs = [{
targets = [ "localhost:${builtins.toString config.services.uptime-kuma.settings.PORT}" ];
}];
}
];
}

View file

@ -1,19 +0,0 @@
{ config, pkgs, ... }:
{
services.prometheus.exporters.zfs = {
enable = true;
listenAddress = "localhost";
extraFlags = [ "--collector.dataset-snapshot" ];
};
services.prometheus.scrapeConfigs = [
{
job_name = "zfs";
static_configs = [{
targets = [ "localhost:${toString config.services.prometheus.exporters.zfs.port}" ];
}];
}
];
}

View file

@ -1,28 +0,0 @@
{ config, ... }:
let
prometheus_user = config.systemd.services.prometheus.serviceConfig.User;
fullDataDirPath = "/var/lib/${config.services.prometheus.stateDir}";
filesetPath = config.mine.zfsMounts."rpool/safe/svcs/prometheus";
in {
services.prometheus = {
enable = true;
globalConfig.scrape_interval = "10s";
globalConfig.scrape_timeout = "10s";
listenAddress = "localhost";
# default is 15 days, we just set it to 14 to be explicit
retentionTime = "14d";
};
fileSystems."${filesetPath}".neededForBoot = true;
environment.persistence."${filesetPath}".directories = [
fullDataDirPath
];
systemd.tmpfiles.rules = [
"Z ${fullDataDirPath} 0770 ${prometheus_user} ${prometheus_user} -"
];
}

View file

@ -3,7 +3,7 @@
let
svc_domain = "nextcloud.${config.mine.shared.settings.domain}";
default_storage_quota = "1mb";
default_storage_quota = "100MB";
# place data into own zfs dataset
stateDir = config.mine.zfsMounts."rpool/safe/svcs/nextcloud";
@ -49,7 +49,7 @@ let
ldapGroupFilter = config.mine.shared.lib.ldap.mkFilter (lconfig: llib:
llib.mkAnd [
(llib.mkOC lconfig.oc.groupOfUniqueNames)
(llib.mkOr [ "cn=${lconfig.groups.nextcloud_admin}" "cn=${lconfig.groups.member}"])
(llib.mkOr [ "cn=${lconfig.groups.admin}" "cn=${lconfig.groups.member}"])
]
);
ldapGroupFilterGroups = "admin;user";
@ -59,8 +59,6 @@ let
ldapUserFilterMode = 1;
ldapExpertUsernameAttr = config.mine.shared.settings.ldap.attr.uid;
ldapConfigurationActive = 1;
ldapQuotaDefault = 1;
ldapQuotaAttribute = config.mine.shared.settings.ldap.attr.nextcloudquota;
};
ldap_commands = lib.mapAttrsToList (n: v: "${occ} ldap:set-config $NEW_CONFIG_ID ${n} '${builtins.toString v}'") ldap_settings;
in pkgs.writeShellScript "nextcloud-add-ldap.sh" ''
@ -86,7 +84,7 @@ let
done
# promote ldap admin group to admins
${occ} ldap:promote-group ${config.mine.shared.settings.ldap.groups.nextcloud_admin} --yes -n
${occ} ldap:promote-group ${config.mine.shared.settings.ldap.groups.admin} --yes -n
'';
# script for resetting nextcloud admin password on each startup
@ -160,7 +158,7 @@ in {
config.dbtype = "pgsql";
# settings
settings = rec {
settings = {
# open connect/oidc
oidc_login_provider_url = "https://${config.mine.shared.settings.authelia.domain}";
oidc_login_client_id = AUTHELIA_AUTH_NAME;
@ -178,25 +176,9 @@ in {
};
oidc_login_scope = "openid profile email groups";
oidc_login_code_challenge_method = "S256";
# mail
mail_from_address = "nextcloud";
mail_smtpmode = "smtp";
mail_sendmailmode = "smtp";
mail_domain = "${config.mine.shared.settings.domain}";
mail_smtphost = "${config.mine.shared.settings.mail.domain_smtp}";
mail_smtpport = config.mine.shared.settings.mail.ports.submissions;
mail_smtpsecure = "ssl";
mail_smtpname = mail_from_address;
# mail_smtppassword = "defined-in-the-secrets-file-and-in-a-separate-file-for-lldap";
};
};
# setup lldap user for nextcloud that can send emails
services.lldap.provision.users = config.mine.shared.lib.ldap.mkScope (lconfig: llib: {
nextcloud = llib.mkProvisionUserSystem "nextcloud" config.age.secrets.nextcloud-smtp-pass.path;
});
systemd.services.nextcloud-setup = {
# runs this after all the main nextcloud-setup stuff
script = lib.mkAfter ''
@ -221,7 +203,6 @@ in {
client_id = AUTHELIA_AUTH_NAME;
client_name = "Nextcloud";
client_secret = "$pbkdf2-sha512$310000$kLNQ/1A.uasSN4g8q94jUQ$8OKNUNNumHCh8dVG5/QWys7u.y1guqFXlrL.bMm7/HKTsWhpib/W.8qlU6VU7V1Be/h14Y.fJi3RLvbkEdo2kA";
consent_mode = "implicit";
redirect_uris = [ "https://${svc_domain}/apps/oidc_login/oidc" ];
scopes = [
"openid"

View file

@ -1,414 +0,0 @@
#!/usr/bin/env nix-shell
#!nix-shell --pure -i python3 -p "python3.withPackages (ps: with ps; [ flask apprise mnemonic wtforms jq ])"
from typing import Any
import apprise
from flask import Flask, request
from mnemonic import Mnemonic
import sqlite3
import os
import jq
import json
app = Flask(__name__)
app.url_map.strict_slashes = False
import logging
log = logging.getLogger("werkzeug")
log.setLevel(logging.ERROR)
ENV_PREFIX = "NOTIFIER_"
def getenv(key: str, default: Any = None) -> Any:
v = os.getenv(ENV_PREFIX + key, default)
if not v:
exit(f"{ENV_PREFIX+key} must be specified!")
return v
CONFIG_URL = getenv("URL", "http://127.0.0.1")
CONFIG_PORT = int(getenv("PORT", 8080))
CONFIG_DATABASE_PATH = getenv("DATABASE_PATH", "notifications.db")
CONFIG_MATRIX_BOT_NAME = getenv("MATRIX_BOT_NAME", "unset")
CONFIG_MATRIX_BOT_TOKEN = getenv("MATRIX_BOT_TOKEN")
CONFIG_MATRIX_HOST = getenv("MATRIX_HOST")
CONFIG_PROXY_AUTH_USERNAME_HEADER = getenv("PROXY_AUTH_USERNAME_HEADER", "Remote-User")
CONFIG_MAIL_USERNAME = getenv("MAIL_USERNAME")
CONFIG_MAIL_PASSWORD = getenv("MAIL_PASSWORD")
CONFIG_MAIL_DOMAIN = getenv("MAIL_DOMAIN")
CONFIG_MAIL_HOST = getenv("MAIL_HOST")
CONFIG_MAIL_PORT = int(getenv("MAIL_PORT", "465"))
CONFIG_MAIL_MODE = getenv("MAIL_MODE", "ssl")
script_example = rf"""#!/usr/bin/env bash
#!/usr/bin/env nix-shell
#!nix-shell --pure -i python3 -p "python3.withPackages (ps: with ps; [ requests ])"
import requests
import argparse
import sys
import os
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("--title", default="", help="Subject/title of the notification")
parser.add_argument("--body", default="", help="`-` for stdin")
parser.add_argument("--jq", default=".", help="Jq filter to use")
parser.add_argument("--type", default="matrix", help="mail or matrix")
parser.add_argument("--token", help="token to use")
parser.add_argument("--token-file", help="file to read token from")
parser.add_argument("--url", default="{CONFIG_URL}/notify", help="Notify endpoint")
args = parser.parse_args()
token: str = args.token
if args.token_file:
token = open(args.token_file, "r").read().strip()
if not token:
exit("No token or tokenfile specified, or empty")
data = args.body
if data == "-" and not sys.stdin.isatty():
data = "\n".join(sys.stdin.readlines())
headers = {{"Authorization": f"Bearer {{token}}"}}
params = {{
"jq": args.jq,
"type": args.type,
}}
if args.title:
params["title"] = args.title
req = requests.post(args.url, headers=headers, params=params, data=data)
exit(not req.status_code == 200)
"""
script_example_with_token = script_example.replace(
'--token"',
'--token", default="||TOKEN||"',
)
def get_db():
con = sqlite3.connect(CONFIG_DATABASE_PATH)
cur = con.cursor()
cur.execute(
"CREATE TABLE IF NOT EXISTS default_room(username TEXT PRIMARY KEY, room_id TEXT NOT NULL)"
)
cur.execute(
"CREATE TABLE IF NOT EXISTS tokens(username TEXT PRIMARY KEY, token TEXT NOT NULL)"
)
return con
@app.route("/", methods=["GET", "POST"])
def index():
username = request.headers.get(CONFIG_PROXY_AUTH_USERNAME_HEADER)
if not username:
return ("Not authenticated", 401)
# handle post stuff
if request.method == "POST":
action = request.form.get("action", "").lower()
if "token" in action:
generate_token_for_user(username)
elif "room id" in action:
roomid = request.form.get("room_id")
if not roomid:
return ("Room Id cannot be empty", 400)
set_user_default_matrix_room(username, roomid)
con = get_db()
cur = con.cursor()
res = cur.execute(
"SELECT token FROM tokens WHERE username = ?", (username,)
).fetchone()
token: str = ""
if res:
token = res[0]
res = cur.execute(
"SELECT room_id FROM default_room WHERE username = ?", (username,)
).fetchone()
room_id: str = ""
if res:
room_id = res[0]
# hack to make users confirm it
generate_token_name: str = "tmpaction"
generate_token_value: str = "Generate Token"
if request.form.get(generate_token_name):
generate_token_name = "action"
generate_token_value = "Generate Token. Are you sure?"
tmpl = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Notification Service</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
</head>
<body>
<div class="container my-5">
<h1>Hello {username}!</h1>
<div class="col-lg-8 px-0">
<form method="post">
<div class="mb-3">
<label class="form-label">Default Matrix Room ID</label>
<input type="text" name="room_id" value="{room_id}" placeholder="!yREJWHUMJhGROiHbtu:fricloud.dk" class="form-control">
</div>
<input type="submit" class="btn btn-primary" name="action" value="Set Default Room ID">
<hr>
<div class="mb-3">
<label class="form-label">Token (dashes are optional)</label>
<input type="text" value="{token}" placeholder="token-not-generated" readonly class="form-control" onclick="this.select();" >
</div>
<input type="submit" class="btn btn-primary" name="{generate_token_name}" value="{generate_token_value}">
</form>
<hr>
<p>The token can be given as a URL parameter 'token', as a auth header `Authorization: Bearer token`, HTTPAuth where username is ignored, and token is the password.
Can also be given a a path, e.g. {CONFIG_URL}/{token}.
</p>
<hr>
<div class="mb-3">
<label class="form-label">Quick URL</label>
<input type="text" value="curl {CONFIG_URL}/notify/{token.replace("-", "")}/mymessage" class="form-control" onclick="this.select();" >
</div>
<hr>
<p>
This notification service has support for both matrix and email.
Matrix notifications will be sent from {CONFIG_MATRIX_BOT_NAME}, be sure to invite them to the room/space if it is private.
If using email, they will only be sent to your member email, and can't be sent anywhere else.
You'll receive them from {CONFIG_MAIL_USERNAME}@{CONFIG_MAIL_DOMAIN}.
</p>
<hr>
<table class="table">
<thead>
<tr>
<th scope="col">Param</th>
<th scope="col">Description</th>
<th scope="col">Default</th>
<th scope="col">Example</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">title</th>
<td>Title of notification</td>
<td>Notification</td>
<td>Compilation Finished!</td>
</tr>
<tr>
<th scope="row">body</th>
<td>Body of notification</td>
<td>empty</td>
<td>Compilation result: success (default is nothing)</td>
</tr>
<tr>
<th scope="row">type</th>
<td>type of notification</td>
<td>matrix</td>
<td>matrix</td>
</tr>
<tr>
<th scope="row">room_id</th>
<td>Matrix room ID</td>
<td>default room</td>
<td>!yREJWHUMJhGROiHbtu:fricloud.dk or #na-offtopic:rend.al</td>
</tr>
<tr>
<th scope="row">token</th>
<td>Authorization Token</td>
<td>empty</td>
<td>enable-trade-decide or enabletradedecide</td>
</tr>
<tr>
<th scope="row">jq</th>
<td>jq filter</td>
<td>.</td>
<td>[.[].commits | .name ] | join("\\n")</td>
</tr>
</tbody>
</table>
<hr>
bash alias
<pre class="border"><code>notify() {{ curl "{CONFIG_URL}/notify/{token}" -g --data-urlencode "body=${{1:-}}"; }}</code></pre>
curl
<pre class="border"><code>curl "{CONFIG_URL}/notify" -H "Authorization: Bearer {token}"</code></pre>
curl w/ specific body/title
<pre class="border"><code>curl "{CONFIG_URL}/notify?title=MyTitle&body=MyBody" -H "Authorization: Bearer {token}"</code></pre>
Python
<pre class="border"><code>{script_example}</code></pre>
Python w/ <b>hardcoded token (DO NOT SHARE)</b>
<pre class="border"><code>{script_example_with_token}</code></pre>
Nix Python Script <b>HARDCODED TOKEN (DO NOT SHARE)</b>
<pre class="border"><code>pkgs.writers.writePython3Bin "notify" {{
libraries = with pkgs.python3Packages; [ requests ];
doCheck = false;
}} ''{script_example_with_token}'';
</code></pre>
<hr>
<h2>Notes</h2>
<p>
<b>jq</b> is very powerful, and can easily be used to turn webhook data into useful information in a notification.
Just append your `jq=<url_encoded_query>`, to your notification URL, and then watch the magic.
Below is an example for doing it with Forgejo, when new commits are pushed.
</p>
<pre class="border"><code>(.total_commits | tostring)
+ " commits pushed to "
+ .repository.full_name
+ "\n\n" +
# format commits
([.commits.[]
| "- "
+ (.message | gsub("[\n\t]"; ""))
+ " (" + .author.name + ")" ]
| join("\n"))
+ "\n\n"
+ "Changes: " + .compare_url</code></pre>
</div>
</div>
</body>
</html>
""".replace(
r"||TOKEN||", token
)
return tmpl
@app.route("/notify", methods=["GET", "POST"], defaults={"token": "", "body": ""})
@app.route("/notify/<token>", methods=["GET", "POST"], defaults={"body": ""})
@app.route("/notify/<token>/<body>", methods=["GET", "POST"])
def send_notification(token: str, body: str):
# default to this
ntype = request.args.get("type", "matrix")
title = request.args.get("title", "Notification")
body = request.args.get("body", request.get_data().decode("utf-8") or body)
if not body:
body = " "
if request.authorization:
token = request.authorization.token or request.authorization.password
if not token:
return (
"No token found, please either specify with Authorization: Bearer <token>, HTTPBasic or in the URL path",
401,
)
if (
request.method == "POST"
and "json" in request.headers.get("content-type", "")
or is_json(body)
):
try:
body = jq.compile(request.args.get("jq", ".")).input_text(body).first()
except Exception:
return ("Unable to compile JQ, please ensure it is correct", 400)
if not isinstance(body, str):
body = json.dumps(body, indent=4)
con = get_db()
cur = con.cursor()
res = cur.execute(
"SELECT username FROM tokens WHERE REPLACE(token, '-', '') = ? OR token = ? ",
(token, token),
).fetchone()
if not res:
return ("Access denied - invalid token", 401)
username = res[0]
if ntype not in ["matrix", "mail"]:
return ("Invalid type, only matrix or mail allowed", 400)
apobj = apprise.Apprise()
if ntype == "matrix":
# try to get a room_id
room_id = request.args.get("room_id")
if not room_id:
res = cur.execute(
"SELECT room_id FROM default_room WHERE username = ?", (username,)
).fetchone()
if res:
room_id = res[0]
if not room_id:
return ("No room_id specified, and no default saved", 400)
apobj.add(f"matrixs://{CONFIG_MATRIX_BOT_TOKEN}@{CONFIG_MATRIX_HOST}/{room_id}")
else:
apobj.add(
f"mailto://{CONFIG_MAIL_USERNAME}:{CONFIG_MAIL_PASSWORD}@{CONFIG_MAIL_HOST}:{CONFIG_MAIL_PORT}?mode={CONFIG_MAIL_MODE}&from={CONFIG_MAIL_USERNAME}@{CONFIG_MAIL_DOMAIN}&to={username}@{CONFIG_MAIL_DOMAIN}"
)
sent_notification = apobj.notify(
title=title,
body=body,
)
if not send_notification:
return ("Unable to send notification", 500)
return ("Notification sent!", 200)
def generate_token_for_user(username: str):
token = generate_token()
con = get_db()
cur = con.cursor()
t = cur.execute(
"INSERT INTO tokens (username, token) VALUES (?, ?) ON CONFLICT (username) DO UPDATE SET token = ?",
(
username,
token,
token,
),
)
con.commit()
def set_user_default_matrix_room(username: str, roomid: str):
con = get_db()
cur = con.cursor()
t = cur.execute(
"INSERT INTO default_room (username, room_id) VALUES (?, ?) ON CONFLICT (username) DO UPDATE SET room_id = ?",
(
username,
roomid,
roomid,
),
)
con.commit()
def generate_token(num_words: int = 3) -> str:
mnemo = Mnemonic("english")
words = mnemo.generate(strength=256)
return "-".join(words.split(" ")[0:num_words])
def is_json(potential_json: str) -> bool:
try:
json.loads(potential_json)
return True
except Exception:
return False
if __name__ == "__main__":
app.run(host="127.0.0.1", port=CONFIG_PORT)

View file

@ -1,90 +0,0 @@
{ config, lib, pkgs, ... }:
let
svc_domain = "notify.${config.mine.shared.settings.domain}";
port = 5055;
ldap_user = "notification";
stateDirName = "notify";
stateDir = "/var/lib/${stateDirName}";
in {
systemd.services.notifify = {
description = "notifications for members";
wantedBy = [ "multi-user.target" ];
after = [ "networking.target" ];
environment = {
NOTIFIER_URL = "https://${svc_domain}";
NOTIFIER_PORT = builtins.toString port;
NOTIFIER_DATABASE_PATH = "${stateDir}/notify.db";
# NOTIFIER_MATRIX_BOT_TOKEN = "";
NOTIFIER_MATRIX_BOT_NAME = "@${ldap_user}:${config.mine.shared.settings.domain}";
NOTIFIER_MATRIX_HOST = config.mine.shared.settings.matrix-synapse.domain;
NOTIFIER_PROXY_AUTH_USERNAME_HEADER = config.mine.shared.lib.authelia.protectedHeaders.username;
NOTIFIER_MAIL_USERNAME = ldap_user;
# NOTIFIER_MAIL_PASSWORD = "";
NOTIFIER_MAIL_DOMAIN = config.mine.shared.settings.domain;
NOTIFIER_MAIL_HOST = config.mine.shared.settings.mail.domain;
NOTIFIER_MAIL_PORT = builtins.toString config.mine.shared.settings.mail.ports.submissions;
# production
FLASK_ENV = "production";
};
serviceConfig = {
EnvironmentFile = [ config.age.secrets.notify-env.path ];
StateDirectory = stateDirName;
DynamicUser = true;
ExecStart = let
pythonEnv = pkgs.python3.withPackages(ps: with ps; [ flask apprise mnemonic wtforms jq ]);
in "${pythonEnv}/bin/python ${./app.py}";
Restart = "always";
};
};
# setup notification user
services.lldap.provision.users = config.mine.shared.lib.ldap.mkScope (lconfig: llib: {
"${ldap_user}" = llib.mkProvisionUserSystem ldap_user config.age.secrets.notify-ldap-pass.path;
});
# persistent files
environment.persistence.root.directories = [
{ directory = "/var/lib/private/${stateDirName}"; mode = "0700"; }
];
# nginx
services.nginx.virtualHosts."${svc_domain}" = config.mine.shared.lib.authelia.mkProtectedWebsite {
forceSSL = true;
enableACME = true;
locations."/" = config.mine.shared.lib.authelia.mkProtectedLocation {
proxyPass = "http://localhost:${builtins.toString port}";
};
locations."/notify".proxyPass = "http://localhost:${builtins.toString port}";
};
# metada
mine.shared.meta.notify = {
name = "Notification Service";
description = "This website you are looking at right now, which is our members website.";
url = "https://${svc_domain}";
package = {
name = "notify-website";
version = "v0.0.1";
meta = with lib; {
description = "Notification website for ${config.mine.shared.settings.domain}";
license = licenses.free;
homepage = "https://git.fricloud.dk/fricloud/server-configs/src/branch/main/machines/gerd/services/notify/app.py";
platforms = platforms.all;
};
};
};
}

View file

@ -1,25 +0,0 @@
#!/usr/bin/env bash
set -e
BODY="$1"
TITLE=${2:-Notification}
JQ_EXPR=${3:-.}
TYPE=${4:-matrix}
# TOKEN="$(cat ~/.config/notify/token)"
# TOKEN="$(cat /run/agenix/notify-token)"
TOKEN="$(cat token)"
URL="https://notify.fricloud.dk/notify"
# URL="||URL||"
# get stdin if needed
if [ "$BODY" = "-" ]; then
BODY="$(cat -)"
fi
# make request
curl -H "Authorization: Bearer $TOKEN" "$URL" \
--get \
--data-urlencode "title=$TITLE" \
--data-urlencode "body=$BODY" \
--data-urlencode "jq=$JQ_EXPR" \
--data-urlencode "type=$TYPE"

View file

@ -1,147 +0,0 @@
{ config, lib, pkgs, ... }:
let
svc_name = "rallly";
svc_domain = "${svc_name}.${config.mine.shared.settings.domain}";
psqlSocket = "/run/postgresql";
user = "rallly";
port = 7384;
ralllyPkgsOrig = pkgs.callPackage ./../../../../shared/pkgs/rallly {};
ralllyPkgs = ralllyPkgsOrig.overrideAttrs (old: {
patches = (old.patches or []) ++ [
./patches/remove-login-register.patch
];
});
in {
# setup rallly service
systemd.services.rallly = {
description = "rallly";
wantedBy = [ "multi-user.target" ];
after = [ "networking.target" ];
# configuration
environment = let
rallly-prisma-engines = ralllyPkgs.passthru.rallly-prisma-engines;
in rec {
HOSTNAME = "localhost";
PORT = builtins.toString port;
DATABASE_URL = "postgresql://${user}@localhost/${user}?host=${psqlSocket}";
NEXT_PUBLIC_BASE_URL = "https://${svc_domain}";
NEXTAUTH_URL = NEXT_PUBLIC_BASE_URL;
# SECRET_PASSWORD = "specified-in-env";
# limit signup even further
ALLOWED_EMAILS = "*@${config.mine.shared.settings.domain}";
# email
SUPPORT_EMAIL = "${svc_name}@${config.mine.shared.settings.domain}";
SMTP_HOST = config.mine.shared.settings.mail.domain_smtp;
SMTP_PORT = builtins.toString config.mine.shared.settings.mail.ports.submissions;
SMTP_SECURE = "true";
SMTP_USER = svc_name;
# SMTP_PWD = "specified-in-env";
# OIDC
OIDC_NAME = "Authelia";
OIDC_DISCOVERY_URL = "https://${config.mine.shared.settings.authelia.domain}/.well-known/openid-configuration";
OIDC_CLIENT_ID = "rallly";
# OIDC_CLIENT_SECRET = "specified-in-env";
# prisma things (database will not work without, needs to match version in rallly deps as well)
PRISMA_SCHEMA_ENGINE_BINARY = "${rallly-prisma-engines}/bin/schema-engine";
PRISMA_QUERY_ENGINE_BINARY = "${rallly-prisma-engines}/bin/query-engine";
PRISMA_QUERY_ENGINE_LIBRARY = "${rallly-prisma-engines}/lib/libquery_engine.node";
PRISMA_INTROSPECTION_ENGINE_BINARY = "${rallly-prisma-engines}/bin/introspection-engine";
PRISMA_FMT_BINARY = "${rallly-prisma-engines}/bin/prisma-fmt";
};
# add, otherwise we get warnings
path = [ pkgs.openssl ];
serviceConfig = {
ExecStartPre = [
# clear cache on each boot, otherwise we might have
# issues when updating it.
"${pkgs.findutils}/bin/find -L /var/cache/${svc_name} -mindepth 1 -delete"
# run db migration each boot
"${ralllyPkgs}/bin/rallly-prisma migrate deploy"
];
ExecStart = "${ralllyPkgs}/bin/rallly";
# secret configurations
EnvironmentFile = [ config.age.secrets.rallly-env.path ];
CacheDirectory = svc_name;
CacheDirectoryMode = "0750";
User = user;
DynamicUser = true;
Restart = "always";
};
};
# setup postgresql
services.postgresql = {
ensureDatabases = [ user ];
ensureUsers = [{
name = user;
ensureDBOwnership = true;
}];
};
# setup ldap user for email
services.lldap.provision.users = config.mine.shared.lib.ldap.mkScope (lconfig: llib: {
"${svc_name}" = llib.mkProvisionUserSystem "${svc_name}" config.age.secrets.rallly-ldap-pass.path;
});
# authelia
services.authelia.instances.main.settings.identity_providers.oidc.clients = [{
client_id = "rallly";
client_name = "Rallly";
client_secret = "$pbkdf2-sha512$310000$KB4UqeuVr86lEOoISSE92w$i2YGpz3wRwceiRfYnMUhZ0MboutkDPPYVWnXqiw6tUt./mgZ5kfV1ES.kcdsHhMdavhCrJfWvVTPQRJKImuUrQ";
consent_mode = "implicit";
redirect_uris = [ "https://${svc_domain}/api/auth/callback/oidc" ];
scopes = [
"openid"
"email"
"profile"
];
}];
# nginx
services.nginx.virtualHosts."${svc_domain}" = config.mine.shared.lib.authelia.mkProtectedWebsite {
forceSSL = true;
enableACME = true;
locations."/" = {
proxyPass = "http://localhost:${builtins.toString port}";
};
# try to disable registration
locations."/api/trpc/auth.requestRegistration" = {
root = pkgs.writeTextDir "index.html" ''
NO REGISTRATION!!
'';
};
};
# meta information!
mine.shared.meta.rallly = {
name = "Rallly";
description = ''Rallly is an open-source scheduling and collaboration tool designed to make organizing events and meetings easier. Please do not try to use the register or normal login, only try to sign in using the SSO method. '';
url = "https://${svc_domain}";
package = let
pkg = ralllyPkgs;
in {
name = pkg.pname;
version = pkg.version;
meta = pkg.meta;
};
};
}

View file

@ -1,84 +0,0 @@
diff --git a/apps/web/src/app/[locale]/(auth)/login/login-form.tsx b/apps/web/src/app/[locale]/(auth)/login/login-form.tsx
index d4a2adcf..8137a790 100644
--- a/apps/web/src/app/[locale]/(auth)/login/login-form.tsx
+++ b/apps/web/src/app/[locale]/(auth)/login/login-form.tsx
@@ -159,45 +159,7 @@ export function LoginForm() {
}
})}
>
- <div className="mb-1 text-2xl font-bold">{t("login")}</div>
- <p className="mb-4 text-gray-500">
- {t("stepSummary", {
- current: 1,
- total: 2,
- })}
- </p>
- <fieldset className="mb-2.5">
- <label htmlFor="email" className="mb-1 text-gray-500">
- {t("email")}
- </label>
- <Input
- className="w-full"
- id="email"
- size="lg"
- error={!!formState.errors.email}
- autoFocus={true}
- disabled={formState.isSubmitting}
- placeholder={t("emailPlaceholder")}
- {...register("email", { validate: validEmail })}
- />
- {formState.errors.email?.message ? (
- <div className="mt-2 text-sm text-rose-500">
- {formState.errors.email.message}
- </div>
- ) : null}
- </fieldset>
<div className="flex flex-col gap-2">
- <Button
- loading={formState.isSubmitting}
- type="submit"
- size="lg"
- variant="primary"
- className=""
- >
- {t("loginWith", {
- provider: t("email"),
- })}
- </Button>
{error === "OAuthAccountNotLinked" ? (
<Alert icon={AlertTriangleIcon} variant="destructive">
<AlertTitle>
@@ -216,12 +178,6 @@ export function LoginForm() {
) : null}
{alternativeLoginMethods.length > 0 ? (
<>
- <div className="relative my-4">
- <hr className="border-grey-500 absolute top-1/2 w-full border-t" />
- <span className="absolute left-1/2 -translate-x-1/2 -translate-y-1/2 transform bg-white px-2 text-center text-xs uppercase text-gray-400">
- {t("or", { defaultValue: "Or" })}
- </span>
- </div>
<div className="grid gap-2.5">
{alternativeLoginMethods.map((method, i) => (
<Button size="lg" key={i} onClick={method.login}>
diff --git a/apps/web/src/app/[locale]/(auth)/login/page.tsx b/apps/web/src/app/[locale]/(auth)/login/page.tsx
index 10caefed..28d6c85a 100644
--- a/apps/web/src/app/[locale]/(auth)/login/page.tsx
+++ b/apps/web/src/app/[locale]/(auth)/login/page.tsx
@@ -13,16 +13,6 @@ export default async function LoginPage({ params }: { params: Params }) {
<AuthCard>
<LoginForm />
</AuthCard>
- <div className="mt-4 pt-4 text-center text-gray-500 sm:text-base">
- <Trans
- t={t}
- i18nKey="notRegistered"
- defaults="Don't have an account? <a>Register</a>"
- components={{
- a: <Link href="/register" className="text-link" />,
- }}
- />
- </div>
</div>
);
}

View file

@ -33,8 +33,8 @@ in {
# meta
mine.shared.meta.searx = {
name = "searXNG";
description = "We host our own searXNG, use it to search the web!";
name = "searX";
description = "We host our own searX, use it to search the web!";
url = "https://${svc_domain}";
package = let

View file

@ -1,95 +0,0 @@
{ config, lib, pkgs, ... }:
let
svc_domain = "uptime-kuma.${config.mine.shared.settings.domain}";
in {
services.uptime-kuma = {
enable = true;
appriseSupport = true;
package = pkgs.uptime-kuma.overrideAttrs (old: rec {
pname = "uptime-kuma";
version = "2.0.0-dev";
src = pkgs.fetchFromGitHub {
owner = "M1CK431";
repo = "uptime-kuma";
rev = "5a16af40fdddcaa61d197242840344804a246d01";
hash = "sha256-W7ieVrfm/SZU/MNB7dJW3V3vq0RBrAJVqv0gK7H4Xik=";
};
npmDepsHash = "sha256-Q2u6ClG6g8yoGvSJ/LGlKTL4XkJGWY+DAojpM1xBwQ0=";
npmDeps = pkgs.fetchNpmDeps {
inherit src;
name = "${pname}-${version}-npm-deps";
hash = npmDepsHash;
};
patches = [
(pkgs.writeText "uptime-kuma-database-writeable.patch" ''
diff --git a/server/database.js b/server/database.js
index 3374aff9..9e890d28 100644
--- a/server/database.js
+++ b/server/database.js
@@ -221,6 +221,7 @@ class Database {
if (! fs.existsSync(Database.sqlitePath)) {
log.info("server", "Copying Database");
fs.copyFileSync(Database.templatePath, Database.sqlitePath);
+ fs.chmodSync(Database.path, 0o640);
}
const Dialect = require("knex/lib/dialects/sqlite3/index.js");
'')
# TODO(eyJhb): do we really want this?
(pkgs.writeText "uptime-kuma-disable-metrics-auth.patch" ''
diff --git a/server/server.js b/server/server.js
index db58ae82..d650a42a 100644
--- a/server/server.js
+++ b/server/server.js
@@ -292,7 +292,7 @@ let needSetup = false;
// Prometheus API metrics /metrics
// With Basic Auth using the first user's username/password
- app.get("/metrics", apiAuth, prometheusAPIMetrics());
+ app.use("/metrics", prometheusAPIMetrics());
app.use("/", expressStaticGzip("dist", {
enableBrotli: true,
'')
];
});
};
# setup persistence
environment.persistence.root.directories = [
{ directory = "/var/lib/private/uptime-kuma"; mode = "0700"; }
];
# setup ldap user for email
services.lldap.provision.users = config.mine.shared.lib.ldap.mkScope (lconfig: llib: {
uptime-kuma = llib.mkProvisionUserSystem "uptime-kuma" config.age.secrets.uptime-kuma-ldap-pass.path;
});
# nginx
services.nginx.virtualHosts."${svc_domain}" = config.mine.shared.lib.authelia.mkProtectedWebsite {
forceSSL = true;
enableACME = true;
locations."/" = config.mine.shared.lib.authelia.mkProtectedLocation {
proxyPass = "http://localhost:${builtins.toString config.services.uptime-kuma.settings.PORT}";
proxyWebsockets = true;
};
};
mine.shared.meta.uptime-kuma = {
name = "Uptime Kuma";
description = ''Fancy self-hosted monitoring tool, which supports VARIOUS methods of monitoring, as well as getting notifications. Multiple users is not officially support, so reach out to admins, and they will create a user for you. Abuse will NOT be tolerated. We have a SMTP account associated with Uptime Kuma, ask for details on how to use this (you're also allowed to to your own member email).'';
url = "https://${svc_domain}";
package = let
pkg = config.services.uptime-kuma.package;
in {
name = pkg.pname;
version = pkg.version;
meta = pkg.meta;
};
};
}

View file

@ -1,126 +0,0 @@
{ config, pkgs, lib, ... }:
let
svc_domain = "vikunja.${config.mine.shared.settings.domain}";
vikunjaOIDCName = "authelia";
in {
services.vikunja = {
enable = true;
package = pkgs.vikunja.overrideAttrs (old: {
# TODO(eyJhb): remove once vikunja updates past 0.24.6
# https://github.com/go-vikunja/vikunja/issues/623
patches = (old.patches or []) ++ [
(pkgs.writeText "vikunja-clientsecret-envvar.patch" ''
diff --git a/pkg/modules/auth/openid/providers.go b/pkg/modules/auth/openid/providers.go
index 5e14c1b31..d9a5215c1 100644
--- a/pkg/modules/auth/openid/providers.go
+++ b/pkg/modules/auth/openid/providers.go
@@ -17,6 +17,8 @@
package openid
import (
+ "fmt"
+ "os"
"regexp"
"strconv"
"strings"
@@ -139,6 +141,10 @@ func getProviderFromMap(pi map[string]interface{}) (provider *Provider, err erro
Scope: scope,
}
+ if clientSecret, ok := os.LookupEnv(fmt.Sprintf("VIKUNJA_AUTH_OPENID_PROVIDERS_%s_CLIENTSECRET", strings.ToUpper(provider.Name))); ok {
+ provider.ClientSecret = clientSecret
+ }
+
cl, is := pi["clientid"].(int)
if is {
provider.ClientID = strconv.Itoa(cl)
'')
];
});
frontendScheme = "https";
frontendHostname = svc_domain;
database = {
type = "postgres";
host = "/run/postgresql";
};
environmentFiles = [
config.age.secrets.vikunja-env.path
];
settings = {
service.enableregistration = false;
auth.local.enabled = false;
auth.openid = {
enabled = true;
providers = [{
key = "authelia";
name = vikunjaOIDCName;
clientid = "vikunja";
authurl = "https://${config.mine.shared.settings.authelia.domain}";
clientsecret = "not-used-but-needs-to-be-set";
}];
};
};
};
# setup for oidc
services.authelia.instances.main.settings.identity_providers.oidc.clients = [{
client_id = "vikunja";
client_name = "Vikunja";
client_secret = "$pbkdf2-sha512$310000$GjslCZ8GAperXUFzmFGslA$QsQHK.HbuvMIiH5Q2vnM1cYR5N.yNjc6RDNU0RBnqVpJjySvjZBQa1dteceTNtvgQz7hXPlnSpRzKTGYj/k.Hw";
consent_mode = "implicit";
redirect_uris = [ "https://${svc_domain}/auth/openid/${vikunjaOIDCName}" ];
scopes = [
"openid"
"profile"
"email"
];
}];
# persistence
environment.persistence.root.directories = [
{ directory = "/var/lib/private/vikunja"; mode = "0700"; }
];
# setup postgresql
services.postgresql = let
user = config.services.vikunja.database.user;
in {
ensureDatabases = [ user ];
ensureUsers = [{
name = user;
ensureDBOwnership = true;
}];
};
# nginx
services.nginx.virtualHosts."${svc_domain}" = {
forceSSL = true;
enableACME = true;
locations."/".proxyPass = "http://localhost:${builtins.toString config.services.vikunja.port}";
};
# meta
mine.shared.meta.vikunja = rec {
name = "Vikunja";
description = ''
The to-do app to organize your life.
'';
url = "https://${svc_domain}";
package = let
pkg = config.services.vikunja.package;
in {
name = pkg.pname;
version = pkg.version;
meta = pkg.meta;
};
};
}

View file

@ -1,4 +1,4 @@
{ config, lib, ... }:
{ config, ... }:
let
svc_domain = "wger.${config.mine.shared.settings.domain}";
@ -20,26 +20,19 @@ in {
wgerSettings = {
EMAIL_FROM = "wger Workout Manager <wger@${config.mine.shared.settings.domain}>";
# use authelia for authentication (disable guest users + regisration)
AUTH_PROXY_HEADER = config.mine.shared.lib.authelia.protectedHeaders.username;
ALLOW_GUEST_USERS = false;
ALLOW_REGISTRATION = false;
};
# django specific settings
djangoSettings = let
headerToDjangoHeader = v: "HTTP_" + (lib.toUpper ((lib.replaceStrings [ "-" ] [ "_" ] v)));
in rec {
djangoSettings = rec {
# setup site stuff
SITE_URL = "https://${svc_domain}";
CSRF_TRUSTED_ORIGINS = [ "https://${svc_domain}" ];
ALLOWED_HOSTS = [ svc_domain ];
# proxy auth
AUTH_PROXY_HEADER = headerToDjangoHeader config.mine.shared.lib.authelia.protectedHeaders.username;
AUTH_PROXY_USER_EMAIL_HEADER = headerToDjangoHeader config.mine.shared.lib.authelia.protectedHeaders.email;
AUTH_PROXY_USER_NAME_HEADER = headerToDjangoHeader config.mine.shared.lib.authelia.protectedHeaders.name;
AUTH_PROXY_TRUSTED_IPS = [ "127.0.0.1" ];
AUTH_PROXY_CREATE_UNKNOWN_USER = true;
# setup email
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend";
EMAIL_HOST = config.mine.shared.settings.mail.domain_smtp;
@ -49,21 +42,6 @@ in {
EMAIL_HOST_PASSWORD = "file:${config.age.secrets.wger-ldap-pass.path}";
EMAIL_FROM_ADDRESS = config.services.wger.wgerSettings.EMAIL_FROM;
EMAIL_PAGE_DOMAIN = SITE_URL;
# LOGGING = {
# version = 1;
# disable_existing_loggers = false;
# formatters.simple.format = "%(levelname)s %(asctime)s %(module)s %(message)s";
# handlers.console = {
# level = "DEBUG";
# class = "logging.StreamHandler";
# formatter = "simple";
# };
# loggers."" = {
# handlers = ["console"];
# level = "DEBUG";
# };
# };
};
};
@ -84,7 +62,7 @@ in {
locations."/api".proxyPass = "http://localhost:${builtins.toString port}";
};
# setup lldap user for wger that can send emails
# setup lldap user for authelia that can send emails
services.lldap.provision.users = config.mine.shared.lib.ldap.mkScope (lconfig: llib: {
wger = llib.mkProvisionUserSystem "wger" config.age.secrets.wger-ldap-pass.path;
});

View file

@ -1,104 +1,65 @@
{
lib
, python
, fetchFromGitHub
, buildPythonPackage
, callPackage
, writeText
, fetchpatch
# build systems
, hatchling
# deps
, bleach
, celery
, django-crispy-bootstrap5
, django
, django-activity-stream ? callPackage ./django-activity-stream.nix {}
, django-axes
, django-compressor
, django-cors-headers
, django-crispy-forms
, django-email-verification ? callPackage ./django-email-verification.nix {}
, django-environ
, django-filter
, django-formtools
, django-prometheus
, django-recaptcha ? callPackage ./django-recaptcha.nix {}
, django-simple-history
, django-sortedm2m ? callPackage ./django-sortedm2m.nix {}
, django-storages
, djangorestframework
, djangorestframework-simplejwt
, drf-spectacular
, easy-thumbnails
, flower
, fontawesomefree
, icalendar
, invoke
, openfoodfacts ? callPackage ./openfoodfacts.nix {}
, pillow
, reportlab
, requests
, tqdm
, tzdata
# extra deps
, redis
, django-redis
, drf-spectacular-sidecar
, django-bootstrap-breadcrumbs ? callPackage ./django-bootstrap-breadcrumbs.nix {}
, psycopg2
lib,
python3,
fetchFromGitHub,
callPackage,
writeText,
fetchpatch,
}:
let
frontend = callPackage ./frontend.nix {};
in buildPythonPackage rec {
in python3.pkgs.buildPythonPackage rec {
pname = "wger";
version = "2.3";
version = "unstable-2024-12-30";
pyproject = true;
# src = fetchFromGitHub {
# owner = "wger-project";
# repo = "wger";
# rev = version;
# hash = "sha256-riJyVl0/GwAGkcHVzkJc666owPk1E4ca8DV5qTjEbjk=";
# };
# TMP: until it's merged
src = fetchFromGitHub {
owner = "eyJhb";
owner = "wger-project";
repo = "wger";
rev = "proxyauthheaderv2";
hash = "sha256-9GMU7CSMKcgBFYrUh6m9LFiJQ7XLkhaJ8EPt+FSZFqY=";
rev = "30871d621fa6e732f07bd33d4112b99539974e5f";
hash = "sha256-WcycWbzKug8vUfNnUDhvgmj1kUCpT1P1YJBfdIC1H9g=";
};
# src = /tmp/wger;
build-system = [
hatchling
python3.pkgs.hatchling
];
patches = [
./patches/pyproject.patch
./patches/manage.patch
./patches/exercises-no-gifs.patch
# adds support for proxy auth header
(fetchpatch {
url = "https://github.com/wger-project/wger/pull/1859/commits/331b2d5d2d520411a7b75193823bbc175802e547.patch";
sha256 = "sha256-5OuuInEO8e7OuWaI311HeHp5Pl6bZmix6wLDn8bEgR4=";
})
];
propagatedBuildInputs = [
# dependencies = with python3.pkgs; [
propagatedBuildInputs = with python3.pkgs; [
bleach
celery
django-crispy-bootstrap5
django
django-activity-stream
# django-activity-stream
(python3.pkgs.callPackage ./django-activity-stream.nix {})
django-axes
django-compressor
django-cors-headers
django-crispy-forms
django-email-verification
# django-email-verification
(python3.pkgs.callPackage ./django-email-verification.nix {})
django-environ
django-filter
django-formtools
django-prometheus
django-recaptcha
# django-recaptcha
(python3.pkgs.callPackage ./django-recaptcha.nix {})
django-simple-history
django-sortedm2m
# django-sortedm2m
(python3.pkgs.callPackage ./django-sortedm2m.nix {})
django-storages
djangorestframework
djangorestframework-simplejwt
@ -108,7 +69,8 @@ in buildPythonPackage rec {
fontawesomefree
icalendar
invoke
openfoodfacts
# openfoodfacts
(python3.pkgs.callPackage ./openfoodfacts.nix {})
pillow
reportlab
requests
@ -119,7 +81,7 @@ in buildPythonPackage rec {
redis
django-redis
drf-spectacular-sidecar
django-bootstrap-breadcrumbs
(python3.pkgs.callPackage ./django-bootstrap-breadcrumbs.nix {})
psycopg2
];
@ -140,7 +102,7 @@ in buildPythonPackage rec {
'';
in ''
# copy over static yarn things
# cp -a ${frontend}/static/yarn $out/${python.sitePackages}/wger/core/static
# cp -a ${frontend}/static/yarn $out/${python3.sitePackages}/wger/core/static
cp -a ${frontend}/static/yarn wger/core/static
python3 -m wger create-settings -s $PWD/tmp_settings.py
@ -148,18 +110,18 @@ in buildPythonPackage rec {
mkdir tmpstatic
pushd tmpstatic
static=. WGER_SETTINGS=../tmp_settings.py python ../manage.py collectstatic --no-input
static=. WGER_SETTINGS=../tmp_settings.py python ../manage.py compress --force
static=. WGER_SETTINGS=../tmp_settings.py python3 ../manage.py collectstatic --no-input
static=. WGER_SETTINGS=../tmp_settings.py python3 ../manage.py compress --force
popd
'';
postInstall = ''
rm -rf $out/${python.sitePackages}/wger/core/static
cp -a tmpstatic $out/${python.sitePackages}/wger/core/static
rm -rf $out/${python3.sitePackages}/wger/core/static
cp -a tmpstatic $out/${python3.sitePackages}/wger/core/static
mkdir $out/share
cp -a $out/${python.sitePackages}/wger/core/static $out/share
cp -a $out/${python3.sitePackages}/wger/core/static $out/share
'';
pythonImportsCheck = [

View file

@ -7,7 +7,7 @@ let
defaultUser = "wger";
wgerpkgs = pkgs.python3Packages.callPackage ./default.nix {};
wgerpkgs = pkgs.callPackage ./default.nix {};
# generate settings files
settingsFormat = pkgs.formats.json {};
@ -210,7 +210,7 @@ in
pythonEnv = pkgs.python3.withPackages (ps: with ps; [
gunicorn
# TODO: fix this, it should work with cfg.package
(ps.callPackage ./default.nix {})
(pkgs.python3Packages.callPackage ./default.nix {})
]);
in ''
# initial setup
@ -220,7 +220,7 @@ in
# run server
# ${cfg.package}/bin/wger start -s ${settingsFile}
PYTHONPATH="${pythonEnv}/${pkgs.python3.sitePackages}:${settingsFileDir}" ${pythonEnv}/bin/gunicorn wger.wsgi:application --bind ${cfg.address}:${builtins.toString cfg.port}
PYTHONPATH="${pythonEnv}/${pkgs.python3.sitePackages}:${settingsFileDir}" ${pythonEnv}/bin/gunicorn wger.wsgi:application --reload --bind ${cfg.address}:${builtins.toString cfg.port}
'';
serviceConfig = {

View file

@ -1,22 +1,24 @@
diff --git a/pyproject.toml b/pyproject.toml
index 354f492f6..7163c4ffe 100644
index f10460b1e..62377bd9c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -37,6 +37,8 @@ Changelog = "https://wger.readthedocs.io/en/latest/changelog.html"
@@ -35,7 +35,8 @@ Issues = "https://github.com/wger-project/wger/issues"
Changelog = "https://wger.readthedocs.io/en/latest/changelog.html"
[project.scripts]
wger = "wger.__main__:main"
-wger = "wger.__main__:main"
+wger = "wger.tasks:main"
+manage = "wger.manage:main"
+
[tool.setuptools]
include-package-data = false
@@ -53,6 +55,8 @@ universal = 1
# path = "wger/__init__.py"
# expression = "get_version()"
@@ -47,6 +48,9 @@ dependencies = { file = ["requirements.txt"] }
[tool.distutils.bdist_wheel]
universal = 1
+[tool.hatch.build.targets.wheel.force-include]
+"wger/settings_global.py" = "wger/settings_global.py"
+
[tool.ruff]
# Exclude a variety of commonly ignored directories.

View file

@ -1,6 +1,2 @@
# Fricloud Server Configuration!
Bla bla bla, something better at some point, big TODO.

View file

@ -33,8 +33,6 @@
# nextcloud
nextcloud-admin-pass.file = ./nextcloud/admin-pass.age;
nextcloud-secrets.file = ./nextcloud/secrets.age;
nextcloud-smtp-pass.file = ./nextcloud/smtp-pass.age;
nextcloud-serverinfo-token.file = ./nextcloud/serverinfo-token.age;
# stalwart
stalwart-admin-fallback-password.file = ./stalwart/admin-fallback-password.age;
@ -52,26 +50,6 @@
# searx
searx-env.file = ./searx/env.age;
# uptime-kuma
uptime-kuma-ldap-pass.file = ./uptime-kuma/ldap-pass.age;
# rallly
rallly-ldap-pass.file = ./rallly/ldap-pass.age;
rallly-env.file = ./rallly/env.age;
# notify
notify-ldap-pass.file = ./notify/ldap-pass.age;
notify-env.file = ./notify/env.age;
# grafana
grafana-authelia-secret.file = ./grafana/authelia-secret.age;
# drasl
drasl-env.file = ./drasl/env.age;
# vikunja
vikunja-env.file = ./vikunja/env.age;
};
users.groups.secrets-lldap-bind-user-pass = {};

View file

@ -1,12 +0,0 @@
age-encryption.org/v1
-> ssh-ed25519 QSDXqg ebgFyJS5wy6KlYd+FCwIr8E8f9BGXVS+buEXS/+h0xE
VsUaM8sZzvhBidHvmhlf8VBEHTWmY1+R/5gKF3BWQyA
-> X25519 zKGYd4fUcN0WOm29enxa9sdu2IASPyjZ2RMpH8AAMAA
tbZofRRRnTKaKwI5GBw3gf0gvIsWcH3mv8jr4v9Okd4
-> ssh-ed25519 n8n9DQ u2gNkt7dggt++rZGevmIKVjX73M9v04opNq2YuAynD8
7YHtRXzVmD1LQeJtcWnSsKKUAL/DKTxfGDFUTC+nNMM
-> ssh-ed25519 BTp6UA fESw3HOP8rvsUgeDKm+BCT5h5HnMbzjlrzU6en6mfGo
Bz0BOmOgDz3wrSaHz7eDe1Y70dpzuRLOdjALmCN14UA
--- vDJkQ31TTcesjWK6t5LNIjPQp3d10i2NRU1lITQDZEI
%ü~Òf¶éÁ Ѳ¿qÓ?Gk…É<46>þç±yª<79>Çâ7ÛÜ“(»¾<<3C>U±åE¯ãH%,ð¹@o€½¤c
ìd“Áö=?zS†ÛPŒl¯þµ~…JÌôVØ[JúŽ;”-ßþvfHÙ¢©ô•

Binary file not shown.

View file

@ -1,11 +0,0 @@
age-encryption.org/v1
-> ssh-ed25519 QSDXqg mcA7aWulfqHTARfxzs9ECZaJRMZKLxZgl4uYXrsL6Tk
IOKrdtTiG/Wc8qQb5zip1F3B4BHAGkEw8hjz22UY80k
-> X25519 kqD2VC9Vw/2rrd/C1TR5He/78anx3UYXNbjs0vNXCz4
ZYenf1LK+YAlil/oiZIfGGyaK9S6pt8LLpCbmlaKn9s
-> ssh-ed25519 n8n9DQ PlW/1TA71RhclXIC2RlKUUOnqOq3qWy8yshqgM3Nu10
2j6c3UjFc/RJJrqeWIezHx53DcPHFPi5a8WXnyqkXhU
-> ssh-ed25519 BTp6UA n2idpPd9RFDbzvD2svo3A0NU7kx1nUEYzwFs0gpxn3Q
/4F5l1dXBvF0nWXvT8nxPPCAxB4heeUMSBrGMY3gfng
--- 7xw3+Ket2jYmH8wsoG2ivWUYLkyoR0et5FELrn+zzMo
9XzvèäJºEŠó«y⺈†è}\šÙ©‰ï\xÓºeè”11ûõ¯ƒô7XÒÑb%„á Õ˜.…ïj‰!‹Ä6œšBÃ[/ÆÀx!8Àâ‹ÕÔÿÿÍ´¤'2ŠvRúž§4W:]k

Binary file not shown.

View file

@ -1,11 +0,0 @@
age-encryption.org/v1
-> ssh-ed25519 QSDXqg nLdvh4Rh7NRfCpubsUOaSTwL+uQYa9jpiWWHmq8tBzo
jIgDAQZAmpoOqShDWMZZC3m/go+DImfYbg+gOlbbLu4
-> X25519 jJ3QUtYdo6FM/xncqZeJMg5JJh2PKhe8rDw46ZrbqWo
uoDuPBJDplDoRiJGi2NFNJqDlo/fRGUqPiD0Jk6AX1c
-> ssh-ed25519 n8n9DQ +3vT7Jfx+kUFbHbEAWFN0hiDn0c0m+65brjuM5M4HRI
+jGGD9trmPr0BV2Ev1PvcdTAbzEyrHtHGleuheuYrIY
-> ssh-ed25519 BTp6UA Da7JqYJiJToDKhRelrwbXCj35URUi9T/Zzr0fLAZX1A
Kyi0O0Wog/VYlnCezm9qyxHiEU606kVHZfp17NKxXQk
--- 2t7lCNkYh/E4RyFx7sAtup5z9z/UFcxvk4XHhfJK+4I
òÊ¡ "<22>V˜nê¢ú]«„þ·ÁhQYŽs¡Y9ÎY®^€rã®ÔÑ6lƒ6*@G{vœRf÷°IÐù7

View file

@ -1,11 +0,0 @@
age-encryption.org/v1
-> ssh-ed25519 QSDXqg sB2scYcZRgd5E/6R27PdImZnUWkJupnHczQQYGZsCD0
FzInWenN2BSOcxXo+vfTEUuMyRfyAuGqyFSp+ByC/U4
-> X25519 a+pbMWxxfI9QYkoXQFojS0FD8WnF+uyGXVlW2zZvaxo
xVZQXcCrW7GIPDa6/n+Zh+Iua5wLO886cgn0aFOzGNQ
-> ssh-ed25519 n8n9DQ LCG5mLWudd8IIFPB/N2ZjnWmNvClDq4p1VSsmxhVdHA
Lv4a9eDu7pZhgYdYziXnIXZEovk0zlkRoweH5zIRW8s
-> ssh-ed25519 BTp6UA dQBcqLui7exxZYoPEtOFtDUFXQUhpxf/XOCkrR1fCSI
ysZfoPv2q6E0dbHxgdsPqCXs2d7ZUMbTJbma3y285E8
--- VhWbBMj96n+u7SuNCMRhI0NF1q4g0yGbyDFL6u5ivrk
“„Ú<LûS¨ô¢3øZGò"aÌ„•ZBÉ.Q•¤Ä ½Ã+‡Ôª„<C2AA>½.¹`z$SÑY_“r×ê߉µWГµñVf«™·€Þ¢ÀßÐɰzĺ<]Bß<<3C> »”å‰à; ^É^²°®o€*å0úa0ë<7F>.6ˆ=k¬Gæê~HÁêŠfb¯0û¦vâÓ 7_¦Äá™~FÎdœÇ•

View file

@ -1,11 +0,0 @@
age-encryption.org/v1
-> ssh-ed25519 QSDXqg zhjn7qGDqDLTSqEDpiY1x5hHaH288k84Gdd0X8GDMio
90o18HnPHL1/kBDDVLS3nzGFtk+R4u4kKZd/l5IrkOM
-> X25519 BiI48P3ND0r5r8uQNMBj5tRhzqFLvdG/eT0cI8HJ21Q
ezuOpM12szomaVkj9K4KyCEoIlzxWti22hWsYtWPuic
-> ssh-ed25519 n8n9DQ SlOFCYQM2ANAdx9gijm7/WLQ5/0jOhj5BTxYZe+s4SU
nFHlb96Pq3YAKJ6ug7jvbZIykc9nup6MxRd4iEfOPyo
-> ssh-ed25519 BTp6UA Ud6jj4XFF5FrBotbDpdjLkvKz/cZckQEBXn3IaV55m0
xRIZrraNuGszKWhDLlJW5JswPbtkT1oUvdwPNbm3Bus
--- fxKZwnhoQA3D2XfZ7zEAGrQBEmL0VS3f9JykUuvcTeA
bÿ\Ð;šKTGßš´NÙsÀi@ó>«]fðq üXpŒtL#ånàÈÝÒrì¨<C3AC>;hIž?F0=°a

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -43,8 +43,6 @@ in
# nextcloud
"nextcloud/admin-pass.age".publicKeys = defaultAccess;
"nextcloud/secrets.age".publicKeys = defaultAccess;
"nextcloud/smtp-pass.age".publicKeys = defaultAccess;
"nextcloud/serverinfo-token.age".publicKeys = defaultAccess;
# mailserver/stalwart
"stalwart/admin-fallback-password.age".publicKeys = defaultAccess;
@ -62,24 +60,4 @@ in
# searx
"searx/env.age".publicKeys = defaultAccess;
# uptime-kuma
"uptime-kuma/ldap-pass.age".publicKeys = defaultAccess;
# rallly
"rallly/ldap-pass.age".publicKeys = defaultAccess;
"rallly/env.age".publicKeys = defaultAccess;
# notify
"notify/ldap-pass.age".publicKeys = defaultAccess;
"notify/env.age".publicKeys = defaultAccess;
# grafana
"grafana/authelia-secret.age".publicKeys = defaultAccess;
# drasl
"drasl/env.age".publicKeys = defaultAccess;
# vikunja
"vikunja/env.age".publicKeys = defaultAccess;
}

Binary file not shown.

Binary file not shown.

View file

@ -10,16 +10,6 @@ let
-out "$out/ca.pem" -keyout "$out/ca.key"
'';
in {
# block all /metrics endpoints
options.services.nginx.virtualHosts = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule {
config.locations."/metrics" = lib.mkDefault {
extraConfig = "deny all;";
};
});
};
config = {
services.nginx = {
enable = true;
@ -62,6 +52,5 @@ in {
allowedTCPPorts = [80 443];
allowedUDPPorts = [443];
};
};
}

View file

@ -2,6 +2,8 @@
{
services.restic = {
# enable = true;
backups.main = {
repository = "b2:situla-${config.mine.shared.settings.brand_lower}:.";

View file

@ -1,5 +1,3 @@
{ pkgs, ... }:
{
services = {
openssh = {
@ -17,10 +15,6 @@
};
};
environment.systemPackages = with pkgs; [
vim
];
nix = {
settings.auto-optimise-store = true;
gc = {

View file

@ -10,15 +10,9 @@ in {
default = {};
};
# config = {
# mine.zfsMounts = let
# zfsFilesystems = lib.filterAttrs (_: v: v.fsType == "zfs") config.fileSystems;
# in lib.mapAttrs' (_: v: lib.nameValuePair v.device v.mountPoint) zfsFilesystems;
# };
# TODO: fix this better. We just do this, so we do not rely on fileSystems, otherwise we cannot
# use this with impermanence
config = {
mine.zfsMounts = lib.mapAttrs' (n: v: lib.nameValuePair ("rpool/" + n) v.mountpoint) config.mine.disks.pools.rpool.datasets;
mine.zfsMounts = let
zfsFilesystems = lib.filterAttrs (_: v: v.fsType == "zfs") config.fileSystems;
in lib.mapAttrs' (_: v: lib.nameValuePair v.device v.mountPoint) zfsFilesystems;
};
}

View file

@ -1,24 +0,0 @@
diff --git a/flake.nix b/flake.nix
index f6cfa25..68358a8 100644
--- a/flake.nix
+++ b/flake.nix
@@ -101,6 +101,7 @@
in {
options.services.drasl = {
enable = mkEnableOption (lib.mdDoc ''drasl'');
+ package = mkPackageOption { drasl = self.defaultPackage.${pkgs.system}; } "drasl" {};
settings = mkOption {
type = format.type;
default = {};
@@ -115,10 +116,9 @@
wantedBy = ["multi-user.target"];
serviceConfig = let
- pkg = self.defaultPackage.${pkgs.system};
config = format.generate "config.toml" cfg.settings;
in {
- ExecStart = "${pkg}/bin/drasl -config ${config}";
+ ExecStart = "${cfg.package}/bin/drasl -config ${config}";
DynamicUser = true;
StateDirectory = "drasl";
Restart = "always";

View file

@ -1,14 +0,0 @@
diff --git a/config.go b/config.go
index 24e17b5..11194e6 100644
--- a/config.go
+++ b/config.go
@@ -393,6 +393,9 @@ func CleanConfig(config *Config) error {
return fmt.Errorf("Duplicate RegistrationOIDC Name: %s", oidcConfig.Name)
}
oidcNames.Add(oidcConfig.Name)
+ envkey := fmt.Sprintf("DRASL_REGISTRATION_OIDC_%s_CLIENT_SECRET", strings.ToUpper(oidcConfig.Name))
+ envvalue := strings.TrimSpace(Getenv(envkey, oidcConfig.ClientSecret))
+ oidcConfig.ClientSecret = envvalue
oidcConfig.Issuer, err = cleanURL(
fmt.Sprintf("RegistrationOIDC %s Issuer", oidcConfig.Name),
mo.Some("https://idm.example.com/oauth2/openid/drasl"),

View file

@ -1,126 +0,0 @@
{
lib
, buildNpmPackage
, fetchFromGitHub
, yarn
, nodejs_20
, inter # icons
# , prisma-old ? null
, oldpkgs ? (import (builtins.fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/ab7b6889ae9d484eed2876868209e33eb262511d.tar.gz";
}) {})
, rallly-prisma ? oldpkgs.prisma
, rallly-prisma-engines ? oldpkgs.prisma-engines
}:
buildNpmPackage rec {
pname = "rallly";
version = "3.11.2";
src = fetchFromGitHub {
owner = "lukevella";
repo = "rallly";
rev = "v${version}";
hash = "sha256-ej6Y0ouiheoH6dSBWsSIW6qt9UvsLh9ODDQA5Fqt3zs=";
};
nodejs = nodejs_20;
npmDepsHash = "sha256-sAs1DhegfI1YbE/Xy2Jzjx1RKYOUgc1Ww7hLL7+f8ZU=";
patches = [
./font.patch
];
postPatch = ''
cp ${./package-lock.json} package-lock.json
'';
SKIP_ENV_VALIDATION = 1;
NEXT_PUBLIC_SELF_HOSTED = "true";
NEXT_PUBLIC_APP_VERSION = version;
preBuild = ''
# We have to pass and bake in the Ollama URL into the package
# Replace the googleapis.com Inter font with a local copy from nixpkgs
cp "${inter}/share/fonts/truetype/InterVariable.ttf" apps/web/Inter.ttf
cp -a packages/database/prisma .
${rallly-prisma}/bin/prisma generate
'';
postBuild = ''
# Add a shebang to the server js file, then patch the shebang to use a nixpkgs nodejs binary.
sed -i '1s|^|#!/usr/bin/env node\n|' apps/web/.next/standalone/apps/web/server.js
patchShebangs ./apps/web/.next/standalone/apps/web/server.js
'';
installPhase = ''
runHook preInstall
cd apps/web
mkdir -p $out/{share,bin}
cp -r .next/standalone $out/share/rallly
cp -a public/. $out/share/rallly/apps/web/public
mkdir -p $out/share/rallly/.next
cp -r .next/static $out/share/rallly/apps/web/.next/static
cp -r ../../prisma $out/share/rallly/apps/web
chmod +x $out/share/rallly/apps/web/server.js
# This patch must be applied here, as it's patching the `dist` directory
# of NextJS. Without this, homepage-dashboard errors when trying to
# write its prerender cache.
#
# This patch ensures that the cache implementation respects the env
# variable `NIXPKGS_HOMEPAGE_CACHE_DIR`, which is set by default in the
# wrapper below.
(cd "$out" && patch -p1 <${./prerender_cache_path.patch})
# we set a default port to support "nix run ..."
makeWrapper $out/share/rallly/apps/web/server.js $out/bin/rallly \
--set-default NIXPKGS_RALLLY_CACHE_DIR /var/cache/rallly
makeWrapper ${rallly-prisma}/bin/prisma $out/bin/rallly-prisma \
--chdir $out/share/rallly/apps/web/prisma \
--append-flags --schema=./schema.prisma
runHook postInstall
'';
# TODO(eyJhb): openssl is needed, but I can't figure out
# how to add it best...?
nativeBuildInputs = [
(yarn.override {nodejs = nodejs_20; })
rallly-prisma
];
passthru = {
inherit rallly-prisma rallly-prisma-engines;
};
meta = with lib; {
homepage = "https://rallly.co/";
changelog = "https://github.com/lukevella/rallly/releases/tag/v${version}";
description = "Rallly is an open-source scheduling and collaboration tool designed to make organizing events and meetings easier";
longDescription = ''
Rallly is an open-source scheduling and collaboration tool
designed to make organizing events and meetings easier.
Schedule group meetings with friends, colleagues and teams.
Create meeting polls to find the best date and time to organize an event
based on your participants' availability. Save time and avoid back-and-forth emails.
'';
license = licenses.agpl3Plus;
maintainers = with maintainers; [
eyjhb
];
mainProgram = "rallly";
};
}

View file

@ -1,48 +0,0 @@
diff --git a/apps/web/src/app/[locale]/layout.tsx b/apps/web/src/app/[locale]/layout.tsx
index ee7d143c..d70ecdfd 100644
--- a/apps/web/src/app/[locale]/layout.tsx
+++ b/apps/web/src/app/[locale]/layout.tsx
@@ -4,16 +4,13 @@ import "../../style.css";
import languages from "@rallly/languages";
import { Toaster } from "@rallly/ui/toaster";
import { Viewport } from "next";
-import { Inter } from "next/font/google";
+import localFont from "next/font/local";
import React from "react";
import { TimeZoneChangeDetector } from "@/app/[locale]/timezone-change-detector";
import { Providers } from "@/app/providers";
-const inter = Inter({
- subsets: ["latin"],
- display: "swap",
-});
+const inter = localFont({ src: './../../../Inter.ttf' });
export async function generateStaticParams() {
return Object.keys(languages).map((locale) => ({ locale }));
diff --git a/apps/web/src/pages/_app.tsx b/apps/web/src/pages/_app.tsx
index b3d67d0e..16ee65a1 100644
--- a/apps/web/src/pages/_app.tsx
+++ b/apps/web/src/pages/_app.tsx
@@ -6,7 +6,7 @@ import { TooltipProvider } from "@rallly/ui/tooltip";
import { domMax, LazyMotion } from "framer-motion";
import { NextPage } from "next";
import { AppProps } from "next/app";
-import { Inter } from "next/font/google";
+import localFont from "next/font/local";
import Head from "next/head";
import { SessionProvider, signIn, useSession } from "next-auth/react";
import React from "react";
@@ -19,10 +19,7 @@ import { trpc } from "@/utils/trpc/client";
import { NextPageWithLayout } from "../types";
-const inter = Inter({
- subsets: ["latin"],
- display: "swap",
-});
+const inter = localFont({ src: './../../Inter.ttf' });
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout;

File diff suppressed because it is too large Load diff

View file

@ -1,21 +0,0 @@
diff --git a/share/rallly/node_modules/next/dist/server/lib/incremental-cache/file-system-cache.js b/share/rallly/node_modules/next/dist/server/lib/incremental-cache/file-system-cache.js
index f3be830..fdafb45 100755
--- a/share/rallly/node_modules/next/dist/server/lib/incremental-cache/file-system-cache.js
+++ b/share/rallly/node_modules/next/dist/server/lib/incremental-cache/file-system-cache.js
@@ -10,6 +10,7 @@ Object.defineProperty(exports, "default", {
});
const _lrucache = /*#__PURE__*/ _interop_require_default(require("next/dist/compiled/lru-cache"));
const _path = /*#__PURE__*/ _interop_require_default(require("../../../shared/lib/isomorphic/path"));
+var path = require('node:path');
const _constants = require("../../../lib/constants");
function _interop_require_default(obj) {
return obj && obj.__esModule ? obj : {
@@ -22,7 +23,7 @@ class FileSystemCache {
constructor(ctx){
this.fs = ctx.fs;
this.flushToDisk = ctx.flushToDisk;
- this.serverDistDir = ctx.serverDistDir;
+ this.serverDistDir = path.join(process.env.NIXPKGS_RALLLY_CACHE_DIR, "rallly");
this.appDir = !!ctx._appDir;
this.pagesDir = !!ctx._pagesDir;
this.revalidatedTags = ctx.revalidatedTags;

View file

@ -20,14 +20,4 @@ in sources // {
# })
];
};
drasl = pkgs.applyPatches {
src = sources.drasl;
name = "drasl-patched";
patches = [
./../patches/drasl-flakes-nix-add-option-package.patch
./../patches/drasl-registration-oidc-env.patch
];
};
}

View file

@ -17,41 +17,10 @@
"homepage": "",
"owner": "nix-community",
"repo": "disko",
"rev": "51d33bbb7f1e74ba5f9d9a77357735149da99081",
"sha256": "0fg2ym4kc1pcayfg4jka742512r8nackwl8w1syxvg82yasixnjc",
"rev": "18d0a984cc2bc82cf61df19523a34ad463aa7f54",
"sha256": "02i8sgnjzk4srk4j7qnjzig0rd4ip7a1vpbi5ynaqs1hh56q10r9",
"type": "tarball",
"url": "https://github.com/nix-community/disko/archive/51d33bbb7f1e74ba5f9d9a77357735149da99081.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"drasl": {
"sha256": "08fxv66qx5a8q52ci0hw2yvxx14a3mdsds5i79brxc1hilxiaksw",
"type": "tarball",
"url": "https://github.com/unmojang/drasl/archive/v3.0.0.tar.gz",
"url_template": "https://github.com/unmojang/drasl/archive/<version>.tar.gz",
"version": "v3.0.0"
},
"drtvrss": {
"branch": "main",
"description": "RSS feeds for DRTV",
"homepage": null,
"owner": "RasmusRendal",
"repo": "drtvrss",
"rev": "1234121a3f615d80bc18107768182fb43df0bbac",
"sha256": "0yxarbbsj4giyszc8pf64d0gy9qsld9skgdxxfgygrgk2wspycnc",
"type": "tarball",
"url": "https://github.com/RasmusRendal/drtvrss/archive/1234121a3f615d80bc18107768182fb43df0bbac.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"flake-compat": {
"branch": "master",
"description": null,
"homepage": null,
"owner": "edolstra",
"repo": "flake-compat",
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
"sha256": "19d2z6xsvpxm184m41qrpi1bplilwipgnzv9jy17fgw421785q1m",
"type": "tarball",
"url": "https://github.com/edolstra/flake-compat/archive/ff81ac966bb2cae68946d5ed5fc4994f96d0ffec.tar.gz",
"url": "https://github.com/nix-community/disko/archive/18d0a984cc2bc82cf61df19523a34ad463aa7f54.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"impermanence": {
@ -72,10 +41,10 @@
"homepage": null,
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b024ced1aac25639f8ca8fdfc2f8c4fbd66c48ef",
"sha256": "09dahi81cn02gnzsc8a00n945dxc18656ar0ffx5vgxjj1nhgsvy",
"rev": "3a228057f5b619feb3186e986dbe76278d707b6e",
"sha256": "0iqla32cwy147lx1jw84174qkf8jyd912vxqjfgggyil1k8fix66",
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/b024ced1aac25639f8ca8fdfc2f8c4fbd66c48ef.tar.gz",
"url": "https://github.com/NixOS/nixpkgs/archive/3a228057f5b619feb3186e986dbe76278d707b6e.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
}
}