Compare commits
No commits in common. "main" and "element" have entirely different histories.
129 changed files with 405 additions and 32489 deletions
21
LICENSE
21
LICENSE
|
@ -1,21 +0,0 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2025 eyJhb and the Fricloud.dk contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -5,7 +5,6 @@ set -ex
|
|||
USERNAME="root"
|
||||
IP="gerd.fricloud.dk"
|
||||
NIXPKGS=$(nix build --impure --json --expr '(import ./shared/sources).nixpkgs' | jq -r '.[].outputs.out')
|
||||
NIXPKGS=$(nix eval --impure --json --expr '(import ./shared/sources/default.nix).nixpkgs.outPath' | jq -r)
|
||||
|
||||
export NIX_PATH="nixpkgs=$NIXPKGS"
|
||||
|
||||
|
|
|
@ -4,37 +4,24 @@
|
|||
|
||||
./../shared/applications/server/acme.nix
|
||||
./../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/server/postgresql.nix
|
||||
./../shared/applications/state/postgresql.nix
|
||||
./../shared/applications/state/ssh.nix
|
||||
|
||||
./gerd/services/fricloud-website.nix
|
||||
./gerd/services/member-website
|
||||
./gerd/services/lldap
|
||||
./gerd/services/lldap.nix
|
||||
./gerd/services/authelia
|
||||
./gerd/services/forgejo
|
||||
./gerd/services/teeworlds.nix
|
||||
./gerd/services/murmur.nix
|
||||
./gerd/services/hedgedoc.nix
|
||||
./gerd/services/cyberchef.nix
|
||||
./gerd/services/nextcloud.nix
|
||||
./gerd/services/nextcloud.nix
|
||||
./gerd/services/stalwart
|
||||
./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/headscale
|
||||
|
||||
./gerd/services/monitoring
|
||||
./gerd/services/element.nix
|
||||
./gerd/services/matrix-synapse.nix
|
||||
];
|
||||
|
||||
networking.hostName = "gerd";
|
||||
|
@ -45,15 +32,11 @@
|
|||
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"; };
|
||||
};
|
||||
|
@ -66,24 +49,11 @@
|
|||
platforms.hetzner = {
|
||||
enable = true;
|
||||
network.address = [
|
||||
"65.108.221.240"
|
||||
"65.108.221.240/32"
|
||||
"2a01:4f9:c012:743e::1/64"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
# setup zramswap (we are very ram limited)
|
||||
zramSwap = {
|
||||
enable = true;
|
||||
memoryPercent = 75;
|
||||
algorithm = "lz4";
|
||||
};
|
||||
|
||||
|
||||
# TMP FIX FOR https://github.com/nix-community/impermanence/issues/229
|
||||
boot.initrd.systemd.suppressedUnits = [ "systemd-machine-id-commit.service" ];
|
||||
systemd.suppressedSystemUnits = [ "systemd-machine-id-commit.service" ];
|
||||
|
||||
|
||||
system.stateVersion = "24.11";
|
||||
}
|
||||
|
|
|
@ -51,10 +51,10 @@ let
|
|||
auth_request_set $email $upstream_http_remote_email;
|
||||
|
||||
## Inject the metadata response headers from the variables into the request made to the backend.
|
||||
proxy_set_header ${config.mine.shared.lib.authelia.protectedHeaders.username} $user;
|
||||
proxy_set_header ${config.mine.shared.lib.authelia.protectedHeaders.groups} $groups;
|
||||
proxy_set_header ${config.mine.shared.lib.authelia.protectedHeaders.email} $email;
|
||||
proxy_set_header ${config.mine.shared.lib.authelia.protectedHeaders.name} $name;
|
||||
proxy_set_header Remote-User $user;
|
||||
proxy_set_header Remote-Groups $groups;
|
||||
proxy_set_header Remote-Email $email;
|
||||
proxy_set_header Remote-Name $name;
|
||||
|
||||
## Configure the redirection when the authz failure occurs. Lines starting with 'Modern Method' and 'Legacy Method'
|
||||
## should be commented / uncommented as pairs. The modern method uses the session cookies configuration's authelia_url
|
||||
|
@ -73,29 +73,15 @@ let
|
|||
|
||||
## Legacy Method: When there is a 401 response code from the authz endpoint redirect to the portal with the 'rd'
|
||||
## URL parameter set to $target_url. This requires users update 'auth.example.com/' with their external authelia URL.
|
||||
error_page 401 =302 https://${config.mine.shared.settings.authelia.domain}/?rd=$target_url;
|
||||
'';
|
||||
|
||||
nginxUnsetAuthHeaders = ''
|
||||
proxy_set_header ${config.mine.shared.lib.authelia.protectedHeaders.username} "";
|
||||
proxy_set_header ${config.mine.shared.lib.authelia.protectedHeaders.groups} "";
|
||||
proxy_set_header ${config.mine.shared.lib.authelia.protectedHeaders.email} "";
|
||||
proxy_set_header ${config.mine.shared.lib.authelia.protectedHeaders.name} "";
|
||||
error_page 401 =302 https://auth.fricloud.dk/?rd=$target_url;
|
||||
'';
|
||||
in {
|
||||
mine.shared.lib.authelia.mkProtectedWebsite = websiteConfig: lib.recursiveUpdate websiteConfig {
|
||||
extraConfig = (websiteConfig.extraConfig or "") + "\n" + "include ${autheliaLocation};";
|
||||
locations = lib.mapAttrs (n: v: v // { extraConfig = nginxUnsetAuthHeaders + (v.extraConfig or ""); }) (websiteConfig.locations or {});
|
||||
mine.shared.lib.authelia.mkProtectedWebsite = { vhostConfig, endpoint ? "/" }: lib.recursiveUpdate vhostConfig {
|
||||
extraConfig = (lib.attrByPath [ "extraConfig" ] "" vhostConfig) + "\n" + "include ${autheliaLocation};";
|
||||
locations."${endpoint}" = config.mine.shared.lib.authelia.mkProtectedLocation (lib.attrByPath [ "locations" endpoint ] {} vhostConfig);
|
||||
};
|
||||
|
||||
mine.shared.lib.authelia.mkProtectedLocation = vhostLocationConfig: lib.recursiveUpdate vhostLocationConfig {
|
||||
extraConfig = (lib.attrByPath [ "extraConfig" ] "" vhostLocationConfig) + "\n" + "include ${autheliaRequest};";
|
||||
};
|
||||
|
||||
mine.shared.lib.authelia.protectedHeaders = {
|
||||
username = "Remote-User";
|
||||
groups = "Remote-Groups"; # comma separated string of groups
|
||||
email = "Remote-Email";
|
||||
name = "Remote-Name";
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{ pkgs, config, ... }:
|
||||
{ config, ... }:
|
||||
|
||||
let
|
||||
svc_domain = "auth.${config.mine.shared.settings.domain}";
|
||||
|
@ -10,14 +10,6 @@ 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)"
|
||||
'';
|
||||
});
|
||||
};
|
||||
|
||||
environmentVariables.AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE = config.age.secrets.lldap-bind-user-pass.path;
|
||||
environmentVariables.AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE = config.age.secrets.authelia-smtp-password.path;
|
||||
|
@ -34,10 +26,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
|
||||
|
@ -90,35 +78,9 @@ in {
|
|||
user = config.mine.shared.settings.ldap.bind_dn;
|
||||
};
|
||||
};
|
||||
|
||||
# authelia have changed how the by-default handles auth, so in theory everything
|
||||
# should contact the `userinfo` endpoint. but not everything does, which leads to us
|
||||
# having to create a default policy for this
|
||||
# https://github.com/pulsejet/nextcloud-oidc-login/issues/311#issuecomment-2763239352
|
||||
identity_providers.oidc.claims_policies.default.id_token = [
|
||||
"rat"
|
||||
"groups"
|
||||
"email"
|
||||
"email_verified"
|
||||
"alt_emails"
|
||||
"preferred_username"
|
||||
"name"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
# 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;
|
||||
});
|
||||
|
||||
services.nginx.virtualHosts."${svc_domain}" = {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
|
@ -6,17 +6,11 @@ let
|
|||
# configure element web client
|
||||
pkg_element = pkgs.element-web.override {
|
||||
conf = {
|
||||
default_server_name = config.mine.shared.settings.domain;
|
||||
embedded_pages.login_for_welcome = true;
|
||||
default_theme = "dark";
|
||||
features.feature_latex_maths = true;
|
||||
disable_guests = true;
|
||||
|
||||
brand = config.mine.shared.settings.brand;
|
||||
default_theme = "dark";
|
||||
|
||||
features = {
|
||||
feature_latex_maths = true;
|
||||
feature_video_rooms = false;
|
||||
};
|
||||
default_server_name = config.mine.shared.settings.domain;
|
||||
};
|
||||
};
|
||||
in {
|
|
@ -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"
|
||||
|
|
|
@ -37,6 +37,10 @@ in {
|
|||
};
|
||||
};
|
||||
|
||||
# TODO(eyJhb): remove after our ban expires (and nginx config)
|
||||
# already issued for this exact set of domains in the last 168 hours: git.fricloud.dk, retry after 2024-08-10T01:34:44Z
|
||||
security.acme.certs."git.fricloud.dk".extraDomainNames = [ "git2.fricloud.dk" ];
|
||||
|
||||
services.nginx.virtualHosts."${svc_domain}" = {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
|
|
|
@ -1,26 +1,11 @@
|
|||
diff --git a/templates/user/auth/link_account.tmpl b/templates/user/auth/link_account.tmpl
|
||||
index e8bb3d409c..aa6d18b97a 100644
|
||||
index 8dd49ccd60..8cdce5e1ad 100644
|
||||
--- a/templates/user/auth/link_account.tmpl
|
||||
+++ b/templates/user/auth/link_account.tmpl
|
||||
@@ -4,12 +4,12 @@
|
||||
<div class="overflow-menu-items tw-justify-center">
|
||||
<!-- TODO handle .ShowRegistrationButton once other login bugs are fixed -->
|
||||
{{if not .AllowOnlyInternalRegistration}}
|
||||
- <a class="item {{if not .user_exists}}active{{end}}"
|
||||
+ <a class="item"
|
||||
data-tab="auth-link-signup-tab">
|
||||
{{ctx.Locale.Tr "auth.oauth_signup_tab"}}
|
||||
</a>
|
||||
{{end}}
|
||||
- <a class="item {{if .user_exists}}active{{end}}"
|
||||
+ <a class="item active"
|
||||
data-tab="auth-link-signin-tab">
|
||||
{{ctx.Locale.Tr "auth.oauth_signin_tab"}}
|
||||
</a>
|
||||
@@ -17,11 +17,11 @@
|
||||
</overflow-menu>
|
||||
<div class="ui middle very relaxed page grid">
|
||||
<div class="column tw-flex tw-flex-col tw-gap-4 tw-max-w-2xl tw-m-auto">
|
||||
<div class="column">
|
||||
- <div class="ui tab {{if not .user_exists}}active{{end}}"
|
||||
+ <div class="ui tab"
|
||||
data-tab="auth-link-signup-tab">
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl
|
||||
index d4ba664e37..2c94eafc22 100644
|
||||
index 9872096fbc..1076f90326 100644
|
||||
--- a/templates/user/auth/signin_inner.tmpl
|
||||
+++ b/templates/user/auth/signin_inner.tmpl
|
||||
@@ -11,6 +11,7 @@
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<form class="ui form" action="{{.SignInLink}}" method="post">
|
||||
+ <div {{if not .LinkAccountMode}}style="display:none;"{{end}}>
|
||||
{{.CsrfTokenHtml}}
|
||||
<div class="required field {{if and (.Err_UserName) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}">
|
||||
<label for="user_name">{{ctx.Locale.Tr "home.uname_holder"}}</label>
|
||||
@@ -43,6 +44,7 @@
|
||||
{{end}}
|
||||
</button>
|
||||
</div>
|
||||
+ </div>
|
||||
</form>
|
||||
|
||||
{{template "user/auth/oauth_container" .}}
|
||||
@@ -10,6 +10,7 @@
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<form class="ui form tw-max-w-2xl tw-m-auto" action="{{.SignInLink}}" method="post">
|
||||
+ <div {{if not .LinkAccountMode}}style="display:none;"{{end}}>
|
||||
{{.CsrfTokenHtml}}
|
||||
<div class="required field {{if and (.Err_UserName) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}">
|
||||
<label for="user_name">{{ctx.Locale.Tr "home.uname_holder"}}</label>
|
||||
@@ -53,6 +54,7 @@
|
||||
<div class="divider divider-text">
|
||||
{{ctx.Locale.Tr "sign_in_or"}}
|
||||
</div>
|
||||
+ </div>
|
||||
<div id="oauth2-login-navigator" class="tw-py-1">
|
||||
<div class="tw-flex tw-flex-col tw-justify-center">
|
||||
<div id="oauth2-login-navigator-inner" class="tw-flex tw-flex-col tw-flex-wrap tw-items-center tw-gap-2">
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
{ config, pkgs, ... }:
|
||||
|
||||
let
|
||||
svc_domain = "headscale.${config.mine.shared.settings.domain}";
|
||||
in {
|
||||
imports = [
|
||||
./headscale.nix
|
||||
./headplane.nix
|
||||
];
|
||||
|
||||
|
||||
mine.shared.meta.headscale = {
|
||||
name = "";
|
||||
description = "";
|
||||
|
||||
package = let
|
||||
pkg = pkgs.headscale;
|
||||
in {
|
||||
name = pkg.pname;
|
||||
version = pkg.version;
|
||||
meta = pkg.meta;
|
||||
};
|
||||
};
|
||||
|
||||
mine.shared.settings.headscale.domain = svc_domain;
|
||||
}
|
|
@ -1,126 +0,0 @@
|
|||
{ config, pkgs, ... }:
|
||||
|
||||
let
|
||||
svc_domain = config.mine.shared.settings.headscale.domain;
|
||||
|
||||
sources = import ./../../../../shared/sources;
|
||||
flake-compat = sources.flake-compat;
|
||||
|
||||
newpkgs = (import (builtins.fetchTarball {
|
||||
url = "https://github.com/NixOS/nixpkgs/archive/c2a03962b8e24e669fb37b7df10e7c79531ff1a4.tar.gz";
|
||||
}) {});
|
||||
|
||||
|
||||
headplanesrc = let
|
||||
tmppkgs = (import (builtins.fetchTarball {
|
||||
url = "https://github.com/NixOS/nixpkgs/archive/ab7b6889ae9d484eed2876868209e33eb262511d.tar.gz";
|
||||
}) {});
|
||||
|
||||
src = builtins.fetchTarball {
|
||||
url = "https://github.com/tale/headplane/archive/2f316176c8c37ad63946d7075c727478f81303b2.tar.gz";
|
||||
};
|
||||
in tmppkgs.applyPatches {
|
||||
src = src;
|
||||
name = "headplane-patched";
|
||||
patches = [
|
||||
(tmppkgs.writeText "headplane-package-pnpm-hash.patch" ''
|
||||
diff --git a/nix/package.nix b/nix/package.nix
|
||||
index bb430d7..11349c4 100644
|
||||
--- a/nix/package.nix
|
||||
+++ b/nix/package.nix
|
||||
@@ -23,7 +23,7 @@ stdenv.mkDerivation (finalAttrs: {
|
||||
|
||||
pnpmDeps = pnpm_10.fetchDeps {
|
||||
inherit (finalAttrs) pname version src;
|
||||
- hash = "sha256-OOWgYaGwa5PtWhFEEkRCojCDmkPIR6tJ5cfFMOLND3I=";
|
||||
+ hash = "sha256-xjjkqbgjYaAGYAmlTFE+Lq3Hp6myZKaW3br0YTDNhQA=";
|
||||
};
|
||||
'')
|
||||
];
|
||||
};
|
||||
|
||||
headplane = import flake-compat { src = headplanesrc; };
|
||||
in {
|
||||
imports = [
|
||||
headplane.defaultNix.nixosModules.headplane
|
||||
];
|
||||
|
||||
services.headplane = {
|
||||
enable = true;
|
||||
agent.enable = false;
|
||||
|
||||
settings = {
|
||||
server = {
|
||||
host = "127.0.0.1";
|
||||
port = 53874;
|
||||
cookie_secret = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; # replaced in env
|
||||
cookie_secure = true;
|
||||
};
|
||||
|
||||
headscale = {
|
||||
url = "https://${svc_domain}";
|
||||
config_strict = false;
|
||||
};
|
||||
|
||||
oidc = {
|
||||
issuer = "https://${config.mine.shared.settings.authelia.domain}";
|
||||
client_id = "headplane";
|
||||
client_secret = "<from_env>";
|
||||
redirect_uri = "https://${svc_domain}/admin/oidc/callback";
|
||||
|
||||
# headscale API key for authenticating users
|
||||
headscale_api_key = "<from_env>";
|
||||
|
||||
# default to state directory
|
||||
user_storage_file = "/var/lib/headplane/users.json";
|
||||
|
||||
# set to the default authelia auth method
|
||||
token_endpoint_auth_method = "client_secret_basic";
|
||||
|
||||
# disable authenticating with headscale api key
|
||||
disable_api_key_login = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# headplane module does not allow setting package,
|
||||
# so we have to add headplane to pkgs
|
||||
nixpkgs.overlays = [
|
||||
(self: super: {
|
||||
headplane = headplane.defaultNix.packages.x86_64-linux.headplane;
|
||||
})
|
||||
];
|
||||
|
||||
systemd.services.headplane.serviceConfig = {
|
||||
# setup state directory
|
||||
StateDirectory = "headplane";
|
||||
|
||||
# load configs from env file
|
||||
EnvironmentFile = [ config.age.secrets.headplane-env.path ];
|
||||
};
|
||||
|
||||
# setup for oidc
|
||||
services.authelia.instances.main.settings.identity_providers.oidc.clients = [{
|
||||
client_id = "headplane";
|
||||
client_name = "Headplane";
|
||||
client_secret = "$pbkdf2-sha512$310000$h7Te42JTu4Xsqz/8CGan7Q$qDd183LHmEsgNvVAI8Xf.1DpRMeS8DqNmDpkkjkxgRR/lZYQgAkXYzL2MyvLqNFFSVKAdMTsD/Jxk72g9fxnew";
|
||||
consent_mode = "implicit";
|
||||
redirect_uris = [ "https://${svc_domain}/admin/oidc/callback" ];
|
||||
scopes = [
|
||||
"openid"
|
||||
"profile"
|
||||
"email"
|
||||
];
|
||||
}];
|
||||
|
||||
# nginx
|
||||
services.nginx.virtualHosts."${svc_domain}".locations."/admin" = {
|
||||
proxyPass = "http://127.0.0.1:${builtins.toString config.services.headplane.settings.server.port}";
|
||||
priority = 5;
|
||||
};
|
||||
|
||||
# persistence
|
||||
environment.persistence.root.directories = [
|
||||
"/var/lib/headplane"
|
||||
];
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
{ config, pkgs, ... }:
|
||||
|
||||
let
|
||||
svc_domain = config.mine.shared.settings.headscale.domain;
|
||||
svc_domain_dns_magic_domain = "ts.${config.mine.shared.settings.domain}";
|
||||
|
||||
grpc_port = 50443;
|
||||
in {
|
||||
services.headscale = {
|
||||
enable = true;
|
||||
|
||||
settings = {
|
||||
server_url = "https://${svc_domain}";
|
||||
|
||||
# grpc
|
||||
grpc_listen_addr = "127.0.0.1:${builtins.toString grpc_port}";
|
||||
grpc_allow_insecure = true;
|
||||
|
||||
# dns
|
||||
dns.base_domain = svc_domain_dns_magic_domain;
|
||||
dns.nameservers.global = [ "1.1.1.1" ];
|
||||
|
||||
# acl -> save in database
|
||||
policy.mode = "database";
|
||||
|
||||
# auth
|
||||
oidc = {
|
||||
only_start_if_oidc_is_available = true;
|
||||
issuer = "https://${config.mine.shared.settings.authelia.domain}";
|
||||
client_id = "headscale";
|
||||
client_secret_path = "$CREDENTIALS_DIRECTORY/authelia-secret";
|
||||
};
|
||||
|
||||
# debug
|
||||
log.level = "debug";
|
||||
};
|
||||
};
|
||||
systemd.services.headscale.serviceConfig.LoadCredential = [
|
||||
"authelia-secret:${config.age.secrets.headscale-authelia-secret.path}"
|
||||
];
|
||||
|
||||
# setup for oidc
|
||||
services.authelia.instances.main.settings.identity_providers.oidc.clients = [{
|
||||
client_id = "headscale";
|
||||
client_name = "Headscale";
|
||||
client_secret = "$pbkdf2-sha512$310000$/xiUR1oDvUEn4OKEu31COw$Z.IMgW3Qb2.mgCyEq1UUDC0i7cX2GOiywUjY4MsNv4ixQCP1jFO7njctCW2mVCqvkgfylDpsWRM3z.uXTs89IA";
|
||||
consent_mode = "implicit";
|
||||
redirect_uris = [ "https://${svc_domain}/oidc/callback" ];
|
||||
scopes = [
|
||||
"openid"
|
||||
"profile"
|
||||
"email"
|
||||
];
|
||||
}];
|
||||
|
||||
# nginx
|
||||
services.nginx.virtualHosts."${svc_domain}" = {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
|
||||
# headscale gRPC API
|
||||
locations."/headscale" = {
|
||||
extraConfig = ''
|
||||
grpc_pass 127.0.0.1:${builtins.toString grpc_port};
|
||||
'';
|
||||
priority = 0;
|
||||
};
|
||||
|
||||
# fallback to headscale
|
||||
locations."/" = {
|
||||
proxyPass = "http://127.0.0.1:${builtins.toString config.services.headscale.port}";
|
||||
proxyWebsockets = true;
|
||||
};
|
||||
};
|
||||
|
||||
# persistence
|
||||
environment.persistence.root.directories = [
|
||||
"/var/lib/headscale"
|
||||
];
|
||||
}
|
158
machines/gerd/services/lldap.nix
Normal file
158
machines/gerd/services/lldap.nix
Normal file
|
@ -0,0 +1,158 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
svc_domain = "ldap.${config.mine.shared.settings.domain}";
|
||||
|
||||
resetPasswordStartPatch = pkgs.writeText "lldap-reset-password-start.patch" ''
|
||||
diff --git a/server/src/main.rs b/server/src/main.rs
|
||||
index 71e4928..63be13c 100644
|
||||
--- a/server/src/main.rs
|
||||
+++ b/server/src/main.rs
|
||||
@@ -158,7 +158,7 @@ async fn set_up_server(config: Configuration) -> Result<ServerBuilder> {
|
||||
))?;
|
||||
}
|
||||
if config.force_update_private_key || config.force_ldap_user_pass_reset {
|
||||
- bail!("Restart the server without --force-update-private-key or --force-ldap-user-pass-reset to continue.");
|
||||
+ // bail!("Restart the server without --force-update-private-key or --force-ldap-user-pass-reset to continue.");
|
||||
}
|
||||
let server_builder = infra::ldap_server::build_ldap_server(
|
||||
&config,
|
||||
'';
|
||||
in {
|
||||
services.lldap = {
|
||||
enable = true;
|
||||
|
||||
package = pkgs.lldap.overrideAttrs (old: {
|
||||
patches = old.patches ++ [ resetPasswordStartPatch ];
|
||||
});
|
||||
|
||||
settings = {
|
||||
verbose = true;
|
||||
ldap_user_email = "fricloudlldap.grief462@simplelogin.com";
|
||||
ldap_base_dn = config.mine.shared.settings.ldap.dc;
|
||||
};
|
||||
|
||||
environment = {
|
||||
# always set admin password on startup
|
||||
LLDAP_LDAP_USER_PASS_FILE = config.age.secrets.lldap-admin-user-pass.path;
|
||||
LLDAP_FORCE_LDAP_USER_PASS_RESET = "true";
|
||||
};
|
||||
};
|
||||
|
||||
services.nginx.virtualHosts."${svc_domain}" = {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
locations."/".proxyPass = "http://localhost:${builtins.toString config.services.lldap.settings.http_port}";
|
||||
};
|
||||
|
||||
# persistent files
|
||||
environment.persistence.root.directories = [
|
||||
{ directory = "/var/lib/private/lldap"; mode = "0700"; }
|
||||
];
|
||||
|
||||
# lldap user + setup secrets owner (need to add user for secrets to work)
|
||||
users.users.lldap = { group = "lldap"; isSystemUser = true; };
|
||||
users.groups.lldap = {};
|
||||
age.secrets = {
|
||||
lldap-admin-user-pass.owner = "lldap";
|
||||
};
|
||||
|
||||
# set settings other services can use
|
||||
# CN = Common Name
|
||||
# OU = Organizational Unit
|
||||
# DC = Domain Component
|
||||
#
|
||||
# The users are all located in ou=people, + the base DN, so by default user bob is at cn=bob,ou=people,dc=example,dc=com.
|
||||
# Similarly, the groups are located in ou=groups, so the group family will be at cn=family,ou=groups,dc=example,dc=com.
|
||||
# Testing group membership through memberOf is supported, so you can have a filter like: (memberOf=cn=admins,ou=groups,dc=example,dc=com).
|
||||
mine.shared.settings.ldap = rec {
|
||||
host = "localhost";
|
||||
port = 3890;
|
||||
url = "ldap://${host}:${builtins.toString port}";
|
||||
|
||||
dc = "dc=${config.mine.shared.settings.domain_sld},dc=${config.mine.shared.settings.domain_tld}";
|
||||
bind_dn = "uid=${users.bind},ou=${ou.users},${dc}";
|
||||
search_base = "ou=${ou.users},${dc}";
|
||||
user_filter = ph: let
|
||||
attrs = [ attr.uid attr.email ];
|
||||
in config.mine.shared.lib.ldap.mkFilter (lconfig: llib:
|
||||
llib.mkAnd [
|
||||
(llib.mkGroup lconfig.groups.member)
|
||||
(llib.mkOr (lib.forEach attrs (v: llib.mkSearch v ph)))
|
||||
]
|
||||
);
|
||||
|
||||
oc = {
|
||||
person = "person";
|
||||
mailAccount = "mailAccount";
|
||||
groupOfUniqueNames = "groupOfUniqueNames";
|
||||
};
|
||||
|
||||
users = {
|
||||
admin = "admin";
|
||||
bind = "bind_user";
|
||||
};
|
||||
|
||||
groups = {
|
||||
admin = "lldap_admin";
|
||||
member = "base_member";
|
||||
};
|
||||
|
||||
ou = {
|
||||
groups = "groups";
|
||||
users = "people";
|
||||
};
|
||||
|
||||
attr = {
|
||||
uid = "uid";
|
||||
firstname = "givenName";
|
||||
lastname = "sn";
|
||||
email = "mail";
|
||||
avatar = "jpegPhoto";
|
||||
groupname = "cn";
|
||||
};
|
||||
|
||||
age_secret = config.age.secrets.lldap-bind-user-pass.path;
|
||||
};
|
||||
|
||||
mine.shared.lib.ldap = rec {
|
||||
mkGroup = group_name: "memberof=cn=${group_name},ou=${config.mine.shared.settings.ldap.ou.groups},${config.mine.shared.settings.ldap.dc}";
|
||||
mkOC = object_class_name: "objectclass=${object_class_name}";
|
||||
mkSearch = attribute: ph: "${attribute}=${ph}";
|
||||
|
||||
mkFilterAdvanced = expr: let
|
||||
isExpr = value: if value ? type then true else false;
|
||||
|
||||
__mkExpr = value: if isExpr value then mkFilterAdvanced value else "(${value})";
|
||||
_mkExpr = op: value: "(${op}" + (builtins.concatStringsSep "" (lib.forEach value (v: __mkExpr v))) + ")";
|
||||
mkExpr = expr: assert isExpr expr; if expr.type == "and" then _mkExpr "&" expr.values else _mkExpr "|" expr.values;
|
||||
in mkExpr expr;
|
||||
|
||||
mkAndOr = andExprs: orExprs: mkFilterAdvanced {
|
||||
type = "and";
|
||||
values = andExprs ++ [
|
||||
{ type = "or"; values = orExprs; }
|
||||
];
|
||||
};
|
||||
|
||||
mkFilter = t: mkFilterAdvanced (t config.mine.shared.settings.ldap config.mine.shared.lib.ldap);
|
||||
mkScope = t: t config.mine.shared.settings.ldap config.mine.shared.lib.ldap;
|
||||
|
||||
mkAnd = v: { type = "and"; values = v; };
|
||||
mkOr = v: { type = "or"; values = v; };
|
||||
};
|
||||
|
||||
mine.shared.meta.lldap = {
|
||||
name = "LDAP";
|
||||
description = "We host our own LDAP server, you can use it to change your displayname, name, password, etc.";
|
||||
url = "https://${svc_domain}";
|
||||
|
||||
package = let
|
||||
pkg = config.services.lldap.package;
|
||||
in {
|
||||
name = pkg.pname;
|
||||
version = pkg.version;
|
||||
meta = pkg.meta;
|
||||
};
|
||||
};
|
||||
}
|
|
@ -1,301 +0,0 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
svc_domain = "ldap.${config.mine.shared.settings.domain}";
|
||||
|
||||
resetPasswordStartPatch = pkgs.writeText "lldap-reset-password-start.patch" ''
|
||||
diff --git a/server/src/main.rs b/server/src/main.rs
|
||||
index 6f42473..b3746a1 100644
|
||||
--- a/server/src/main.rs
|
||||
+++ b/server/src/main.rs
|
||||
@@ -171,7 +171,7 @@ async fn set_up_server(config: Configuration) -> Result<ServerBuilder> {
|
||||
))?;
|
||||
}
|
||||
if config.force_update_private_key || config.force_ldap_user_pass_reset.is_yes() {
|
||||
- bail!("Restart the server without --force-update-private-key or --force-ldap-user-pass-reset to continue.");
|
||||
+ // bail!("Restart the server without --force-update-private-key or --force-ldap-user-pass-reset to continue.");
|
||||
}
|
||||
let server_builder = infra::ldap_server::build_ldap_server(
|
||||
&config,
|
||||
'';
|
||||
|
||||
whoamiPatch = pkgs.writeText "lldap-whoami.patch" ''
|
||||
diff --git a/server/src/infra/ldap_handler.rs b/server/src/infra/ldap_handler.rs
|
||||
index 7257c31..feda03c 100644
|
||||
--- a/server/src/infra/ldap_handler.rs
|
||||
+++ b/server/src/infra/ldap_handler.rs
|
||||
@@ -26,7 +26,7 @@ use ldap3_proto::proto::{
|
||||
LdapDerefAliases, LdapExtendedRequest, LdapExtendedResponse, LdapFilter, LdapModify,
|
||||
LdapModifyRequest, LdapModifyType, LdapOp, LdapPartialAttribute, LdapPasswordModifyRequest,
|
||||
LdapResult as LdapResultOp, LdapResultCode, LdapSearchRequest, LdapSearchResultEntry,
|
||||
- LdapSearchScope,
|
||||
+ LdapSearchScope, OID_PASSWORD_MODIFY, OID_WHOAMI,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use tracing::{debug, instrument, warn};
|
||||
@@ -181,7 +181,7 @@ fn root_dse_response(base_dn: &str) -> LdapOp {
|
||||
LdapPartialAttribute {
|
||||
atype: "supportedExtension".to_string(),
|
||||
// Password modification extension.
|
||||
- vals: vec![b"1.3.6.1.4.1.4203.1.11.1".to_vec()],
|
||||
+ vals: vec![OID_PASSWORD_MODIFY.as_bytes().to_vec()],
|
||||
},
|
||||
LdapPartialAttribute {
|
||||
atype: "supportedControl".to_string(),
|
||||
@@ -204,6 +204,11 @@ fn root_dse_response(base_dn: &str) -> LdapOp {
|
||||
atype: "isGlobalCatalogReady".to_string(),
|
||||
vals: vec![b"false".to_vec()],
|
||||
},
|
||||
+ LdapPartialAttribute {
|
||||
+ atype: "supportedExtension".to_string(),
|
||||
+ // whoami extension.
|
||||
+ vals: vec![OID_WHOAMI.as_bytes().to_vec()],
|
||||
+ },
|
||||
],
|
||||
})
|
||||
}
|
||||
@@ -413,16 +418,33 @@ impl<Backend: BackendHandler + LoginHandler + OpaqueHandler> LdapHandler<Backend
|
||||
|
||||
#[instrument(skip_all, level = "debug")]
|
||||
async fn do_extended_request(&mut self, request: &LdapExtendedRequest) -> Vec<LdapOp> {
|
||||
- match LdapPasswordModifyRequest::try_from(request) {
|
||||
- Ok(password_request) => self
|
||||
+ if let Ok(password_request) = LdapPasswordModifyRequest::try_from(request) {
|
||||
+ return self
|
||||
.do_password_modification(&password_request)
|
||||
.await
|
||||
- .unwrap_or_else(|e: LdapError| vec![make_extended_response(e.code, e.message)]),
|
||||
- Err(_) => vec![make_extended_response(
|
||||
- LdapResultCode::UnwillingToPerform,
|
||||
- format!("Unsupported extended operation: {}", &request.name),
|
||||
- )],
|
||||
+ .unwrap_or_else(|e: LdapError| vec![make_extended_response(e.code, e.message)]);
|
||||
}
|
||||
+
|
||||
+ if request.name == OID_WHOAMI {
|
||||
+ let dn = self
|
||||
+ .user_info
|
||||
+ .as_ref()
|
||||
+ .map(|user_info| {
|
||||
+ format!(
|
||||
+ "dn:uid={},ou=people,{}",
|
||||
+ user_info.user.clone().into_string(),
|
||||
+ self.ldap_info.base_dn_str
|
||||
+ )
|
||||
+ })
|
||||
+ .unwrap_or_default();
|
||||
+
|
||||
+ return vec![make_extended_response(LdapResultCode::Success, dn)];
|
||||
+ }
|
||||
+
|
||||
+ return vec![make_extended_response(
|
||||
+ LdapResultCode::UnwillingToPerform,
|
||||
+ format!("Unsupported extended operation: {}", &request.name),
|
||||
+ )];
|
||||
}
|
||||
|
||||
async fn handle_modify_change(
|
||||
'';
|
||||
|
||||
pkgLLDAPCli = pkgs.callPackage ./../../../../shared/pkgs/lldap-cli.nix {};
|
||||
in {
|
||||
imports = [
|
||||
./provision.nix
|
||||
];
|
||||
|
||||
environment.systemPackages = [
|
||||
pkgLLDAPCli
|
||||
];
|
||||
|
||||
services.lldap = {
|
||||
enable = true;
|
||||
|
||||
package = pkgs.lldap.overrideAttrs (old: {
|
||||
patches = old.patches ++ [ resetPasswordStartPatch whoamiPatch ];
|
||||
});
|
||||
|
||||
settings = {
|
||||
verbose = true;
|
||||
ldap_user_email = "fricloudlldap.grief462@simplelogin.com";
|
||||
ldap_base_dn = config.mine.shared.settings.ldap.dc;
|
||||
};
|
||||
|
||||
environment = {
|
||||
# always set admin password on startup
|
||||
LLDAP_LDAP_USER_PASS_FILE = config.age.secrets.lldap-admin-user-pass.path;
|
||||
LLDAP_FORCE_LDAP_USER_PASS_RESET = "true";
|
||||
};
|
||||
};
|
||||
|
||||
services.nginx.virtualHosts."${svc_domain}" = {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
locations."/".proxyPass = "http://localhost:${builtins.toString config.services.lldap.settings.http_port}";
|
||||
};
|
||||
|
||||
# persistent files
|
||||
environment.persistence.root.directories = [
|
||||
{ directory = "/var/lib/private/lldap"; mode = "0700"; }
|
||||
];
|
||||
|
||||
# lldap user + setup secrets owner (need to add user for secrets to work)
|
||||
users.users.lldap = { group = "lldap"; isSystemUser = true; };
|
||||
users.groups.lldap = {};
|
||||
age.secrets = {
|
||||
lldap-admin-user-pass.owner = "lldap";
|
||||
};
|
||||
|
||||
# set settings other services can use
|
||||
# CN = Common Name
|
||||
# OU = Organizational Unit
|
||||
# DC = Domain Component
|
||||
#
|
||||
# The users are all located in ou=people, + the base DN, so by default user bob is at cn=bob,ou=people,dc=example,dc=com.
|
||||
# Similarly, the groups are located in ou=groups, so the group family will be at cn=family,ou=groups,dc=example,dc=com.
|
||||
# Testing group membership through memberOf is supported, so you can have a filter like: (memberOf=cn=admins,ou=groups,dc=example,dc=com).
|
||||
mine.shared.settings.ldap = rec {
|
||||
host = "localhost";
|
||||
port = 3890;
|
||||
url = "ldap://${host}:${builtins.toString port}";
|
||||
|
||||
dc = "dc=${config.mine.shared.settings.domain_sld},dc=${config.mine.shared.settings.domain_tld}";
|
||||
bind_dn = "uid=${users.bind},ou=${ou.users},${dc}";
|
||||
search_base = "ou=${ou.users},${dc}";
|
||||
user_filter = ph: let
|
||||
attrs = [ attr.uid attr.email ];
|
||||
in config.mine.shared.lib.ldap.mkFilter (lconfig: llib:
|
||||
llib.mkAnd [
|
||||
(llib.mkGroup lconfig.groups.member)
|
||||
(llib.mkOr (lib.forEach attrs (v: llib.mkSearch v ph)))
|
||||
]
|
||||
);
|
||||
|
||||
oc = {
|
||||
person = "person";
|
||||
mailAccount = "mailAccount";
|
||||
groupOfUniqueNames = "groupOfUniqueNames";
|
||||
};
|
||||
|
||||
provision = config.services.lldap.provision;
|
||||
|
||||
users = {
|
||||
admin = "admin";
|
||||
# bind = "bind_user";
|
||||
} // (lib.mapAttrs (n: v: v.user_id) config.services.lldap.provision.users);
|
||||
|
||||
groups = {
|
||||
admin = "lldap_admin";
|
||||
member = "base_member";
|
||||
password_manager = "lldap_password_manager";
|
||||
strict_readonly = "lldap_strict_readonly";
|
||||
# system = "system_service";
|
||||
# system_email = "system_email";
|
||||
} // (lib.mapAttrs (n: v: v.display_name) config.services.lldap.provision.groups);
|
||||
|
||||
ou = {
|
||||
groups = "groups";
|
||||
users = "people";
|
||||
};
|
||||
|
||||
attr = let
|
||||
toCamelCase = w: let
|
||||
parts = lib.splitString "_" w;
|
||||
cap = lib.imap0 (i: v: if i == 0 then v else (lib.toUpper (lib.substring 0 1 v)) + (lib.substring 1 (lib.stringLength v) v));
|
||||
in lib.concatStrings (cap parts);
|
||||
in {
|
||||
uid = "uid";
|
||||
creationdate = "creationdate";
|
||||
firstname = "givenName";
|
||||
lastname = "sn";
|
||||
email = "mail";
|
||||
avatar = "jpegPhoto";
|
||||
groupname = "cn";
|
||||
|
||||
# custom
|
||||
# member_email = "member_email";
|
||||
# mail_disk_quota = "mail_disk_quota";
|
||||
} // (lib.mapAttrs (n: v: toCamelCase v.name) config.services.lldap.provision.user_attributes);
|
||||
|
||||
age_secret = config.age.secrets.lldap-bind-user-pass.path;
|
||||
};
|
||||
|
||||
mine.shared.lib.ldap = rec {
|
||||
mkGroup = group_name: "memberof=cn=${group_name},ou=${config.mine.shared.settings.ldap.ou.groups},${config.mine.shared.settings.ldap.dc}";
|
||||
mkOC = object_class_name: "objectclass=${object_class_name}";
|
||||
mkSearch = attribute: ph: "${attribute}=${ph}";
|
||||
|
||||
mkFilterAdvanced = expr: let
|
||||
isExpr = value: if value ? type then true else false;
|
||||
|
||||
__mkExpr = value: if isExpr value then mkFilterAdvanced value else "(${value})";
|
||||
_mkExpr = op: value: "(${op}" + (builtins.concatStringsSep "" (lib.forEach value (v: __mkExpr v))) + ")";
|
||||
mkExpr = expr: assert isExpr expr; if expr.type == "and" then _mkExpr "&" expr.values else _mkExpr "|" expr.values;
|
||||
in mkExpr expr;
|
||||
|
||||
mkAndOr = andExprs: orExprs: mkFilterAdvanced {
|
||||
type = "and";
|
||||
values = andExprs ++ [
|
||||
{ type = "or"; values = orExprs; }
|
||||
];
|
||||
};
|
||||
|
||||
mkFilter = t: mkFilterAdvanced (t config.mine.shared.settings.ldap config.mine.shared.lib.ldap);
|
||||
mkScope = t: t config.mine.shared.settings.ldap config.mine.shared.lib.ldap;
|
||||
|
||||
mkAnd = v: { type = "and"; values = v; };
|
||||
mkOr = v: { type = "or"; values = v; };
|
||||
|
||||
# mkProvision helpers for creating users
|
||||
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 ];
|
||||
membermaildiskquota = 100*1024*1024; # mb
|
||||
nextcloudquota = 100*1024*1024; # mb
|
||||
});
|
||||
};
|
||||
|
||||
mine.shared.meta.lldap = {
|
||||
name = "LDAP";
|
||||
description = "We host our own LDAP server, you can use it to change your displayname, name, password, etc.";
|
||||
url = "https://${svc_domain}";
|
||||
|
||||
package = let
|
||||
pkg = config.services.lldap.package;
|
||||
in {
|
||||
name = pkg.pname;
|
||||
version = pkg.version;
|
||||
meta = pkg.meta;
|
||||
};
|
||||
};
|
||||
}
|
|
@ -1,161 +0,0 @@
|
|||
from typing import Any
|
||||
|
||||
import gql
|
||||
from gql.dsl import DSLQuery, DSLSchema, dsl_gql, DSLMutation
|
||||
from gql.transport.aiohttp import AIOHTTPTransport
|
||||
|
||||
from .utils import to_camelcase, to_snakecase
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AttributeTypes:
|
||||
STRING = "STRING"
|
||||
INTEGER = "INTEGER"
|
||||
JPEG_PHOTO = "JPEG_PHOTO"
|
||||
DATE_TIME = "DATE_TIME"
|
||||
|
||||
|
||||
class AttributeSchema:
|
||||
def __init__(self, raw_vals: dict[str, Any]):
|
||||
self.name: str = raw_vals["name"]
|
||||
self.attributeType: str = raw_vals["attributeType"]
|
||||
self.isList: bool = bool(raw_vals["isList"])
|
||||
self.isVisible: bool = bool(raw_vals["isVisible"])
|
||||
self.isEditable: bool = bool(raw_vals["isEditable"])
|
||||
self.isReadonly: bool = bool(raw_vals["isReadonly"])
|
||||
self.isHardcoded: bool = bool(raw_vals["isHardcoded"])
|
||||
|
||||
def __repr__(self):
|
||||
return f"<AttributeSchema name={self.name} attributeType={self.attributeType} isList={self.isList} isVisible={self.isVisible} isEditable={self.isEditable} isReadonly={self.isReadonly} isHardcoded={self.isHardcoded} />"
|
||||
|
||||
|
||||
class AttributeValue:
|
||||
def __init__(self, raw_attribute: dict[str, Any]):
|
||||
self.name: str = raw_attribute["name"]
|
||||
|
||||
tmpValue = raw_attribute.get("value", [])
|
||||
if isinstance(tmpValue, str):
|
||||
tmpValue = [tmpValue]
|
||||
|
||||
self.value: list[str] = tmpValue
|
||||
|
||||
def __repr__(self):
|
||||
return f"<AttributeValue name={self.name} value={self.value} />"
|
||||
|
||||
|
||||
class LLDAPAttributes:
|
||||
def __init__(
|
||||
self,
|
||||
client: gql.Client,
|
||||
using_user_attributes: bool = False,
|
||||
using_group_attributes: bool = False,
|
||||
):
|
||||
self._client = client
|
||||
|
||||
self._using_user_attributes = using_user_attributes
|
||||
self._using_group_attributes = using_group_attributes
|
||||
|
||||
if self._using_user_attributes and self._using_group_attributes:
|
||||
raise Exception("can not both use user attributes and group attributes")
|
||||
|
||||
if not self._using_user_attributes and not self._using_group_attributes:
|
||||
raise Exception("neither user attributes and group attributes specified")
|
||||
|
||||
def list_all(self) -> dict[str, AttributeSchema]:
|
||||
ds = DSLSchema(self._client.schema)
|
||||
|
||||
if self._using_user_attributes:
|
||||
querySchema = ds.Schema.userSchema
|
||||
querySchemaStr = "userSchema"
|
||||
else:
|
||||
querySchema = ds.Schema.groupSchema
|
||||
querySchemaStr = "groupSchema"
|
||||
|
||||
query = dsl_gql(
|
||||
DSLQuery(
|
||||
ds.Query.schema().select(
|
||||
querySchema.select(
|
||||
ds.AttributeList.attributes.select(
|
||||
ds.AttributeSchema.name,
|
||||
ds.AttributeSchema.attributeType,
|
||||
ds.AttributeSchema.isList,
|
||||
ds.AttributeSchema.isVisible,
|
||||
ds.AttributeSchema.isEditable,
|
||||
ds.AttributeSchema.isReadonly,
|
||||
ds.AttributeSchema.isHardcoded,
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
result = self._client.execute(query)
|
||||
|
||||
attrs: dict[str, AttributeSchema] = {}
|
||||
for raw_attribute in result["schema"][querySchemaStr]["attributes"]:
|
||||
attr = AttributeSchema(raw_attribute)
|
||||
attrs[attr.name] = attr
|
||||
|
||||
return attrs
|
||||
|
||||
def create(
|
||||
self,
|
||||
name: str,
|
||||
attributeType: str,
|
||||
isList: bool,
|
||||
isVisible: bool,
|
||||
isEditable: bool,
|
||||
):
|
||||
logger.debug(
|
||||
f"adding attribute, name:'{name}' attributeType:'{attributeType}' isList:'{isList}' isVisible:'{isVisible}' isEditable:'{isEditable}'"
|
||||
)
|
||||
|
||||
ds = DSLSchema(self._client.schema)
|
||||
|
||||
if self._using_user_attributes:
|
||||
mutationSchema = ds.Mutation.addUserAttribute
|
||||
else:
|
||||
mutationSchema = ds.Mutation.addGroupAttribute
|
||||
|
||||
query = dsl_gql(
|
||||
DSLMutation(
|
||||
mutationSchema.args(
|
||||
name=name,
|
||||
attributeType=attributeType,
|
||||
isList=isList,
|
||||
isVisible=isVisible,
|
||||
isEditable=isEditable,
|
||||
).select(ds.Success.ok)
|
||||
),
|
||||
)
|
||||
self._client.execute(query)
|
||||
|
||||
def get(self, name: str) -> AttributeSchema | None:
|
||||
attrs = self.list_all()
|
||||
return attrs.get(name)
|
||||
|
||||
def update(self, name: str):
|
||||
raise Exception("unable to update attribute")
|
||||
|
||||
def delete(self, name: str):
|
||||
logger.debug(f"deleting attribute '{name}'")
|
||||
|
||||
ds = DSLSchema(self._client.schema)
|
||||
|
||||
if self._using_user_attributes:
|
||||
mutationSchema = ds.Mutation.deleteUserAttribute
|
||||
else:
|
||||
mutationSchema = ds.Mutation.deleteGroupAttribute
|
||||
|
||||
query = dsl_gql(
|
||||
DSLMutation(
|
||||
mutationSchema.args(
|
||||
name=name,
|
||||
).select(ds.Success.ok)
|
||||
),
|
||||
)
|
||||
self._client.execute(query)
|
|
@ -1,79 +0,0 @@
|
|||
{
|
||||
"group_attributes": [
|
||||
{
|
||||
"name": "hosted_email2",
|
||||
"attributeType": "STRING",
|
||||
"isEditable": false,
|
||||
"isList": false,
|
||||
"isVisible": false
|
||||
},
|
||||
{
|
||||
"name": "hosted_email10",
|
||||
"attributeType": "STRING",
|
||||
"isEditable": false,
|
||||
"isList": false,
|
||||
"isVisible": false
|
||||
}
|
||||
],
|
||||
"user_attributes": [
|
||||
{
|
||||
"name": "member_email",
|
||||
"attributeType": "STRING",
|
||||
"isEditable": false,
|
||||
"isList": false,
|
||||
"isVisible": true
|
||||
}
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"display_name": "base_member",
|
||||
"hosted_email2": "tehnte_member",
|
||||
"hosted_email10": "tehnte_member"
|
||||
},
|
||||
{ "display_name": "system_service" },
|
||||
{ "display_name": "lldap_admin" },
|
||||
{ "display_name": "lldap_password_manager" },
|
||||
{ "display_name": "lldap_strict_readonly" },
|
||||
{ "display_name": "disabled" }
|
||||
],
|
||||
"users": [
|
||||
{
|
||||
"user_id": "admin",
|
||||
"groups": ["lldap_admin"],
|
||||
"mail": "admin@fricloud.dk"
|
||||
},
|
||||
{
|
||||
"user_id": "bind_user",
|
||||
"groups": ["lldap_password_manager", "lldap_strict_readonly"],
|
||||
"mail": "lldap_bind_user@fricloud.dk"
|
||||
},
|
||||
{
|
||||
"user_id": "authelia",
|
||||
"groups": ["system_service", "base_member"],
|
||||
"mail": "authelia@fricloud.dk"
|
||||
},
|
||||
{
|
||||
"user_id": "wger",
|
||||
"groups": ["base_member"],
|
||||
"mail": "wger@fricloud.dk"
|
||||
},
|
||||
{
|
||||
"user_id": "testusername",
|
||||
"groups": ["base_member"],
|
||||
"password": "env:PASSWORD",
|
||||
"mail": "testusername@fricloud.dk"
|
||||
},
|
||||
{
|
||||
"user_id": "eyjhb",
|
||||
"groups": ["lldap_admin", "base_member"],
|
||||
"mail": "eyjhb@fricloud.dk",
|
||||
"member_email": "eyjhb@fricloud.dk"
|
||||
},
|
||||
{
|
||||
"user_id": "rasmus",
|
||||
"groups": ["lldap_admin", "base_member"],
|
||||
"mail": "rasmus@fricloud.dk",
|
||||
"member_email": "rasmus@fricloud.dk"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,137 +0,0 @@
|
|||
from typing import Any
|
||||
import re
|
||||
|
||||
import gql
|
||||
from gql.dsl import DSLQuery, DSLSchema, dsl_gql, DSLMutation
|
||||
from gql.transport.aiohttp import AIOHTTPTransport
|
||||
|
||||
from .utils import to_camelcase, to_snakecase, get_value
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Group:
|
||||
def __init__(
|
||||
self,
|
||||
raw_attrs: list[dict[str, str]],
|
||||
):
|
||||
self._attributes: dict[str, Any] = {
|
||||
item["name"]: item["value"] for item in raw_attrs
|
||||
}
|
||||
|
||||
self.groupId: int = int(self.__getattr__("groupId")[0])
|
||||
self.name: str = self.__getattr__("displayName")[0]
|
||||
|
||||
def _attributes_camelcase(self) -> dict[str, str]:
|
||||
return {to_camelcase(k): v for k, v in self._attributes.items()}
|
||||
|
||||
def __getattr__(self, key: str):
|
||||
return self._attributes_camelcase().get(key, "")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Group groupId={self.groupId} name={self.name} />"
|
||||
|
||||
|
||||
class LLDAPGroups:
|
||||
def __init__(self, client: gql.Client):
|
||||
self._client = client
|
||||
|
||||
def list_all(self) -> dict[str, Group]:
|
||||
ds = DSLSchema(self._client.schema)
|
||||
query = dsl_gql(
|
||||
DSLQuery(
|
||||
ds.Query.groups().select(
|
||||
ds.Group.attributes.select(
|
||||
ds.AttributeValue.name,
|
||||
ds.AttributeValue.value,
|
||||
ds.AttributeValue.schema.select(
|
||||
ds.AttributeSchema.isHardcoded,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
result = self._client.execute(query)
|
||||
|
||||
groups: dict[str, Group] = {}
|
||||
for group in result["groups"]:
|
||||
g = Group(
|
||||
raw_attrs=group.get("attributes", []),
|
||||
)
|
||||
groups[g.name] = g
|
||||
|
||||
return groups
|
||||
|
||||
def create(self, groupName: str):
|
||||
logger.debug(f"creating group with name '{groupName}'")
|
||||
|
||||
ds = DSLSchema(self._client.schema)
|
||||
query = dsl_gql(
|
||||
DSLMutation(
|
||||
ds.Mutation.createGroup.args(
|
||||
name=groupName,
|
||||
).select(ds.Group.displayName)
|
||||
),
|
||||
)
|
||||
self._client.execute(query)
|
||||
|
||||
def get_by_name(self, groupName: str) -> Group | None:
|
||||
groups = self.list_all()
|
||||
return groups.get(groupName)
|
||||
|
||||
def get_by_id(self, groupId: int) -> Group | None:
|
||||
groups = self.list_all()
|
||||
for group in groups.values():
|
||||
if group.groupId == groupId:
|
||||
return group
|
||||
|
||||
return None
|
||||
|
||||
def name_to_id(self, groupName: str) -> int:
|
||||
group = self.get_by_name(groupName)
|
||||
if not group:
|
||||
raise Exception(f"no group with the name {groupName}")
|
||||
|
||||
return group.groupId
|
||||
|
||||
def update(self, groupId: int, attrs: dict[str, str | list[str]]):
|
||||
insertAttributes: list[dict[str, str | list[str]]] = []
|
||||
for k, v in attrs.items():
|
||||
v = get_value(v)
|
||||
if isinstance(v, str):
|
||||
v = [v]
|
||||
|
||||
insertAttributes.append({"name": k, "value": v})
|
||||
|
||||
ds = DSLSchema(self._client.schema)
|
||||
query = dsl_gql(
|
||||
DSLMutation(
|
||||
ds.Mutation.updateGroup.args(
|
||||
group={
|
||||
"id": groupId,
|
||||
"insertAttributes": insertAttributes,
|
||||
},
|
||||
).select(ds.Success.ok),
|
||||
),
|
||||
)
|
||||
self._client.execute(query)
|
||||
|
||||
def delete(self, groupId: int):
|
||||
logger.debug(f"deleting group with id '{groupId}'")
|
||||
|
||||
ds = DSLSchema(self._client.schema)
|
||||
query = dsl_gql(
|
||||
DSLMutation(
|
||||
ds.Mutation.deleteGroup.args(
|
||||
groupId=groupId,
|
||||
).select(ds.Success.ok),
|
||||
),
|
||||
)
|
||||
self._client.execute(query)
|
||||
|
||||
def test(self):
|
||||
self.list_all()
|
||||
# self.update("testusername", {"displayName": "Test User Name"})
|
|
@ -1,345 +0,0 @@
|
|||
#!/usr/bin/env nix-shell
|
||||
#!nix-shell --pure --keep LLDAP_TOKEN -i python3 -p "python3.withPackages (ps: with ps; [ requests gql aiohttp])"
|
||||
|
||||
from typing import Any
|
||||
import subprocess
|
||||
import requests
|
||||
import secrets
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
|
||||
import gql
|
||||
from gql.dsl import DSLQuery, DSLSchema, dsl_gql, DSLMutation
|
||||
from gql.transport.aiohttp import AIOHTTPTransport
|
||||
from pprint import pprint
|
||||
|
||||
|
||||
from .users import LLDAPUsers
|
||||
from .groups import LLDAPGroups
|
||||
from .attributes import LLDAPAttributes
|
||||
|
||||
import logging
|
||||
|
||||
logging.basicConfig()
|
||||
logging.getLogger("bootstrap").setLevel(logging.DEBUG)
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
class LLDAP:
|
||||
def __init__(
|
||||
self,
|
||||
server_url: str,
|
||||
username: str,
|
||||
password: str,
|
||||
):
|
||||
if password.startswith("file:"):
|
||||
password = open(password[5:], "r").read().strip()
|
||||
|
||||
self._server_url: str = server_url
|
||||
|
||||
self._server_refresh_token: str | None = None
|
||||
self._simple_login(username, password)
|
||||
|
||||
self._client: gql.Client = self._init_gql_client()
|
||||
|
||||
self._users = LLDAPUsers(self._client)
|
||||
self._groups = LLDAPGroups(self._client)
|
||||
self._attrsUser = LLDAPAttributes(self._client, using_user_attributes=True)
|
||||
self._attrsGroup = LLDAPAttributes(self._client, using_group_attributes=True)
|
||||
|
||||
def _simple_login(self, username: str, password: str):
|
||||
r = requests.post(
|
||||
f"{self._server_url}/auth/simple/login",
|
||||
headers={"content-type": "application/json"},
|
||||
data=json.dumps(
|
||||
{
|
||||
"username": username,
|
||||
"password": password,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
if r.status_code != 200:
|
||||
raise Exception(f"failed to signin got response: {r.text}")
|
||||
|
||||
rjson = r.json()
|
||||
self._server_auth_token = rjson["token"]
|
||||
self._server_refresh_token = rjson["refreshToken"]
|
||||
|
||||
def _logout(self):
|
||||
if not self._server_refresh_token:
|
||||
return
|
||||
|
||||
r = requests.get(
|
||||
f"{self._server_url}/auth/logout",
|
||||
headers={"refresh-token": self._server_refresh_token},
|
||||
)
|
||||
print(r.text, r.status_code)
|
||||
|
||||
def _init_gql_client(self) -> gql.Client:
|
||||
# Select your transport with a defined url endpoint
|
||||
transport = AIOHTTPTransport(
|
||||
url=f"{self._server_url}/api/graphql",
|
||||
headers={"Authorization": f"Bearer {self._server_auth_token}"},
|
||||
)
|
||||
|
||||
# Create a GraphQL client using the defined transport
|
||||
client = gql.Client(transport=transport, fetch_schema_from_transport=True)
|
||||
|
||||
# force fetch schema
|
||||
query = gql.gql(
|
||||
"""
|
||||
query {
|
||||
users {
|
||||
displayName
|
||||
}
|
||||
}
|
||||
"""
|
||||
)
|
||||
result = client.execute(query)
|
||||
|
||||
return client
|
||||
|
||||
def _run_ensure_attrs_user(self, attrsClass, neededAttrsUser: list[dict[str, Any]]):
|
||||
dictNeededAttrUser = {v["name"]: v for v in neededAttrsUser}
|
||||
remoteAttrs = attrsClass.list_all()
|
||||
|
||||
# add needed attributes
|
||||
for neededAttr in neededAttrsUser:
|
||||
neededAttrName = neededAttr["name"]
|
||||
|
||||
if neededAttrName in remoteAttrs:
|
||||
cattr = remoteAttrs[neededAttrName]
|
||||
if (
|
||||
neededAttr["attributeType"] != cattr.attributeType
|
||||
or neededAttr["isEditable"] != cattr.isEditable
|
||||
or neededAttr["isList"] != cattr.isList
|
||||
or neededAttr["isVisible"] != cattr.isVisible
|
||||
):
|
||||
logger.debug(
|
||||
f"attribute '{neededAttrName}' out of sync, deleting and adding again"
|
||||
)
|
||||
attrsClass.delete(neededAttrName)
|
||||
else:
|
||||
continue
|
||||
|
||||
attrsClass.create(
|
||||
neededAttrName,
|
||||
attributeType=neededAttr["attributeType"],
|
||||
isEditable=neededAttr["isEditable"],
|
||||
isList=neededAttr["isList"],
|
||||
isVisible=neededAttr["isVisible"],
|
||||
)
|
||||
|
||||
# remove unneeded attributes
|
||||
for remoteAttrName, remoteAttr in remoteAttrs.items():
|
||||
# skip hardcoded ones
|
||||
if remoteAttr.isHardcoded:
|
||||
continue
|
||||
|
||||
if remoteAttrName not in dictNeededAttrUser:
|
||||
attrsClass.delete(remoteAttrName)
|
||||
|
||||
def _run_ensure_groups(self, neededGroups: list[dict[str, Any]]):
|
||||
tmpNeededGroups = {v["display_name"]: v for v in neededGroups}
|
||||
remoteGroups = self._groups.list_all()
|
||||
|
||||
for neededGroup in neededGroups:
|
||||
neededGroupDisplay_Name = neededGroup["display_name"]
|
||||
|
||||
if neededGroupDisplay_Name not in remoteGroups:
|
||||
self._groups.create(neededGroupDisplay_Name)
|
||||
|
||||
# refresh groups
|
||||
remoteGroups = self._groups.list_all()
|
||||
|
||||
remoteGroup = remoteGroups[neededGroupDisplay_Name]
|
||||
|
||||
# we cannot update the display name, and we never would anyways
|
||||
del neededGroup["display_name"]
|
||||
|
||||
self._groups.update(remoteGroup.groupId, neededGroup)
|
||||
|
||||
# delete unused groups
|
||||
for remoteGroupName, remoteGroup in remoteGroups.items():
|
||||
# skip all lldap_ groups
|
||||
if remoteGroupName.startswith("lldap_"):
|
||||
continue
|
||||
|
||||
if remoteGroupName not in tmpNeededGroups:
|
||||
self._groups.delete(remoteGroup.groupId)
|
||||
|
||||
def _run_ensure_users(
|
||||
self,
|
||||
neededUsers: list[dict[str, Any]],
|
||||
softDelete: bool = True,
|
||||
):
|
||||
tmpNeededUsers = {v["user_id"]: v for v in neededUsers}
|
||||
remoteUsers = self._users.list_all()
|
||||
|
||||
for neededUser in neededUsers:
|
||||
# get required info from dict, and DELETE from dict
|
||||
# while we're at it. This means that we can safely use
|
||||
# `neededUser` for updating later
|
||||
neededUserId = neededUser.pop("user_id")
|
||||
neededUserGroups = neededUser.pop("groups", [])
|
||||
neededUserPassword: str | None = neededUser.pop("password", None)
|
||||
|
||||
# create user if needed
|
||||
if neededUserId not in remoteUsers:
|
||||
self._users.create(
|
||||
neededUserId,
|
||||
neededUser.get("mail", "no-email-specified"),
|
||||
)
|
||||
|
||||
# refresh users
|
||||
remoteUsers = self._users.list_all()
|
||||
|
||||
# update user
|
||||
self._users.update(neededUserId, neededUser)
|
||||
|
||||
# set correct groups
|
||||
remoteUser = remoteUsers[neededUserId]
|
||||
|
||||
# print warning about groups attribute
|
||||
if neededUserGroups:
|
||||
logger.info(
|
||||
f"using attribute 'groups' for userId '{neededUserId}', for setting groups, NOT SETTING AS ATTRIBUTE!!"
|
||||
)
|
||||
|
||||
# add to correct groups
|
||||
for groupName in neededUserGroups:
|
||||
if groupName not in remoteUser.groups:
|
||||
self._users.add_group(
|
||||
neededUserId,
|
||||
self._groups.name_to_id(groupName),
|
||||
)
|
||||
|
||||
# remove from unused groups
|
||||
for groupName, group in remoteUser.groups.items():
|
||||
if groupName not in neededUserGroups:
|
||||
self._users.remove_group(
|
||||
neededUserId,
|
||||
self._groups.name_to_id(groupName),
|
||||
)
|
||||
|
||||
if neededUserPassword:
|
||||
logger.info(
|
||||
f"using attribute 'password' for userId '{neededUserId}', for setting password, NOT SETTING AS ATTRIBUTE!!"
|
||||
)
|
||||
|
||||
if neededUserPassword.startswith("file:"):
|
||||
passwordFile = neededUserPassword[len("file:") :]
|
||||
logger.debug(
|
||||
f"reading password from file from file '{passwordFile}' for user '{neededUserId}'"
|
||||
)
|
||||
|
||||
self._user_set_password(
|
||||
neededUserId,
|
||||
open(passwordFile, "r").read().strip(),
|
||||
)
|
||||
elif neededUserPassword.startswith("env:"):
|
||||
cleanedPasswordEnv = neededUserPassword.strip()[len("env:") :]
|
||||
logger.debug(
|
||||
f"reading password from envvar '{cleanedPasswordEnv}' for user '{neededUserId}'"
|
||||
)
|
||||
|
||||
password = os.getenv(cleanedPasswordEnv)
|
||||
if not password:
|
||||
raise Exception(
|
||||
f"could not find env '{cleanedPasswordEnv}' for getting password"
|
||||
)
|
||||
self._user_set_password(
|
||||
neededUserId,
|
||||
password.strip(),
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
f"using the raw value of password, as the password for user '{neededUserId}'"
|
||||
)
|
||||
self._user_set_password(
|
||||
neededUserId,
|
||||
neededUserPassword.strip(),
|
||||
)
|
||||
|
||||
# delete unused users
|
||||
for remoteUserName, remoteUser in remoteUsers.items():
|
||||
if remoteUserName not in tmpNeededUsers:
|
||||
if softDelete:
|
||||
self._user_disable(remoteUserName)
|
||||
else:
|
||||
self._users.delete(remoteUser.userId)
|
||||
|
||||
def _user_disable(self, userId: str, disabled_group_name: str = "disabled"):
|
||||
user = self._users.get(userId)
|
||||
if not user:
|
||||
return
|
||||
|
||||
# remove all groups
|
||||
for groupName, groupId in user.groups.items():
|
||||
if groupName == disabled_group_name:
|
||||
continue
|
||||
|
||||
self._users.remove_group(userId, groupId)
|
||||
|
||||
# if disabled group is in the users groups, then return
|
||||
if disabled_group_name in user.groups:
|
||||
return
|
||||
|
||||
# ensure group exists
|
||||
groups = self._groups.list_all()
|
||||
if disabled_group_name not in groups:
|
||||
self._groups.create(disabled_group_name)
|
||||
|
||||
# add disabled group
|
||||
self._users.add_group(userId, self._groups.name_to_id(disabled_group_name))
|
||||
|
||||
# set password to a long string
|
||||
self._user_set_password(userId, secrets.token_urlsafe(128))
|
||||
|
||||
def _user_set_password(self, userId: str, password: str):
|
||||
subprocess.check_output(
|
||||
[
|
||||
"lldap_set_password",
|
||||
f"--base-url={self._server_url}",
|
||||
f"--token={self._server_auth_token}",
|
||||
f"--username={userId}",
|
||||
f"--password={password}",
|
||||
]
|
||||
)
|
||||
|
||||
def run(self, provision_file_path: str):
|
||||
data = json.load(open(provision_file_path, "r"))
|
||||
|
||||
self._run_ensure_attrs_user(self._attrsUser, data["user_attributes"])
|
||||
self._run_ensure_attrs_user(self._attrsGroup, data["group_attributes"])
|
||||
self._run_ensure_groups(data["groups"])
|
||||
self._run_ensure_users(data["users"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
url = os.getenv("LLDAP_URL")
|
||||
if not url:
|
||||
raise Exception("No LLDAP_URL provided. please set")
|
||||
|
||||
auth_username = os.getenv("LLDAP_USERNAME")
|
||||
if not auth_username:
|
||||
raise Exception("No LLDAP_USERNAME provided. please set")
|
||||
|
||||
auth_password = os.getenv("LLDAP_PASSWORD")
|
||||
if not auth_password:
|
||||
raise Exception("No LLDAP_PASSWORD provided. please set")
|
||||
|
||||
if len(sys.argv) != 2:
|
||||
raise Exception(
|
||||
"Please provide a JSON file containing the provisioning details"
|
||||
)
|
||||
|
||||
x = LLDAP(url, auth_username, auth_password)
|
||||
|
||||
try:
|
||||
x.run(sys.argv[1])
|
||||
finally:
|
||||
x._logout()
|
|
@ -1,155 +0,0 @@
|
|||
from typing import Any
|
||||
import re
|
||||
|
||||
import gql
|
||||
from gql.dsl import DSLQuery, DSLSchema, dsl_gql, DSLMutation
|
||||
from gql.transport.aiohttp import AIOHTTPTransport
|
||||
|
||||
from .utils import to_camelcase, to_snakecase, get_value
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class User:
|
||||
def __init__(
|
||||
self,
|
||||
raw_groups: list[dict[str, Any]],
|
||||
raw_attrs: list[dict[str, str]],
|
||||
):
|
||||
self._attributes: dict[str, Any] = {
|
||||
item["name"]: item["value"] for item in raw_attrs
|
||||
}
|
||||
|
||||
self.groups: dict[str, int] = {
|
||||
info["displayName"]: info["id"] for info in raw_groups
|
||||
}
|
||||
self.userId: str = self.__getattr__("userId")[0]
|
||||
self.email: str = self.__getattr__("mail")[0]
|
||||
|
||||
def _attributes_camelcase(self) -> dict[str, str]:
|
||||
return {to_camelcase(k): v for k, v in self._attributes.items()}
|
||||
|
||||
def __getattr__(self, key: str):
|
||||
return self._attributes_camelcase().get(key, "")
|
||||
|
||||
|
||||
class LLDAPUsers:
|
||||
def __init__(self, client: gql.Client):
|
||||
self._client = client
|
||||
|
||||
def list_all(self) -> dict[str, User]:
|
||||
ds = DSLSchema(self._client.schema)
|
||||
query = dsl_gql(
|
||||
DSLQuery(
|
||||
ds.Query.users().select(
|
||||
ds.User.groups.select(
|
||||
ds.Group.id,
|
||||
ds.Group.displayName,
|
||||
),
|
||||
ds.User.attributes.select(
|
||||
ds.AttributeValue.name,
|
||||
ds.AttributeValue.value,
|
||||
ds.AttributeValue.schema.select(
|
||||
ds.AttributeSchema.isHardcoded,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
result = self._client.execute(query)
|
||||
|
||||
users: dict[str, User] = {}
|
||||
for user in result["users"]:
|
||||
u = User(
|
||||
raw_groups=user.get("groups", []),
|
||||
raw_attrs=user.get("attributes", []),
|
||||
)
|
||||
users[u.userId] = u
|
||||
|
||||
return users
|
||||
|
||||
def create(self, userId: str, email: str):
|
||||
logger.debug(f"creating user with name '{userId}' and email '{email}'")
|
||||
|
||||
ds = DSLSchema(self._client.schema)
|
||||
query = dsl_gql(
|
||||
DSLMutation(
|
||||
ds.Mutation.createUser.args(
|
||||
user={"id": userId, "email": email},
|
||||
).select(ds.User.displayName)
|
||||
),
|
||||
)
|
||||
self._client.execute(query)
|
||||
|
||||
def get(self, userId: str):
|
||||
users = self.list_all()
|
||||
return users.get(userId)
|
||||
|
||||
def update(self, userId: str, attrs: dict[str, str | list[str]]):
|
||||
insertAttributes: list[dict[str, str | list[str]]] = []
|
||||
for k, v in attrs.items():
|
||||
v = get_value(v)
|
||||
if isinstance(v, str):
|
||||
v = [v]
|
||||
|
||||
insertAttributes.append({"name": k, "value": v})
|
||||
|
||||
ds = DSLSchema(self._client.schema)
|
||||
query = dsl_gql(
|
||||
DSLMutation(
|
||||
ds.Mutation.updateUser.args(
|
||||
user={
|
||||
"id": userId,
|
||||
"insertAttributes": insertAttributes,
|
||||
},
|
||||
).select(ds.Success.ok),
|
||||
),
|
||||
)
|
||||
self._client.execute(query)
|
||||
|
||||
def delete(self, userId: str):
|
||||
logger.debug(f"deleting user with name '{userId}'")
|
||||
|
||||
ds = DSLSchema(self._client.schema)
|
||||
query = dsl_gql(
|
||||
DSLMutation(
|
||||
ds.Mutation.deleteUser.args(
|
||||
userId=userId,
|
||||
).select(ds.Success.ok),
|
||||
),
|
||||
)
|
||||
self._client.execute(query)
|
||||
|
||||
# groups
|
||||
def add_group(self, userId: str, groupId: int):
|
||||
logger.debug(f"adding user '{userId}' to group '{groupId}'")
|
||||
|
||||
ds = DSLSchema(self._client.schema)
|
||||
query = dsl_gql(
|
||||
DSLMutation(
|
||||
ds.Mutation.addUserToGroup.args(
|
||||
userId=userId,
|
||||
groupId=groupId,
|
||||
).select(ds.Success.ok)
|
||||
),
|
||||
)
|
||||
|
||||
self._client.execute(query)
|
||||
|
||||
def remove_group(self, userId: str, groupId: int):
|
||||
logger.debug(f"removing user '{userId}' from group '{groupId}'")
|
||||
|
||||
ds = DSLSchema(self._client.schema)
|
||||
query = dsl_gql(
|
||||
DSLMutation(
|
||||
ds.Mutation.removeUserFromGroup.args(
|
||||
userId=userId,
|
||||
groupId=groupId,
|
||||
).select(ds.Success.ok)
|
||||
),
|
||||
)
|
||||
|
||||
self._client.execute(query)
|
|
@ -1,32 +0,0 @@
|
|||
import logging
|
||||
import re
|
||||
import os
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def to_camelcase(s):
|
||||
s = re.sub(r"(_|-)+", " ", s).title().replace(" ", "").replace("*", "")
|
||||
return "".join([s[0].lower(), s[1:]])
|
||||
|
||||
|
||||
def to_snakecase(s):
|
||||
return re.sub(r"(?<!^)(?=[A-Z])", "_", s).lower()
|
||||
|
||||
|
||||
def get_value(s):
|
||||
if not isinstance(s, str):
|
||||
return s
|
||||
|
||||
if s.startswith("file:"):
|
||||
filepath = s[len("file:") :]
|
||||
logger.debug(f"reading from file '{filepath}'")
|
||||
return open(filepath, "r").read().strip()
|
||||
|
||||
if s.startswith("env:"):
|
||||
envkey = s.strip()[len("env:") :]
|
||||
logger.debug(f"reading from envvar '{envkey}'")
|
||||
return os.getenv(envkey)
|
||||
|
||||
logger.debug(f"using original value")
|
||||
return s
|
|
@ -1,167 +0,0 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
inherit (lib) types;
|
||||
|
||||
cfg = config.services.lldap;
|
||||
format = pkgs.formats.json { };
|
||||
|
||||
# helpers
|
||||
_configFile = {
|
||||
user_attributes = lib.mapAttrsToList (n: v: v) cfg.provision.user_attributes;
|
||||
group_attributes = lib.mapAttrsToList (n: v: v) cfg.provision.group_attributes;
|
||||
users = lib.mapAttrsToList (n: v: v) cfg.provision.users;
|
||||
groups = lib.mapAttrsToList (n: v: v) cfg.provision.groups;
|
||||
# users = lib.mapAttrsToList (n: v: v // {
|
||||
# user_id = if v ? user_id then v.user_id else n;
|
||||
# }) cfg.users;
|
||||
# groups = lib.mapAttrsToList (n: v: v // {
|
||||
# display_name = if v ? display_name then v.display_name else n;
|
||||
# }) cfg.groups;
|
||||
};
|
||||
configFile = format.generate "lldap-declarative.json" _configFile;
|
||||
|
||||
# opts
|
||||
optsAttributes = { name, config, ... }: {
|
||||
options = {
|
||||
name = lib.mkOption {
|
||||
type = types.str;
|
||||
default = name;
|
||||
description = "The name of the attribute";
|
||||
};
|
||||
|
||||
attributeType = lib.mkOption {
|
||||
type = types.enum [ "STRING" "INTEGER" "JPEG_PHOTO" "DATE_TIME" ];
|
||||
description = "Type of the attribute";
|
||||
};
|
||||
|
||||
isList = lib.mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Is this attribute a list (multiple values for this attribute)";
|
||||
};
|
||||
|
||||
isEditable = lib.mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Should the user be able to edit this value?";
|
||||
};
|
||||
|
||||
isVisible = lib.mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Should the user be able to see this value?";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
optsUser = { name, config, ... }: {
|
||||
freeformType = format.type;
|
||||
options = {
|
||||
user_id = lib.mkOption {
|
||||
type = types.str;
|
||||
default = name;
|
||||
description = "The name of the user";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
optsGroup = { name, config, ... }: {
|
||||
freeformType = format.type;
|
||||
options = {
|
||||
display_name = lib.mkOption {
|
||||
type = types.str;
|
||||
default = name;
|
||||
description = "The display name of the group";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
in {
|
||||
options = {
|
||||
services.lldap = {
|
||||
provisionUsername = lib.mkOption {
|
||||
type = types.str;
|
||||
description = "Username to use when signing into lldap";
|
||||
};
|
||||
|
||||
provisionPasswordFile = lib.mkOption {
|
||||
type = types.path;
|
||||
description = "Path for the password file to authenticate the user";
|
||||
};
|
||||
|
||||
provision = {
|
||||
group_attributes = lib.mkOption {
|
||||
type = types.attrsOf (types.submodule optsAttributes);
|
||||
default = {};
|
||||
};
|
||||
|
||||
user_attributes = lib.mkOption {
|
||||
type = types.attrsOf (types.submodule optsAttributes);
|
||||
default = {};
|
||||
};
|
||||
|
||||
users = lib.mkOption {
|
||||
type = types.attrsOf (types.submodule optsUser);
|
||||
default = {};
|
||||
example = {
|
||||
user1 = {
|
||||
password = "env:LLDAP_USER1_PASSWORD";
|
||||
mail = "something@something.dk";
|
||||
|
||||
foo = "value for user attribute foo";
|
||||
bar = "value for user attribute bar";
|
||||
groups = [ "group1" "group2" ];
|
||||
};
|
||||
user2 = { user_id = "superuserawesome"; };
|
||||
};
|
||||
};
|
||||
|
||||
groups = lib.mkOption {
|
||||
type = types.attrsOf (types.submodule optsGroup);
|
||||
default = {};
|
||||
example = {
|
||||
base_member = {
|
||||
foo = "value for group attribute foo";
|
||||
bar = "value for group attribute bar";
|
||||
};
|
||||
system = {
|
||||
display_name = "system_service - override display_name";
|
||||
};
|
||||
testgroup = {};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf (cfg.enable && cfg.provision != {}) {
|
||||
systemd.services.lldapsetup = {
|
||||
description = "setup lldap declaratively";
|
||||
wantedBy = [ config.systemd.services.lldap.name "multi-user.target" ];
|
||||
after = [ config.systemd.services.lldap.name ];
|
||||
|
||||
environment = {
|
||||
LLDAP_URL = "${cfg.settings.http_url}:${builtins.toString cfg.settings.http_port}";
|
||||
LLDAP_USERNAME = cfg.provisionUsername;
|
||||
LLDAP_PASSWORD = "file:${cfg.provisionPasswordFile}";
|
||||
};
|
||||
|
||||
path = with pkgs; [
|
||||
lldap
|
||||
];
|
||||
|
||||
script = let
|
||||
pythonEnv = pkgs.python3.withPackages(ps: with ps; [ gql aiohttp requests ]);
|
||||
pythonDir = pkgs.runCommand "lldap-bootstrap" {} ''
|
||||
mkdir -p $out/bootstrap
|
||||
cp -a ${./bootstrap}/. $out/bootstrap
|
||||
'';
|
||||
in ''
|
||||
cd ${pythonDir}
|
||||
${pythonEnv}/bin/python -m bootstrap.main ${configFile}
|
||||
'';
|
||||
};
|
||||
systemd.services.lldap.restartTriggers = [ configFile ];
|
||||
};
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
{ config, lib, ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
./module
|
||||
];
|
||||
|
||||
services.lldap = {
|
||||
provisionUsername = "admin";
|
||||
provisionPasswordFile = config.age.secrets.lldap-admin-user-pass.path;
|
||||
|
||||
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 ];
|
||||
};
|
||||
|
||||
# system users - defined in each service
|
||||
# should not be done here
|
||||
|
||||
# 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"; };
|
||||
};
|
||||
|
||||
# groups
|
||||
groups = {
|
||||
"base_member" = {};
|
||||
"system_service" = {};
|
||||
"system_mail" = {};
|
||||
"nextcloud_admin" = {};
|
||||
"drasl_admin" = {};
|
||||
"grafana_admin" = {};
|
||||
};
|
||||
|
||||
# attributes
|
||||
group_attributes = {
|
||||
group_foo = {
|
||||
attributeType = "STRING";
|
||||
isEditable = true;
|
||||
isVisible = true;
|
||||
};
|
||||
};
|
||||
user_attributes = {
|
||||
membermail = {
|
||||
attributeType = "STRING";
|
||||
isEditable = false;
|
||||
isVisible = true;
|
||||
};
|
||||
membermaildiskquota = {
|
||||
attributeType = "INTEGER";
|
||||
};
|
||||
nextcloudquota = {
|
||||
attributeType = "INTEGER";
|
||||
};
|
||||
};
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
systemd.services.lldapsetup.serviceConfig.EnvironmentFile = config.age.secrets.lldap-user-emails-env.path;
|
||||
}
|
|
@ -56,6 +56,7 @@ in {
|
|||
max_upload_size = max_upload_size;
|
||||
|
||||
# only authenticated media
|
||||
# TODO: Should default to true at some point
|
||||
enable_authenticated_media = true;
|
||||
|
||||
# retentien policies
|
||||
|
@ -74,12 +75,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 +123,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"
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
imports = [
|
||||
./matrix-synapse.nix
|
||||
./element.nix
|
||||
./housecleaning.nix
|
||||
];
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
}
|
|
@ -8,7 +8,6 @@ import argparse
|
|||
import logging
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
|
||||
logging.basicConfig()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -95,10 +94,10 @@ def extract_secrets() -> dict[str, str]:
|
|||
def index():
|
||||
# extract user information
|
||||
user_info = {
|
||||
"username": request.headers.get(os.environ.get("AUTH_PROXY_USERNAME")),
|
||||
"name": request.headers.get(os.environ.get("AUTH_PROXY_NAME")),
|
||||
"groups": request.headers.get(os.environ.get("AUTH_PROXY_GROUPS")),
|
||||
"email": request.headers.get(os.environ.get("AUTH_PROXY_EMAIL")),
|
||||
"username": request.headers.get("Remote-User"),
|
||||
"name": request.headers.get("Remote-Name"),
|
||||
"groups": request.headers.get("Remote-Groups"),
|
||||
"email": request.headers.get("Remote-Email"),
|
||||
}
|
||||
tmpl_firstpass = render_template_string(
|
||||
tmpl_index,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
{ config, pkgs, ... }:
|
||||
|
||||
let
|
||||
urlpath = "/members";
|
||||
|
@ -9,14 +9,6 @@ in {
|
|||
description = "members area website";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "networking.target" ];
|
||||
|
||||
environment = {
|
||||
AUTH_PROXY_USERNAME = config.mine.shared.lib.authelia.protectedHeaders.username;
|
||||
AUTH_PROXY_GROUPS = config.mine.shared.lib.authelia.protectedHeaders.groups;
|
||||
AUTH_PROXY_EMAIL = config.mine.shared.lib.authelia.protectedHeaders.email;
|
||||
AUTH_PROXY_NAME = config.mine.shared.lib.authelia.protectedHeaders.name;
|
||||
};
|
||||
|
||||
serviceConfig = {
|
||||
ExecStart = let
|
||||
pythonEnv = pkgs.python3.withPackages(ps: with ps; [ flask ]);
|
||||
|
@ -26,25 +18,10 @@ in {
|
|||
};
|
||||
|
||||
services.nginx.virtualHosts."${config.mine.shared.settings.domain}" = config.mine.shared.lib.authelia.mkProtectedWebsite {
|
||||
locations."${urlpath}" = config.mine.shared.lib.authelia.mkProtectedLocation {
|
||||
endpoint = urlpath;
|
||||
vhostConfig.locations."${urlpath}" = {
|
||||
# extraConfig = "rewrite ^${urlpath}(.*)$ /$1 break;";
|
||||
proxyPass = "http://localhost:${builtins.toString port}";
|
||||
};
|
||||
};
|
||||
|
||||
mine.shared.meta.website-members = {
|
||||
name = "Members Website";
|
||||
description = "This website you are looking at right now, which is our members website.";
|
||||
url = "https://${config.mine.shared.settings.domain}${urlpath}";
|
||||
|
||||
package = {
|
||||
name = "members-website";
|
||||
version = "v0.0.1";
|
||||
meta = with lib; {
|
||||
description = "Members website for ${config.mine.shared.settings.domain}";
|
||||
license = licenses.free;
|
||||
homepage = "https://git.fricloud.dk/fricloud/server-configs/src/branch/main/machines/gerd/services/member-website/app.py";
|
||||
platforms = platforms.all;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,68 +0,0 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
svc_domain = "miniflux.${config.mine.shared.settings.domain}";
|
||||
port = 6466;
|
||||
|
||||
listenOn = "localhost:${builtins.toString port}";
|
||||
httpListenOn = "http://${listenOn}";
|
||||
in {
|
||||
services.miniflux = {
|
||||
enable = true;
|
||||
|
||||
config = {
|
||||
# listen only on localhost
|
||||
LISTEN_ADDR = listenOn;
|
||||
|
||||
# setup the correct baseurl
|
||||
BASE_URL = "https://${svc_domain}";
|
||||
|
||||
# disable admin account, disable local auth
|
||||
CREATE_ADMIN = 0;
|
||||
DISABLE_LOCAL_AUTH = "true";
|
||||
|
||||
# use auth proxy
|
||||
AUTH_PROXY_HEADER = config.mine.shared.lib.authelia.protectedHeaders.username;
|
||||
AUTH_PROXY_USER_CREATION = "true";
|
||||
|
||||
# For privacy, proxy images instead of hotlinking
|
||||
MEDIA_PROXY_RESOURCE_TYPES = "image,audio,video";
|
||||
MEDIA_PROXY_MODE = "all";
|
||||
};
|
||||
};
|
||||
|
||||
# nginx
|
||||
services.nginx.virtualHosts."${svc_domain}" = config.mine.shared.lib.authelia.mkProtectedWebsite {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
|
||||
locations."/" = config.mine.shared.lib.authelia.mkProtectedLocation {
|
||||
proxyPass = httpListenOn;
|
||||
};
|
||||
|
||||
# allow API for mobile apps etc.
|
||||
locations."/v1".proxyPass = httpListenOn;
|
||||
|
||||
# allow sharing
|
||||
locations."/share".proxyPass = httpListenOn;
|
||||
|
||||
# used for sharing
|
||||
# TODO: would be nice if nginx could serve this instead
|
||||
locations."~* \.(js|jpg|png|css)$".proxyPass = httpListenOn;
|
||||
};
|
||||
|
||||
# meta
|
||||
mine.shared.meta.miniflux = {
|
||||
name = "Miniflux";
|
||||
description = "We host our own miniflux, use it to read all your feeds!";
|
||||
url = "https://${svc_domain}";
|
||||
|
||||
package = let
|
||||
pkg = config.services.miniflux.package;
|
||||
in {
|
||||
name = pkg.pname;
|
||||
version = pkg.version;
|
||||
meta = pkg.meta;
|
||||
};
|
||||
};
|
||||
}
|
|
@ -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
|
||||
];
|
||||
}
|
|
@ -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}";
|
||||
};
|
||||
}
|
|
@ -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) ];
|
||||
}];
|
||||
}
|
||||
];
|
||||
}
|
|
@ -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}" ];
|
||||
}];
|
||||
}
|
||||
];
|
||||
}
|
|
@ -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}"];
|
||||
}];
|
||||
}
|
||||
];
|
||||
}
|
|
@ -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}"];
|
||||
}];
|
||||
}
|
||||
];
|
||||
}
|
|
@ -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 ];
|
||||
}];
|
||||
}
|
||||
];
|
||||
}
|
|
@ -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}" ];
|
||||
}];
|
||||
}
|
||||
];
|
||||
}
|
|
@ -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}"];
|
||||
}];
|
||||
}
|
||||
];
|
||||
}
|
|
@ -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}" ];
|
||||
}];
|
||||
}
|
||||
];
|
||||
}
|
|
@ -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 ];
|
||||
}];
|
||||
}
|
||||
];
|
||||
}
|
|
@ -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";
|
||||
}];
|
||||
}
|
||||
];
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
{ config, ... }:
|
||||
|
||||
{
|
||||
services.prometheus.scrapeConfigs = [
|
||||
{
|
||||
job_name = "uptime-kuma";
|
||||
static_configs = [{
|
||||
targets = [ "localhost:${builtins.toString config.services.uptime-kuma.settings.PORT}" ];
|
||||
}];
|
||||
}
|
||||
];
|
||||
}
|
|
@ -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}" ];
|
||||
}];
|
||||
}
|
||||
];
|
||||
}
|
|
@ -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} -"
|
||||
];
|
||||
}
|
|
@ -14,11 +14,7 @@ in {
|
|||
|
||||
environmentFile = config.age.secrets.murmur-env.path;
|
||||
password = "$MURMUR_PASSWORD";
|
||||
welcometext = ''Welcome to ${config.mine.shared.settings.brand}s Mumble server!
|
||||
Feel free to join our Teeworlds server while waiting. Credentials can be found on the members website here ${config.mine.shared.meta.website-members.url}.
|
||||
'';
|
||||
|
||||
bandwidth = 130000;
|
||||
welcometext = "Welcome to Friclouds Mumble server!";
|
||||
};
|
||||
|
||||
# set superpassword on start from secrets
|
||||
|
|
|
@ -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
|
||||
|
@ -127,7 +125,7 @@ let
|
|||
in {
|
||||
services.nextcloud = {
|
||||
enable = true;
|
||||
package = pkgs.nextcloud31;
|
||||
package = pkgs.nextcloud29;
|
||||
datadir = stateDir;
|
||||
|
||||
config.adminpassFile = config.age.secrets.nextcloud-admin-pass.path;
|
||||
|
@ -140,12 +138,12 @@ in {
|
|||
# apps
|
||||
extraAppsEnable = true;
|
||||
extraApps = {
|
||||
inherit (config.services.nextcloud.package.packages.apps) contacts calendar tasks gpoddersync;
|
||||
inherit (config.services.nextcloud.package.packages.apps) contacts calendar tasks;
|
||||
oidc_login = let
|
||||
version = "3.2.2";
|
||||
version = "3.1.1";
|
||||
# TODO(eyJhb): add to niv
|
||||
in pkgs.fetchNextcloudApp {
|
||||
sha256 = "sha256-RLYquOE83xquzv+s38bahOixQ+y4UI6OxP9HfO26faI=";
|
||||
sha256 = "sha256-b/tKk+y+ZypCHGNDtunDua2msYD6/TzA0haoC0k85F4=";
|
||||
url = "https://github.com/pulsejet/nextcloud-oidc-login/releases/download/v${version}/oidc_login.tar.gz";
|
||||
license = "agpl3Only";
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -168,6 +166,7 @@ in {
|
|||
oidc_login_proxy_ldap = true; # SUPER IMPORTANT!
|
||||
oidc_login_auto_redirect = true;
|
||||
oidc_login_button_text = "Log in with Authelia";
|
||||
oidc_login_use_id_token = true;
|
||||
oidc_login_attributes = {
|
||||
id = "preferred_username";
|
||||
ldap_uid = "preferred_username";
|
||||
|
@ -177,32 +176,14 @@ 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 ''
|
||||
${nextcloudSetupLdap}
|
||||
# TODO(eyJhb): fix, broken by upstream here
|
||||
# https://github.com/NixOS/nixpkgs/commit/2ce1e841032eac4913f2cd3dce416da3d5c799ef
|
||||
# ${nextcloudSetupAdmin}
|
||||
${nextcloudSetupAdmin}
|
||||
${nextcloudSetupEncryption}
|
||||
${nextcloudSetupStorageQuota}
|
||||
'';
|
||||
|
@ -222,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"
|
||||
|
|
|
@ -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)
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
|
@ -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"
|
||||
|
|
@ -1,148 +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" ];
|
||||
claims_policy = "default";
|
||||
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;
|
||||
};
|
||||
};
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
svc_domain = "searx.${config.mine.shared.settings.domain}";
|
||||
port = 7378;
|
||||
in {
|
||||
services.searx = {
|
||||
enable = true;
|
||||
runInUwsgi = true;
|
||||
redisCreateLocally = true;
|
||||
|
||||
environmentFile = config.age.secrets.searx-env.path;
|
||||
|
||||
uwsgiConfig.http = "127.0.0.1:${builtins.toString port}";
|
||||
settings = {
|
||||
general.debug = false;
|
||||
server = {
|
||||
base_url = "https://${svc_domain}";
|
||||
secret_key = "@SECRET_KEY@";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# 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}";
|
||||
};
|
||||
};
|
||||
|
||||
# meta
|
||||
mine.shared.meta.searx = {
|
||||
name = "searXNG";
|
||||
description = "We host our own searXNG, use it to search the web!";
|
||||
url = "https://${svc_domain}";
|
||||
|
||||
package = let
|
||||
pkg = config.services.searx.package;
|
||||
in {
|
||||
name = pkg.pname;
|
||||
version = pkg.version;
|
||||
meta = pkg.meta;
|
||||
};
|
||||
};
|
||||
}
|
|
@ -7,13 +7,7 @@
|
|||
|
||||
mine.shared.meta.stalwart = {
|
||||
name = "Stalwart Mail";
|
||||
description = ''
|
||||
We host our own mailserver, which can be reached on ${config.mine.shared.settings.mail.domain} with your LDAP username and password.
|
||||
From here encryption-at-rest can be setup with either OpenPGP or S/MIME. Keep in mind, Change Password, Two-factor Auth and App Passwords
|
||||
do not work, as we are using LDAP for authentication instead.
|
||||
'';
|
||||
|
||||
url = "https://${config.mine.shared.settings.mail.domain}";
|
||||
description = "We host our own mailserver, which can be reached on ${config.mine.shared.settings.mail.domain} with your LDAP username and password.";
|
||||
|
||||
package = let
|
||||
pkg = config.services.stalwart-mail.package;
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
svc_domain = "stalwart.${config.mine.shared.settings.domain}";
|
||||
svc_domain_mail = "mail.${config.mine.shared.settings.domain}";
|
||||
# TODO(eyJhb): in theory these domains are not used, they're just
|
||||
# nice to have.. maybe.
|
||||
svc_domain = "mail.${config.mine.shared.settings.domain}";
|
||||
svc_domain_smtp = "smtp.${config.mine.shared.settings.domain}";
|
||||
svc_domain_imap = "imap.${config.mine.shared.settings.domain}";
|
||||
|
||||
|
@ -23,14 +20,20 @@ let
|
|||
stalwart_user = config.users.users.stalwart-mail.name;
|
||||
stalwart_group = config.users.groups.stalwart-mail.name;
|
||||
|
||||
certLocation = config.security.acme.certs."${svc_domain_mail}".directory;
|
||||
certLocation = config.security.acme.certs."${svc_domain}".directory;
|
||||
in {
|
||||
services.stalwart-mail = {
|
||||
enable = true;
|
||||
openFirewall = true;
|
||||
|
||||
package = pkgs.stalwart-mail.overrideAttrs (old: {
|
||||
patches = old.patches ++ [
|
||||
./patches/stalwart-cli-dns-records.patch
|
||||
];
|
||||
});
|
||||
|
||||
settings = {
|
||||
lookup.default.hostname = svc_domain_mail;
|
||||
lookup.default.hostname = svc_domain;
|
||||
|
||||
store.db.path = "${stateDir}/db";
|
||||
|
||||
|
@ -53,10 +56,7 @@ in {
|
|||
filter = let
|
||||
_mkFilter = attrs: ph: config.mine.shared.lib.ldap.mkFilter (lconfig: llib:
|
||||
llib.mkAnd [
|
||||
(llib.mkOr [
|
||||
(llib.mkGroup lconfig.groups.member)
|
||||
(llib.mkGroup lconfig.groups.system_mail)
|
||||
])
|
||||
(llib.mkGroup lconfig.groups.member)
|
||||
(llib.mkOr (lib.forEach attrs (v: llib.mkSearch v ph)))
|
||||
]
|
||||
);
|
||||
|
@ -64,27 +64,23 @@ in {
|
|||
attrs = config.mine.shared.settings.ldap.attr // { emailAlias = "mailAlias"; emailList = "mailList"; };
|
||||
in {
|
||||
name = _mkFilter [ attrs.uid ] "?";
|
||||
email = _mkFilter [ attrs.membermail ] "?";
|
||||
email = _mkFilter [ attrs.email attrs.emailAlias attrs.emailList ] "?";
|
||||
verify = _mkFilter [ attrs.email attrs.emailAlias ] "*?*";
|
||||
expand = _mkFilter [ attrs.emailList ] "?";
|
||||
domains = _mkFilter [ attrs.email attrs.emailAlias ] "*@?";
|
||||
};
|
||||
|
||||
attributes = config.mine.shared.lib.ldap.mkScope (lconfig: llib: {
|
||||
name = lconfig.attr.uid;
|
||||
# name = lconfig.attr.member_mail;
|
||||
description = lconfig.attr.firstname;
|
||||
email = lconfig.attr.membermail;
|
||||
quota = lconfig.attr.membermaildiskquota;
|
||||
attributes = {
|
||||
name = "uid";
|
||||
class = "objectClass";
|
||||
description = "givenName";
|
||||
secret = "uid";
|
||||
groups = "memberOf";
|
||||
# we dont have access to this in lldap, and
|
||||
# therefore we use secret-changed instead
|
||||
# secret = lconfig.attr.stalwart_secret;
|
||||
secret = lconfig.attr.creationdate;
|
||||
# TODO(eyJhb): remove once LLDAP gets
|
||||
# plugin support, so we can make a plugin
|
||||
# that updates a attribute on password updates
|
||||
# https://github.com/lldap/lldap/pull/1119
|
||||
secret-changed = lconfig.attr.creationdate;
|
||||
});
|
||||
email = "mail";
|
||||
# email-alias = "mailAlias";
|
||||
# quota = "diskQuota";
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
storage.directory = "ldap";
|
||||
|
@ -95,7 +91,7 @@ in {
|
|||
submissions = { bind = [ "[::]:${builtins.toString ports.submissions}"]; protocol = "smtp"; tls.implicit = true; };
|
||||
imaptls = { bind = [ "[::]:${builtins.toString ports.imaptls}"]; protocol = "imap"; tls.implicit = true; };
|
||||
|
||||
management = { bind = [ "[::]:${builtins.toString ports.http_management}" ]; protocol = "http"; };
|
||||
management = { bind = [ "127.0.0.1:${builtins.toString ports.http_management}" ]; protocol = "http"; };
|
||||
};
|
||||
|
||||
certificate.domain = {
|
||||
|
@ -147,25 +143,17 @@ in {
|
|||
];
|
||||
|
||||
# setup certs
|
||||
services.nginx.virtualHosts."${svc_domain_mail}" = {
|
||||
services.nginx.virtualHosts."${svc_domain}" = {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
serverAliases = [ svc_domain_smtp svc_domain_imap ];
|
||||
root = pkgs.writeTextDir "index.html" "Nothing.";
|
||||
};
|
||||
|
||||
services.nginx.virtualHosts."${svc_domain}" = {
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
locations."/".proxyPass = "http://localhost:${builtins.toString ports.http_management}";
|
||||
};
|
||||
|
||||
# need to change group to stalwart-mail for cert + add nginx to stalwart-mail group to do HTTP ACME
|
||||
users.users.nginx.extraGroups = [ stalwart_group ];
|
||||
security.acme.certs."${svc_domain_mail}" = {
|
||||
group = stalwart_group;
|
||||
reloadServices = [ config.systemd.services.stalwart-mail.name ];
|
||||
};
|
||||
security.acme.certs."${svc_domain}".group = stalwart_group;
|
||||
|
||||
|
||||
# setup secrets for stalwart
|
||||
# setup access to ldap bind user credential
|
||||
|
@ -174,7 +162,6 @@ in {
|
|||
|
||||
mine.shared.settings.mail = {
|
||||
domain = svc_domain;
|
||||
domain_mail = svc_domain_mail;
|
||||
domain_smtp = svc_domain_smtp;
|
||||
domain_imap = svc_domain_imap;
|
||||
|
||||
|
|
|
@ -11,10 +11,10 @@
|
|||
password = "$TEEWORLDS_PASSWORD";
|
||||
};
|
||||
|
||||
mine.shared.meta.teeworlds = rec {
|
||||
mine.shared.meta.teeworlds = {
|
||||
name = "Teeworlds";
|
||||
description = ''We host our own Teeworlds instance. Connect using `nix-shell -p teeworlds --run 'teeworlds "connect ${url}" "password {{secrets.TEEWORLDS_PASSWORD}}"'`, the password is {{secrets.TEEWORLDS_PASSWORD}}'';
|
||||
url = "teeworlds.${config.mine.shared.settings.domain}";
|
||||
description = ''We host our own Teeworlds instance. Connect using `nix-shell -p teeworlds --run 'teeworlds "connect teeworlds.fricloud.dk" "password {{secrets.TEEWORLDS_PASSWORD}}"'`, the password is {{secrets.TEEWORLDS_PASSWORD}}'';
|
||||
url = "";
|
||||
|
||||
secrets.auth = config.age.secrets.teeworlds-env.path;
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
{ config, lib, ... }:
|
||||
|
||||
let
|
||||
svc_domain = "wger.${config.mine.shared.settings.domain}";
|
||||
port = config.services.wger.port;
|
||||
in {
|
||||
imports = [
|
||||
./wgerpkg/module.nix
|
||||
];
|
||||
|
||||
services.wger = {
|
||||
enable = true;
|
||||
|
||||
configureRedis = true;
|
||||
configurePostgres = true;
|
||||
|
||||
dataDir = config.mine.zfsMounts."rpool/safe/svcs/wger";
|
||||
|
||||
# wger specific settings
|
||||
wgerSettings = {
|
||||
EMAIL_FROM = "wger Workout Manager <wger@${config.mine.shared.settings.domain}>";
|
||||
|
||||
ALLOW_GUEST_USERS = false;
|
||||
ALLOW_REGISTRATION = false;
|
||||
};
|
||||
|
||||
# django specific settings
|
||||
djangoSettings = let
|
||||
headerToDjangoHeader = v: "HTTP_" + (lib.toUpper ((lib.replaceStrings [ "-" ] [ "_" ] v)));
|
||||
in 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;
|
||||
EMAIL_PORT = config.mine.shared.settings.mail.ports.submissions;
|
||||
EMAIL_USE_SSL = true;
|
||||
EMAIL_HOST_USER = "wger";
|
||||
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";
|
||||
# };
|
||||
# };
|
||||
};
|
||||
};
|
||||
|
||||
# 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."/api/v2/register" = config.mine.shared.lib.authelia.mkProtectedLocation {
|
||||
proxyPass = "http://localhost:${builtins.toString port}";
|
||||
};
|
||||
|
||||
locations."/static".root = "${config.services.wger.package}/share";
|
||||
locations."/media".root = "${config.services.wger.dataDir}";
|
||||
locations."/api".proxyPass = "http://localhost:${builtins.toString port}";
|
||||
};
|
||||
|
||||
# setup lldap user for wger 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;
|
||||
});
|
||||
|
||||
# setup permissions
|
||||
age.secrets.wger-ldap-pass.owner = config.services.wger.user;
|
||||
|
||||
# metadata
|
||||
mine.shared.meta.wger = {
|
||||
name = "Wger";
|
||||
description = "We host Wger, which is a FLOSS fitness/workout/nutrition and weight tracker, with FLOSS apps, read more [here](https://wger.de/).";
|
||||
url = "https://${svc_domain}";
|
||||
|
||||
package = let
|
||||
pkg = config.services.wger.package;
|
||||
in {
|
||||
name = pkg.pname;
|
||||
version = pkg.version;
|
||||
meta = pkg.meta;
|
||||
};
|
||||
};
|
||||
}
|
|
@ -1,176 +0,0 @@
|
|||
{
|
||||
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
|
||||
}:
|
||||
|
||||
let
|
||||
frontend = callPackage ./frontend.nix {};
|
||||
in buildPythonPackage rec {
|
||||
pname = "wger";
|
||||
version = "2.3";
|
||||
pyproject = true;
|
||||
|
||||
# src = fetchFromGitHub {
|
||||
# owner = "wger-project";
|
||||
# repo = "wger";
|
||||
# rev = version;
|
||||
# hash = "sha256-riJyVl0/GwAGkcHVzkJc666owPk1E4ca8DV5qTjEbjk=";
|
||||
# };
|
||||
# TMP: until it's merged
|
||||
src = fetchFromGitHub {
|
||||
owner = "eyJhb";
|
||||
repo = "wger";
|
||||
rev = "proxyauthheaderv2";
|
||||
hash = "sha256-9GMU7CSMKcgBFYrUh6m9LFiJQ7XLkhaJ8EPt+FSZFqY=";
|
||||
};
|
||||
# src = /tmp/wger;
|
||||
|
||||
build-system = [
|
||||
hatchling
|
||||
];
|
||||
|
||||
patches = [
|
||||
./patches/pyproject.patch
|
||||
./patches/manage.patch
|
||||
./patches/exercises-no-gifs.patch
|
||||
];
|
||||
|
||||
propagatedBuildInputs = [
|
||||
bleach
|
||||
celery
|
||||
django-crispy-bootstrap5
|
||||
django
|
||||
django-activity-stream
|
||||
django-axes
|
||||
django-compressor
|
||||
django-cors-headers
|
||||
django-crispy-forms
|
||||
django-email-verification
|
||||
django-environ
|
||||
django-filter
|
||||
django-formtools
|
||||
django-prometheus
|
||||
django-recaptcha
|
||||
django-simple-history
|
||||
django-sortedm2m
|
||||
django-storages
|
||||
djangorestframework
|
||||
djangorestframework-simplejwt
|
||||
drf-spectacular
|
||||
easy-thumbnails
|
||||
flower
|
||||
fontawesomefree
|
||||
icalendar
|
||||
invoke
|
||||
openfoodfacts
|
||||
pillow
|
||||
reportlab
|
||||
requests
|
||||
tqdm
|
||||
tzdata
|
||||
|
||||
# extra??
|
||||
redis
|
||||
django-redis
|
||||
drf-spectacular-sidecar
|
||||
django-bootstrap-breadcrumbs
|
||||
psycopg2
|
||||
];
|
||||
|
||||
postPatch = ''
|
||||
cp manage.py wger/manage.py
|
||||
'';
|
||||
|
||||
# fixup compressed files
|
||||
postBuild = let
|
||||
staticSettings = writeText "static_settings.py" ''
|
||||
import os
|
||||
|
||||
DEBUG = False
|
||||
STATIC_ROOT = os.environ["static"]
|
||||
COMPRESS_OFFLINE = True
|
||||
# So we don't need postgres dependencies
|
||||
DATABASES = {}
|
||||
'';
|
||||
in ''
|
||||
# copy over static yarn things
|
||||
# cp -a ${frontend}/static/yarn $out/${python.sitePackages}/wger/core/static
|
||||
cp -a ${frontend}/static/yarn wger/core/static
|
||||
|
||||
python3 -m wger create-settings -s $PWD/tmp_settings.py
|
||||
cat ${staticSettings} >> $PWD/tmp_settings.py
|
||||
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
|
||||
|
||||
popd
|
||||
'';
|
||||
|
||||
postInstall = ''
|
||||
rm -rf $out/${python.sitePackages}/wger/core/static
|
||||
cp -a tmpstatic $out/${python.sitePackages}/wger/core/static
|
||||
|
||||
mkdir $out/share
|
||||
cp -a $out/${python.sitePackages}/wger/core/static $out/share
|
||||
'';
|
||||
|
||||
pythonImportsCheck = [
|
||||
"wger"
|
||||
];
|
||||
|
||||
meta = {
|
||||
description = "";
|
||||
homepage = "https://github.com/wger-project/wger";
|
||||
license = lib.licenses.agpl3Only;
|
||||
maintainers = with lib.maintainers; [ eyjhb ];
|
||||
mainProgram = "wger";
|
||||
};
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
{
|
||||
lib,
|
||||
buildPythonPackage,
|
||||
fetchFromGitHub,
|
||||
setuptools,
|
||||
wheel,
|
||||
django,
|
||||
}:
|
||||
|
||||
buildPythonPackage rec {
|
||||
pname = "django-activity-stream";
|
||||
version = "2.0.0";
|
||||
pyproject = true;
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "justquick";
|
||||
repo = "django-activity-stream";
|
||||
rev = version;
|
||||
hash = "sha256-fZrZDCWBFx1R9GGcTkjos7blSBNx1JTdTIVLKz+E2+c=";
|
||||
};
|
||||
|
||||
build-system = [
|
||||
setuptools
|
||||
wheel
|
||||
];
|
||||
|
||||
dependencies = [
|
||||
django
|
||||
];
|
||||
|
||||
pythonImportsCheck = [
|
||||
# "django_activity_stream"
|
||||
"actstream"
|
||||
];
|
||||
|
||||
meta = {
|
||||
description = "Generate generic activity streams from the actions on your site. Users can follow any actors' activities for personalized streams";
|
||||
homepage = "https://github.com/justquick/django-activity-stream";
|
||||
changelog = "https://github.com/justquick/django-activity-stream/blob/${src.rev}/CHANGELOG.rst";
|
||||
license = lib.licenses.bsd3;
|
||||
maintainers = with lib.maintainers; [ ];
|
||||
};
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
{
|
||||
lib,
|
||||
buildPythonPackage,
|
||||
fetchFromGitHub,
|
||||
setuptools,
|
||||
wheel,
|
||||
django,
|
||||
six,
|
||||
}:
|
||||
|
||||
buildPythonPackage rec {
|
||||
pname = "bootstrap-breadcrumbs";
|
||||
version = "0.9.2";
|
||||
pyproject = true;
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "prymitive";
|
||||
repo = "bootstrap-breadcrumbs";
|
||||
rev = version;
|
||||
hash = "sha256-w6s3LL/skzz4EnWtdsa5GXeISrJzr4yQ8hm/gQMva1o=";
|
||||
};
|
||||
|
||||
patches = [
|
||||
./patches/breadcrumbs.patch
|
||||
];
|
||||
|
||||
build-system = [
|
||||
setuptools
|
||||
wheel
|
||||
];
|
||||
|
||||
dependencies = [
|
||||
django
|
||||
six
|
||||
];
|
||||
|
||||
pythonImportsCheck = [
|
||||
# "bootstrap_breadcrumbs"
|
||||
];
|
||||
|
||||
meta = {
|
||||
description = "Django template tags for easy breadcrumbs using twitter bootstrap css classes or custom template";
|
||||
homepage = "https://github.com/prymitive/bootstrap-breadcrumbs";
|
||||
license = lib.licenses.mit;
|
||||
maintainers = with lib.maintainers; [ ];
|
||||
};
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
{
|
||||
lib,
|
||||
buildPythonPackage,
|
||||
fetchFromGitHub,
|
||||
setuptools,
|
||||
wheel,
|
||||
asgiref,
|
||||
coverage,
|
||||
deprecation,
|
||||
django,
|
||||
iniconfig,
|
||||
packaging,
|
||||
pluggy,
|
||||
pyjwt,
|
||||
pytest,
|
||||
pytest-django,
|
||||
sqlparse,
|
||||
validators,
|
||||
}:
|
||||
|
||||
buildPythonPackage rec {
|
||||
pname = "django-email-verification";
|
||||
version = "unstable-2024-07-12";
|
||||
pyproject = true;
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "LeoneBacciu";
|
||||
repo = "django-email-verification";
|
||||
rev = "49e841b96e8bd39f0ad359a75be4711508ac4879";
|
||||
hash = "sha256-4hMSA1d6GOu7Xo7Qq1tBob4lW2zq1E4YaD8w0BnFfVc=";
|
||||
};
|
||||
|
||||
build-system = [
|
||||
setuptools
|
||||
wheel
|
||||
];
|
||||
|
||||
dependencies = [
|
||||
asgiref
|
||||
coverage
|
||||
deprecation
|
||||
django
|
||||
iniconfig
|
||||
packaging
|
||||
pluggy
|
||||
pyjwt
|
||||
pytest
|
||||
pytest-django
|
||||
sqlparse
|
||||
validators
|
||||
];
|
||||
|
||||
pythonImportsCheck = [
|
||||
# "django_email_verification"
|
||||
];
|
||||
|
||||
meta = {
|
||||
description = "A Django app that takes care of verifying a users's email address and activating their profile";
|
||||
homepage = "https://github.com/LeoneBacciu/django-email-verification";
|
||||
license = lib.licenses.mit;
|
||||
maintainers = with lib.maintainers; [ ];
|
||||
};
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
{
|
||||
lib,
|
||||
buildPythonPackage,
|
||||
fetchFromGitHub,
|
||||
setuptools,
|
||||
wheel,
|
||||
django,
|
||||
coveralls,
|
||||
tox,
|
||||
}:
|
||||
|
||||
buildPythonPackage rec {
|
||||
pname = "django-recaptcha";
|
||||
version = "4.0.0";
|
||||
pyproject = true;
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "django-recaptcha";
|
||||
repo = "django-recaptcha";
|
||||
rev = version;
|
||||
hash = "sha256-B6Z9oKcMjSh+zE28k0ipoBppm9dD+Moa+PAZqXVabpA=";
|
||||
};
|
||||
|
||||
build-system = [
|
||||
setuptools
|
||||
wheel
|
||||
];
|
||||
|
||||
dependencies = [
|
||||
django
|
||||
coveralls
|
||||
tox
|
||||
];
|
||||
|
||||
pythonImportsCheck = [
|
||||
# "django_recaptcha"
|
||||
];
|
||||
|
||||
meta = {
|
||||
description = "Django reCAPTCHA form field/widget integration app";
|
||||
homepage = "https://github.com/django-recaptcha/django-recaptcha";
|
||||
changelog = "https://github.com/django-recaptcha/django-recaptcha/blob/${src.rev}/CHANGELOG.md";
|
||||
license = lib.licenses.bsd3;
|
||||
maintainers = with lib.maintainers; [ ];
|
||||
};
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
{
|
||||
lib,
|
||||
buildPythonPackage,
|
||||
fetchFromGitHub,
|
||||
setuptools,
|
||||
wheel,
|
||||
coverage,
|
||||
isort,
|
||||
pycodestyle,
|
||||
pylint-django,
|
||||
}:
|
||||
|
||||
buildPythonPackage rec {
|
||||
pname = "django-sortedm2m";
|
||||
version = "4.0.0";
|
||||
pyproject = true;
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "jazzband";
|
||||
repo = "django-sortedm2m";
|
||||
rev = version;
|
||||
hash = "sha256-Jr3C6teU4On2PiJJV9vW4EEPEuknNCZRVMDMmrs6VY8=";
|
||||
};
|
||||
|
||||
build-system = [
|
||||
setuptools
|
||||
wheel
|
||||
];
|
||||
|
||||
dependencies = [
|
||||
coverage
|
||||
isort
|
||||
pycodestyle
|
||||
pylint-django
|
||||
setuptools
|
||||
];
|
||||
|
||||
pythonImportsCheck = [
|
||||
# "django_sortedm2m"
|
||||
];
|
||||
|
||||
meta = {
|
||||
description = "A transparent sorted ManyToMany field for django";
|
||||
homepage = "https://github.com/jazzband/django-sortedm2m";
|
||||
changelog = "https://github.com/jazzband/django-sortedm2m/blob/${src.rev}/CHANGES.rst";
|
||||
license = lib.licenses.bsd3;
|
||||
maintainers = with lib.maintainers; [ ];
|
||||
};
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
{
|
||||
lib,
|
||||
python3,
|
||||
fetchFromGitHub,
|
||||
mkYarnPackage,
|
||||
fetchYarnDeps,
|
||||
sass,
|
||||
stdenv,
|
||||
yarn,
|
||||
fixup-yarn-lock,
|
||||
}:
|
||||
|
||||
let
|
||||
src = fetchFromGitHub {
|
||||
owner = "wger-project";
|
||||
repo = "wger";
|
||||
rev = "bfca74e88f6c9ff6e917e0ba0e8e9c782ae0047b";
|
||||
hash = "sha256-VuVKgkNp6Omiag72lOn6p51kC/jvApX/kRAPpK95U7w=";
|
||||
};
|
||||
|
||||
offlineCache = fetchYarnDeps {
|
||||
yarnLock = "${src}/yarn.lock";
|
||||
hash = "sha256-olRU6ZGh6bpZ/WfwIKeREJRGd3oo7kEffFx8+4+7s5k=";
|
||||
};
|
||||
in
|
||||
stdenv.mkDerivation {
|
||||
pname = "tetrio-plus";
|
||||
version = "1.0.0";
|
||||
|
||||
src = src;
|
||||
|
||||
nativeBuildInputs = [
|
||||
yarn
|
||||
fixup-yarn-lock
|
||||
sass
|
||||
];
|
||||
|
||||
buildPhase = ''
|
||||
runHook preBuild
|
||||
export HOME=$(mktemp -d)
|
||||
|
||||
yarn config --offline set yarn-offline-mirror ${offlineCache}
|
||||
fixup-yarn-lock yarn.lock
|
||||
yarn install --offline --frozen-lockfile --ignore-platform --ignore-scripts --no-progress --non-interactive
|
||||
|
||||
sass wger/core/static/scss/main.scss wger/core/static/yarn/bootstrap-compiled.css
|
||||
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out
|
||||
cp -a wger/core/static $out/static
|
||||
'';
|
||||
|
||||
meta = {
|
||||
description = "";
|
||||
homepage = "https://github.com/wger-project/wger";
|
||||
license = lib.licenses.agpl3Only;
|
||||
maintainers = with lib.maintainers; [ ];
|
||||
mainProgram = "wger";
|
||||
};
|
||||
}
|
|
@ -1,300 +0,0 @@
|
|||
{ config, pkgs, lib, ... }:
|
||||
|
||||
# TODO: when DEBUG = False, serving static/media does not work, not sure why
|
||||
with lib;
|
||||
let
|
||||
cfg = config.services.wger;
|
||||
|
||||
defaultUser = "wger";
|
||||
|
||||
wgerpkgs = pkgs.python3Packages.callPackage ./default.nix {};
|
||||
|
||||
# generate settings files
|
||||
settingsFormat = pkgs.formats.json {};
|
||||
|
||||
wger_settings_file = pkgs.writeText "settings.json" (builtins.toJSON cfg.wgerSettings);
|
||||
django_settings_file = pkgs.writeText "settings.json" (builtins.toJSON cfg.djangoSettings);
|
||||
settingsFile = pkgs.writeText "settings.py" ''
|
||||
from wger.settings_global import *
|
||||
import json
|
||||
import os
|
||||
|
||||
with open("${django_settings_file}") as f:
|
||||
for k, v in json.load(f).items():
|
||||
if isinstance(v, str) and v.startswith("$"):
|
||||
v = os.environ[v[1:]]
|
||||
if isinstance(v, str) and v.startswith("file:"):
|
||||
v = open(v[5:], "r").read().strip()
|
||||
|
||||
globals()[k] = v
|
||||
|
||||
with open("${wger_settings_file}") as f:
|
||||
for k, v in json.load(f).items():
|
||||
if isinstance(v, str) and v.startswith("$"):
|
||||
v = os.environ[v[1:]]
|
||||
if isinstance(v, str) and v.startswith("file:"):
|
||||
v = open(v[5:], "r").read().strip()
|
||||
|
||||
WGER_SETTINGS[k] = v
|
||||
'';
|
||||
settingsFileDir = pkgs.writeTextDir "settings.py" (builtins.readFile settingsFile);
|
||||
in
|
||||
{
|
||||
meta.maintainers = with maintainers; [ eyjhb ];
|
||||
|
||||
options.services.wger = {
|
||||
enable = mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Enable Wger.
|
||||
'';
|
||||
};
|
||||
|
||||
dataDir = mkOption {
|
||||
type = types.str;
|
||||
default = "/var/lib/wger";
|
||||
description = "Directory to store the Wger data.";
|
||||
};
|
||||
|
||||
mediaDir = mkOption {
|
||||
type = types.str;
|
||||
default = "${cfg.dataDir}/media";
|
||||
defaultText = literalExpression ''"''${dataDir}/media"'';
|
||||
description = "Directory to store the Wger media.";
|
||||
};
|
||||
|
||||
environmentFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = null;
|
||||
example = "/var/lib/teeworlds/teeworlds.env";
|
||||
description = ''
|
||||
Environment file as defined in {manpage}`systemd.exec(5)`.
|
||||
|
||||
Secrets may be passed to the service without adding them to the world-readable
|
||||
Nix store, by specifying placeholder variables as the option value in Nix and
|
||||
setting these variables accordingly in the environment file.
|
||||
|
||||
```
|
||||
# snippet of teeworlds-related config
|
||||
services.teeworlds.settings.SECRET_KEY = "$SECRETS_KEY";
|
||||
```
|
||||
|
||||
```
|
||||
# content of the environment file
|
||||
SECRETS_KEY=verysecretpassword
|
||||
```
|
||||
|
||||
Note that this file needs to be available on the host on which
|
||||
`wger` is running.
|
||||
'';
|
||||
};
|
||||
|
||||
address = mkOption {
|
||||
type = types.str;
|
||||
default = "localhost";
|
||||
description = "Web interface address.";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 28391;
|
||||
description = "Web interface port.";
|
||||
};
|
||||
|
||||
djangoSettings = mkOption {
|
||||
type = lib.types.submodule {
|
||||
freeformType = settingsFormat.type;
|
||||
};
|
||||
|
||||
default = { };
|
||||
};
|
||||
|
||||
wgerSettings = mkOption {
|
||||
type = lib.types.submodule {
|
||||
freeformType = settingsFormat.type;
|
||||
};
|
||||
|
||||
default = { };
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = defaultUser;
|
||||
# TODO: fix this, it is because of the database thing
|
||||
readOnly = true;
|
||||
description = "User under which Wger runs.";
|
||||
};
|
||||
|
||||
configureRedis = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
};
|
||||
|
||||
configurePostgres = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
};
|
||||
|
||||
package = mkPackageOption { wger = wgerpkgs; } "wger" { };
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
services.wger.wgerSettings = {
|
||||
EMAIL_FROM = mkDefault "wger Workout Manager <wger@example.com>";
|
||||
ALLOW_REGISTRATION = mkDefault true;
|
||||
ALLOW_GUEST_USERS = mkDefault true;
|
||||
ALLOW_UPLOAD_VIDEOS = mkDefault false;
|
||||
MIN_ACCOUNT_AGE_TO_TRUST = mkDefault 1;
|
||||
EXERCISE_CACHE_TTL = mkDefault 3600; # 1 hour
|
||||
};
|
||||
|
||||
services.wger.djangoSettings = rec {
|
||||
DEBUG = mkDefault false;
|
||||
|
||||
# configure database as postgresql or sqlite
|
||||
DATABASES.default = if cfg.configurePostgres then {
|
||||
ENGINE = "django.db.backends.postgresql";
|
||||
NAME = "wger";
|
||||
USER = "wger";
|
||||
PASSWORD = "";
|
||||
HOST = "/run/postgresql";
|
||||
PORT = "";
|
||||
} else {
|
||||
ENGINE = "django.db.backends.sqlite3";
|
||||
NAME = "${cfg.dataDir}/database.db";
|
||||
USER = "";
|
||||
PASSWORD = "";
|
||||
HOST = "";
|
||||
PORT = "";
|
||||
};
|
||||
|
||||
SECRET_KEY = "$SECRET_KEY";
|
||||
|
||||
MEDIA_ROOT = cfg.mediaDir;
|
||||
MEDIA_URL = "/media/";
|
||||
|
||||
# EMAIL
|
||||
EMAIL_BACKEND = mkDefault "django.core.mail.backends.console.EmailBackend";
|
||||
|
||||
# Cache - Redis
|
||||
CACHES.default = mkIf cfg.configureRedis {
|
||||
BACKEND = "django_redis.cache.RedisCache";
|
||||
LOCATION = "unix://${config.services.redis.servers.wger.unixSocket}";
|
||||
TIMEOUT = 15 * 24 * 60 * 60; # 15 days
|
||||
OPTIONS.CLIENT_CLASS = "django_redis.client.DefaultClient";
|
||||
};
|
||||
|
||||
# setup allowed hosts
|
||||
# CSRF_TRUSTED_ORIGINS = [ "https://${svc_domain}" ];
|
||||
# ALLOWED_HOSTS = [ svc_domain ];
|
||||
|
||||
# disable recaptcha
|
||||
RECAPTCHA_PUBLIC_KEY = "";
|
||||
RECAPTCHA_PRIVATE_KEY = "";
|
||||
USE_RECAPTCHA = false;
|
||||
|
||||
# does not work
|
||||
STATIC_ROOT = "${cfg.package}/share/static";
|
||||
COMPRESS_ROOT = STATIC_ROOT;
|
||||
COMPRESS_OFFLINE = true;
|
||||
};
|
||||
|
||||
# main service
|
||||
systemd.services.wger = {
|
||||
description = "wger fitness";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "networking.target" ];
|
||||
|
||||
script = let
|
||||
pythonEnv = pkgs.python3.withPackages (ps: with ps; [
|
||||
gunicorn
|
||||
# TODO: fix this, it should work with cfg.package
|
||||
(ps.callPackage ./default.nix {})
|
||||
]);
|
||||
in ''
|
||||
# initial setup
|
||||
${cfg.package}/bin/wger migrate-db -s ${settingsFile} || true
|
||||
# TODO: fix at some point
|
||||
# ${cfg.package}/bin/wger load-fixtures -s ${settingsFile} || true
|
||||
|
||||
# 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}
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
EnvironmentFile = config.age.secrets.wger-env.path;
|
||||
|
||||
Restart = "on-failure";
|
||||
RestartSec = "5s";
|
||||
|
||||
PrivateTmp = "yes";
|
||||
|
||||
User = cfg.user;
|
||||
# TODO: fix this, maybe
|
||||
Group = cfg.user;
|
||||
};
|
||||
};
|
||||
|
||||
# periodic keep up-to-date
|
||||
systemd.timers."wger-housekeeping" = {
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig.OnCalendar = "daily";
|
||||
};
|
||||
|
||||
systemd.services."wger-housekeeping" = {
|
||||
after = [ "wger.service" ];
|
||||
requires = [ "wger.service" ];
|
||||
script = ''
|
||||
WGER_SETTINGS=${settingsFile} ${cfg.package}/bin/manage sync-exercises || true
|
||||
WGER_SETTINGS=${settingsFile} ${cfg.package}/bin/manage download-exercise-images || true
|
||||
WGER_SETTINGS=${settingsFile} ${cfg.package}/bin/manage download-exercise-videos || true
|
||||
# WGER_SETTINGS=${settingsFile} ${cfg.package}/bin/manage sync-ingredients || true
|
||||
${cfg.package}/bin/wger load-online-fixtures -s ${settingsFile} || true
|
||||
WGER_SETTINGS=${settingsFile} ${cfg.package}/bin/manage exercises-health-check || true
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
EnvironmentFile = config.age.secrets.wger-env.path;
|
||||
|
||||
# Type = "oneshot";
|
||||
User = cfg.user;
|
||||
# TODO: fix this, maybe
|
||||
Group = cfg.user;
|
||||
};
|
||||
};
|
||||
|
||||
# postgresql
|
||||
services.postgresql = lib.mkIf cfg.configurePostgres {
|
||||
ensureDatabases = [ cfg.user ];
|
||||
ensureUsers = [{
|
||||
name = cfg.user;
|
||||
ensureDBOwnership = true;
|
||||
}];
|
||||
};
|
||||
|
||||
# redis
|
||||
services.redis.servers.wger = lib.mkIf cfg.configureRedis {
|
||||
enable = true;
|
||||
user = cfg.user;
|
||||
};
|
||||
|
||||
# setup user
|
||||
users = optionalAttrs (cfg.user == defaultUser) {
|
||||
users.${defaultUser} = {
|
||||
group = defaultUser;
|
||||
# TODO: fix this
|
||||
# uid = config.ids.uids.paperless + 2;
|
||||
uid = 738;
|
||||
home = cfg.dataDir;
|
||||
};
|
||||
|
||||
groups.${defaultUser} = {
|
||||
# TODO: fix this
|
||||
# gid = config.ids.gids.paperless + 2;
|
||||
gid = 738;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
{
|
||||
lib,
|
||||
buildPythonPackage,
|
||||
fetchFromGitHub,
|
||||
poetry-core,
|
||||
pydantic,
|
||||
requests,
|
||||
tqdm,
|
||||
pillow,
|
||||
redis,
|
||||
}:
|
||||
|
||||
buildPythonPackage rec {
|
||||
pname = "openfoodfacts-python";
|
||||
version = "2.2.0";
|
||||
pyproject = true;
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "openfoodfacts";
|
||||
repo = "openfoodfacts-python";
|
||||
rev = "v${version}";
|
||||
hash = "sha256-aG+zbFr7lhh5OCdPe7h2XJSwok7sdrnpsEBzPgJ6Bas=";
|
||||
};
|
||||
|
||||
build-system = [
|
||||
poetry-core
|
||||
];
|
||||
|
||||
dependencies = [
|
||||
pydantic
|
||||
requests
|
||||
tqdm
|
||||
];
|
||||
|
||||
optional-dependencies = {
|
||||
Pillow = [
|
||||
pillow
|
||||
];
|
||||
redis = [
|
||||
redis
|
||||
];
|
||||
};
|
||||
|
||||
pythonImportsCheck = [
|
||||
"openfoodfacts"
|
||||
];
|
||||
|
||||
meta = {
|
||||
description = "Python package for Open Food Facts";
|
||||
homepage = "https://github.com/openfoodfacts/openfoodfacts-python";
|
||||
changelog = "https://github.com/openfoodfacts/openfoodfacts-python/blob/${src.rev}/CHANGELOG.md";
|
||||
license = with lib.licenses; [ mit asl20 ];
|
||||
maintainers = with lib.maintainers; [ ];
|
||||
};
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
diff --git a/django_bootstrap_breadcrumbs/templatetags/django_bootstrap_breadcrumbs.py b/django_bootstrap_breadcrumbs/templatetags/django_bootstrap_breadcrumbs.py
|
||||
index 0e98c65..4a4c13e 100644
|
||||
--- a/django_bootstrap_breadcrumbs/templatetags/django_bootstrap_breadcrumbs.py
|
||||
+++ b/django_bootstrap_breadcrumbs/templatetags/django_bootstrap_breadcrumbs.py
|
||||
@@ -12,7 +12,7 @@ from inspect import ismethod
|
||||
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
-from django.utils.encoding import smart_text
|
||||
+from django.utils.encoding import smart_str
|
||||
from django.db.models import Model
|
||||
from django.conf import settings
|
||||
from django import template, VERSION
|
||||
@@ -148,7 +148,7 @@ def render_breadcrumbs(context, *args):
|
||||
kwargs=view_kwargs, current_app=current_app)
|
||||
except NoReverseMatch:
|
||||
url = viewname
|
||||
- links.append((url, smart_text(label) if label else label))
|
||||
+ links.append((url, smart_str(label) if label else label))
|
||||
|
||||
if not links:
|
||||
return ''
|
|
@ -1,32 +0,0 @@
|
|||
diff --git a/wger/exercises/api/views.py b/wger/exercises/api/views.py
|
||||
index d6387bb2b..86bca386b 100644
|
||||
--- a/wger/exercises/api/views.py
|
||||
+++ b/wger/exercises/api/views.py
|
||||
@@ -374,12 +374,13 @@ def search(request):
|
||||
image = image_obj.image.url
|
||||
t = get_thumbnailer(image_obj.image)
|
||||
thumbnail = None
|
||||
- try:
|
||||
- thumbnail = t.get_thumbnail(aliases.get('micro_cropped')).url
|
||||
- except InvalidImageFormatError as e:
|
||||
- logger.info(f'InvalidImageFormatError while processing a thumbnail: {e}')
|
||||
- except OSError as e:
|
||||
- logger.info(f'OSError while processing a thumbnail: {e}')
|
||||
+ if not image.lower().endswith(".gif"):
|
||||
+ try:
|
||||
+ thumbnail = t.get_thumbnail(aliases.get('micro_cropped')).url
|
||||
+ except InvalidImageFormatError as e:
|
||||
+ logger.info(f'InvalidImageFormatError while processing a thumbnail: {e}')
|
||||
+ except OSError as e:
|
||||
+ logger.info(f'OSError while processing a thumbnail: {e}')
|
||||
|
||||
result_json = {
|
||||
'value': translation.name,
|
||||
@@ -393,6 +394,7 @@ def search(request):
|
||||
},
|
||||
}
|
||||
results.append(result_json)
|
||||
+
|
||||
response['suggestions'] = results
|
||||
return Response(response)
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
diff --git a/manage.py b/manage.py
|
||||
index 873291be6..368de89fe 100644
|
||||
--- a/manage.py
|
||||
+++ b/manage.py
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
# Standard Library
|
||||
import sys
|
||||
+import os
|
||||
|
||||
# Django
|
||||
from django.core.management import execute_from_command_line
|
||||
@@ -12,13 +13,20 @@ from wger.tasks import (
|
||||
setup_django_environment,
|
||||
)
|
||||
|
||||
-
|
||||
-if __name__ == '__main__':
|
||||
+def main():
|
||||
# If user passed the settings flag ignore the default wger settings
|
||||
- if not any('--settings' in s for s in sys.argv):
|
||||
+ settings_file = os.getenv("WGER_SETTINGS")
|
||||
+
|
||||
+ if not any('--settings' in s for s in sys.argv) and not settings_file:
|
||||
setup_django_environment(get_path('settings.py'))
|
||||
|
||||
+ if settings_file:
|
||||
+ setup_django_environment(get_path(settings_file))
|
||||
+
|
||||
# Alternative to above
|
||||
# os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
|
||||
|
||||
execute_from_command_line(sys.argv)
|
||||
+
|
||||
+if __name__ == '__main__':
|
||||
+ main()
|
|
@ -1,22 +0,0 @@
|
|||
diff --git a/pyproject.toml b/pyproject.toml
|
||||
index 354f492f6..7163c4ffe 100644
|
||||
--- a/pyproject.toml
|
||||
+++ b/pyproject.toml
|
||||
@@ -37,6 +37,8 @@ Changelog = "https://wger.readthedocs.io/en/latest/changelog.html"
|
||||
|
||||
[project.scripts]
|
||||
wger = "wger.__main__:main"
|
||||
+manage = "wger.manage:main"
|
||||
+
|
||||
|
||||
[tool.setuptools]
|
||||
include-package-data = false
|
||||
@@ -53,6 +55,8 @@ universal = 1
|
||||
# path = "wger/__init__.py"
|
||||
# expression = "get_version()"
|
||||
|
||||
+[tool.hatch.build.targets.wheel.force-include]
|
||||
+"wger/settings_global.py" = "wger/settings_global.py"
|
||||
|
||||
[tool.ruff]
|
||||
# Exclude a variety of commonly ignored directories.
|
|
@ -1,6 +1,2 @@
|
|||
# Fricloud Server Configuration!
|
||||
Bla bla bla, something better at some point, big TODO.
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,13 +1,9 @@
|
|||
age-encryption.org/v1
|
||||
-> ssh-ed25519 QSDXqg tEIy1xIa5R5j/fEdG7bF+DqcHjAM8ECWdVod2VAxPQw
|
||||
icCj7TE8HPoHKVKu6fCQf/xP960k+lThRssBbKDsgZA
|
||||
-> X25519 1zS/IpbtMt6ou+IPqAWkl5ZgzIPuw7f2/P0oOEcNsQ0
|
||||
lA3hqus1M7r9nfAemyVL17WRwaF9FPznt5L5J21Wy/s
|
||||
-> ssh-ed25519 n8n9DQ PtaxammhSzlPG2mJqoPowrGOEhCG8zKhS778Vc8w1CM
|
||||
MwTEogGIqX7tAWTiHWdoYg0prWf0iqRWupxQHllwZ3I
|
||||
-> ssh-ed25519 BTp6UA Ay9Y/HNv1P5d4Y/yQ+IAumk6P8fRcVB/Lo0EDXg4WCk
|
||||
68vCguKTvIvvyjD5jlsYe71wV/OjdTVzM3j6w5c1onk
|
||||
--- K46hZrYKyLnC/zUbRQ48nAeyUFwrD7+JkMyMaKiecGg
|
||||
ÍăŇbóÜ”š`f BŻĺoÎ(Č,•ąďsi}u).zŤ
|
||||
L ·á îŠ<C3AE>LČ8- ŠŐ8ĄďS„÷×mRn=ĆőçÚç
|
||||
aů3˝óKĐńl3´4ť¶¨(ň[]Ń:¤¨¬‰±ku
|
||||
-> ssh-ed25519 QSDXqg 2i+hCYHZQ8bEtQJWnazPdAkDky907gzu1tMod6tIUkQ
|
||||
c7AoKQEZERJziS+b89OP9v3j5BFG1FTcc5yK4U7wHtg
|
||||
-> ssh-ed25519 n8n9DQ O1jM3fRClKiKGaJig/u+APxwi/MzIvs7l/HC+rDiQiw
|
||||
+0VQR4gO/rxXZJRjfv/t+mfaDi0kUioTom8OoNoFDio
|
||||
-> ssh-ed25519 BTp6UA 93ld1x4OCnO4GshJz3Hf7mB2jFVGYqZQ8AwvB7cOqzg
|
||||
AMFa8ueIf3Fz8VQpWWrS6ncfrh+pdsU7RMR3ZjA8KLE
|
||||
--- qDtFEysXwYfNfu63ufZFt2lARP72Gkx0Kp6zs81VkT8
|
||||
Oj´}¼4VfĬj¢Ç\cBÁ!9ÏìÚYÚ¨Ô(ìd2©\bÙs5…ïâ2ËhTRœ@êg¼ªÔ·®•„kì9¹S<½wq~ÕÞ%)º^B ÎõJS @Å©x±Í‘1[†Ì0œá>
|
Binary file not shown.
Binary file not shown.
|
@ -1,12 +1,10 @@
|
|||
age-encryption.org/v1
|
||||
-> ssh-ed25519 QSDXqg YoKK3e/O8rsIw9olLq0xOEbUHi/OWUlNTndiM6mW/x4
|
||||
/F9jMFY31Brnj5UHzP6VmxUVvnsfvaM+rJc0gpn2Bc0
|
||||
-> X25519 gGccDs1K9oQJYejMUS0EYJwgDcD0qMvNlgC+mYq0dTg
|
||||
sXZshUFzMF/vlo6KLIAWZD5D/jI35/fS0wKTAehuZB8
|
||||
-> ssh-ed25519 n8n9DQ 7NT1vUQu+HpRFULWtRc/o4J8O5Z3U8m3LlJ8Q2Bq9wo
|
||||
UBDmhGTwtUmghywmzGTwhcu3fHExBFy0rif3pPfblrY
|
||||
-> ssh-ed25519 BTp6UA oSpx5EIkXL8nvmw72WdCwFomtGfpq5+SyrtKCIMu33Q
|
||||
00AEgN3W0kiUivhA+xnfoETHhn4tvgJQMc7caGZ5q4w
|
||||
--- T+xLNMpqfWIaV9dkq8l+kBuADWmtCIFrZhOoOK1fyro
|
||||
õãÑXÔÁ?5åŽ=Cžwf®´<15>‹5l;?2l ²¾YB]ªqãhêÇ¥P’ <E28099>o.¿ð±’1«
|
||||
„û§©–&.5—ÞÆxZ_!¶Ùð\ȸÒÈ=|è—ÍHtòìŠKDëmúD&Ý«=
|
||||
-> ssh-ed25519 QSDXqg s4bJfm5nhl8dESl1yXgQFkCT2nJdKeMVhOC10Z1e1TE
|
||||
m1MEBzSr/GZRdNrw2ceFFVjFfcVOdO3D8dxsg4x/lUU
|
||||
-> ssh-ed25519 n8n9DQ GwPbYmxKFHZ/JJtJV5o/MSi2mYyJtpupT6TF/QAUAjI
|
||||
FZ0WMuYfq3e8Kcp7DAI6kkHVavfVFNm4mIwGbaw1VWk
|
||||
-> ssh-ed25519 BTp6UA QcXiF+NIbadObCT3jK7KnVluDqjFev+XA5xQJwk2cA4
|
||||
/FKzec70a9cuKq3FStESSwbbgUi3Zf5k5xfa45eMB5g
|
||||
--- lwDjO24aMTssxFfekozBYCnigZJ7ztklFwFh0Gn10pA
|
||||
cïPvýqÕÿœæT‰Ï_Kt
``\˜–1_Ô0^S¬ô’BQ8Þ<38>u’Ã}òEËÒϬ¿â3{))š<3®uwCµ‹jëý„R ¡ÏÉ#û@g0xk TÍ8ÊR<C38A>Un·¨$
|
||||
æ³µ
|
|
@ -1,11 +1,9 @@
|
|||
age-encryption.org/v1
|
||||
-> ssh-ed25519 QSDXqg f5BkTN2OAhqz73HpxPFRKynlnOtzvTr7Xiwfrt5OblY
|
||||
vyVQDs1RCxStxJcdL23b7xKJZM7kzx6/CafYbXcvZqA
|
||||
-> X25519 i75r1tQHsdp8/znywa9dPJ290PYZtz6r37pobJd4rF0
|
||||
xk/B0oIOT3C16pZMWzo2ab2507rkVjCDf5JTocHL9uw
|
||||
-> ssh-ed25519 n8n9DQ LCWwNu2knTWEVyM95r7JZRa1ewrbLG8jfkJDcGuCFgM
|
||||
3Oe9WnBHLvOvWdHBHX1/w6oHZAgcAWVLI+3WWNEcJUo
|
||||
-> ssh-ed25519 BTp6UA g2xh6AGZTvC4wl6V9HRZSM/np12aRMI0bhB2r/tRBkI
|
||||
Stk2VwOZL/Mr3jhBEG4GLn5Gve1qjsRltdPn/XQQZBI
|
||||
--- 5pvauehP6HZ7uOzWT2vYN3T9JKLYZ9lVhBOyOC22/YM
|
||||
×ZÛÛ‹ÃP<C383><50>¤Ú%[bãcçµø€ª,—6BÏ}îÄZÆÎ‘jc¢¤íE8O/íå&D}/÷f9·²ºå–ÔD
|
||||
-> ssh-ed25519 QSDXqg ukkpdyQwjxQ5ZSDRNp3scWW/UaL9KkvYjgOohagME2E
|
||||
TBz/F6ki/WRQ36dwWGya/+jk6d/CVit0uk6ftUGwkM0
|
||||
-> ssh-ed25519 n8n9DQ 4tAfBISscjJkXdT2ze7qwjSgXsKVORQdJ6BU2FWziBI
|
||||
KsgjA34+cX5JP5zQDJu2S42T07L1bUH6rFNLnGpGsCk
|
||||
-> ssh-ed25519 BTp6UA MN57AovukP5h7xP0TtdZJnbGUVGem9Ag4yrXeQPLOmc
|
||||
XvlsXC6kI6gbzrujxfGQII2bwPoXd7pAQfP3oXGqe4U
|
||||
--- qWkj5My+16z9Qjge9GR0ezFkzi4zONiEny+I/5j9qpQ
|
||||
*ヲ{|<7C>o訒<6F>i_k}oセ暉<EFBDBE>ー乕z)ァ讐ネQウオク、ニc弥戻ヲH「、<]約・g\{W<><57>7:
|
|
@ -1,11 +1,10 @@
|
|||
age-encryption.org/v1
|
||||
-> ssh-ed25519 QSDXqg 73fdkNhCNfHaCy86Ec3KL5oaZfzsn189LX5VpJSZVCM
|
||||
MdBa9ufGc1Zh7bIph+wQkfwrAlkTLE4KtC59nD9bjvk
|
||||
-> X25519 3uJmCujV7zE6Lf9mbcGCoHrUjceLyRIg4RrJQHoLcQ0
|
||||
P3OP2PH5i/bRTuxeMGOeKsTuN8j9i+1DqT4m3oqLl5g
|
||||
-> ssh-ed25519 n8n9DQ KOQn0fTCgfO/CaBPmlsTERgByfkMBR/g+MAzdCsg5C0
|
||||
C3hY9QIr2DsNk1xz/58h5gKMRb18IXr13NWbKrvxdAA
|
||||
-> ssh-ed25519 BTp6UA SLKD+FrhoE+SIDpV+WdBq9epMHTZsrMEKLvBNc8XQwc
|
||||
DZyFUjhZHuelH5Rs0XV49aqc5PlHZyUqSdy/GgHJ9+g
|
||||
--- 09dMjfzGZgH1Ol73FQxDx/Lg51TFMLePxrOE4WDThFo
|
||||
J—m'Ú¤öî¢Ë_£e»ÞHfPS’ë¢áØÖ-ìG4K銔0иÆåtβÒw×0JUóß9ÞÙŸEóeIëºíÑ#ËÊêðåq+¦3a¢‰ñQ¬ÑXHXŽØà$õëÈÉ
|
||||
-> ssh-ed25519 QSDXqg /Ywa18VQyXbCgwIBWGRDB0m9mNd7TtQH4HEQvJpxLkU
|
||||
NdigMBP4yDz1v6Q8OXGu7lOd4JpxnBJuaWj5xgz/I/w
|
||||
-> ssh-ed25519 n8n9DQ yAQO33Csz6+h8dEKmOvVbZUgxN+nPY6+OvE2W3wBNmI
|
||||
5v8JM8vHAmWUlnYiK+eBhp+BIKwbGSOS4UzFpxuvzEo
|
||||
-> ssh-ed25519 BTp6UA VnmGREd7Rn1c4sYJRo85cvnuH1QBTQxG6P+c/tdat1M
|
||||
0TBJ+a1BBtFBo4beFx5671hIq/pluFJ9wiUK59dZEc0
|
||||
--- qzbsERkRBc+PLfAg8/+MiwO2Rh2bWQi6YD0B1QiyzJ0
|
||||
<EFBFBD>ra•ËteX
PœZ¥Á Ê!Y *ð§aþ™ß;‰í±ˆYöÏá&¶
|
||||
eñ4<>¡¹ÿéì¥UÉz )ºº2«
Ê’ «¤>íº8SßozRÈÁ@·Âè(UÒ´rܹË$åUVóÆßäw
|
|
@ -17,7 +17,6 @@
|
|||
group = "secrets-lldap-bind-user-pass";
|
||||
mode = "0440";
|
||||
};
|
||||
lldap-user-emails-env.file = ./lldap/user-emails-env.age;
|
||||
lldap-bind-user-pass-hedgedoc-env.file = ./lldap/bind-user-pass-hedgedoc-env.age;
|
||||
|
||||
# mumble
|
||||
|
@ -33,49 +32,12 @@
|
|||
# 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;
|
||||
|
||||
# matrix-synapse
|
||||
matrix-synapse-config-authelia-secret.file = ./matrix-synapse/config-authelia-secret.age;
|
||||
|
||||
# wger
|
||||
wger-env.file = ./wger/env.age;
|
||||
wger-ldap-pass.file = ./wger/ldap-pass.age;
|
||||
|
||||
# restic
|
||||
restic-env.file = ./restic/env.age;
|
||||
restic-pass.file = ./restic/pass.age;
|
||||
|
||||
# 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;
|
||||
|
||||
# headscale/headplan
|
||||
headscale-authelia-secret.file = ./headscale/headscale-authelia-secret.age;
|
||||
headplane-env.file = ./headscale/headplane-env.age;
|
||||
};
|
||||
|
||||
users.groups.secrets-lldap-bind-user-pass = {};
|
||||
|
|
|
@ -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…ÉF±<46>þç±yª<79>Çâ7ÛÜ“(»¾<<3C>U±åE¯ãH%,ð¹@o€½¤c
|
||||
ìd“Áö=?zS†ÛPŒl¯þµ~…JÌôVØ[JúŽ;”-ßþvfHÙ¢©ô•
|
Binary file not shown.
|
@ -1,12 +1,10 @@
|
|||
age-encryption.org/v1
|
||||
-> ssh-ed25519 QSDXqg Z2carBfkWnq3pSVRwIsaOK4LtUwQylMzl6qhJrVHPFk
|
||||
84ho1ewfxZLddbwoTS8OfK2+DqQee+ew1QfkM1WodZM
|
||||
-> X25519 zNMm+jdN0pdifxqkCRrwSMlwnDth31GojIrVROyUCEo
|
||||
fpgNEoxiMGDDvj/xvznNRdaS1Q4gHxu02MoqH7zqjN8
|
||||
-> ssh-ed25519 n8n9DQ wIJ3wnHibsg0UrxrzDhKXeXzDgH3wNraL1DTa9K2clU
|
||||
myZVNdu74xFFZ2NiljKHq8/KlJwyhXWB+rkCFTz1IlY
|
||||
-> ssh-ed25519 BTp6UA 7R89FE3UckXyVnSqnxv+fI9FQ8FTeuRIQAy4crC6USc
|
||||
wBWGkwiF4b+fjS7VMDlqLIzMh7oj2VyYujIRDFUo41o
|
||||
--- L89Yp8X8Pte6KefTjNtz4YX+2d66w71EOg6dHHeiPys
|
||||
šÑ ÌÅ¥lº·g$+ŠG%œÏ0®(¸AÊÊÿÊ(7DÒñ®Ž$º
f|Ú'£D«böRÚæL¥š"q¥i„y$A"Q î÷"ô
|
||||
ý¸®Ýœ÷>,¥½ho”8"š›Š!›óºo–L<E28093>ª‰
|
||||
-> ssh-ed25519 QSDXqg 6QDsf2ACEhUdb2IUxDaILcvoPLPdpXupn7DDTQCREWA
|
||||
8VYqwj6UoIee6DHG+bF3lHnY+NxLP8KkjjxynSiEUEk
|
||||
-> ssh-ed25519 n8n9DQ wAIhE1M0sMi+0tdc2fZDFzLJLlaZkhZOXIEkB2lm3UE
|
||||
iG5tvWLkYfv/klmc0LjUn+96RuYTRUrLbMpPicpVchc
|
||||
-> ssh-ed25519 BTp6UA GSOxjXyCvjwg/jS3pG3gegh9jcYbVHy3y4v/dUM7plQ
|
||||
ItU5OQJswLZ3nCpfUcLMhkdEVF6Iu+lPP0qGj9MU7h0
|
||||
--- ONOkuJRp+quOeMUJ6kf+j4Bweks4FAGurAE3xgk3wIw
|
||||
ðà7‡h#p€<>·ŒØ˜ÿ,ŒÙîúà‹SV|^8"‰)¢š§†‡è]ýbiàڡܶġTWêi Aÿ
|
||||
¹\›ò¾%X¨v
«
4Fè”9{<7B>FY’<59>"‹Ä5`wZ¬¼¸MÓ$eý='€a
|
|
@ -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
|
|
@ -1,11 +0,0 @@
|
|||
age-encryption.org/v1
|
||||
-> ssh-ed25519 QSDXqg U2hjU5FeVfd+/agny133FUmLJ47zqpww10DgzHbkKW8
|
||||
k6k+WH6fZ1UZjnOwe3LS6cXYVcucE+kBhuPRMcNFabI
|
||||
-> X25519 JfLnC3qBptSdDwBRo1Zj/rDqKqWFvxWqBANIKhCIVlc
|
||||
iuqSt9gdPjQVRuv9+iUub1zkecSUjZD1XKvtvkNAaOE
|
||||
-> ssh-ed25519 n8n9DQ eVkQODcS4wG+5xliWzqfEcRKvkHaHTzz8BWXH/ld/3k
|
||||
9q0TB7ZLluGIQhFjMcXUmXlGiof5/ACrrHQRWEq4jik
|
||||
-> ssh-ed25519 BTp6UA hGuGXru5pgShG15hEH6B8fYF4x1SK6blgP+3NSfvklI
|
||||
UZYG5ZfErwlBtux5fWh3CPrT/9oWoDjGXX1M0ojtY8c
|
||||
--- NlksmWK4pF3hQkpCkZaCBWvEv7I9TB0zgYRiJpuLB18
|
||||
±±-ke(~ñ…"KktWv´&«í´~ÔúšžÝ¶û¿Žè½éHÆ)ûjQÔÅÐtRe/<2F>C_s.“ƒÓéýÚÚà/º~21ŸòÜ«"æ!á²ÎÓ<C38E>7IƒÅüÙìŒCj¥KÝÛ¼H«ˆ³o!È{ÁüÏþYhÉzcÀ¢³¬go?ßåtˆäãÔÃ<C394><sì©`ÌÇñŠMZgôÙ¨R:þñ3-i@dVÕDÂ^¸…'{R6{Œ€ÑóIĹѯ›Ò¬¼`UÜSJ&E¯/WâÏ >¾JS @`Ùm±h|‰ˆ±Áe3é¨U!;sñíãÔ}õ¼©L—í Àb¿ê$<24>·-R8Vq:ÏíÚ£uûÉ&üÊK\¥™ÉÛiªð¸8¬‡TALO•ÍÂùOñõgñ3ÒfˆêàT›½€
|
Binary file not shown.
|
@ -1,11 +1,10 @@
|
|||
age-encryption.org/v1
|
||||
-> ssh-ed25519 QSDXqg n5RiJL05MGTaHFNBOq4V7aJDdnw2wNI9S4rgHwLLIkE
|
||||
M67SzRRJzEbWzlLp4QJG71dbQ5HEp8R5n6Tmxl//PtQ
|
||||
-> X25519 O3ZSSvjwXHqZbQcrIAiMbbscdYpC29NQKNP+jIX2GW4
|
||||
8fbx+Dfgg+Huueeic272Nz9hXkntkMSt8zoYaUCgzy0
|
||||
-> ssh-ed25519 n8n9DQ RUKMRjuVi4pjCqJO9ZqP5txbBNnq8yQ84Rvqq0ooG3g
|
||||
JuEioORVfSLsOHqSmdQaJBd+xp/yYl/wuJUvhTQEmDY
|
||||
-> ssh-ed25519 BTp6UA NEPujkvmlk9mHl7KeaCKh7lOSanfKlU2lm9dqMy1yEc
|
||||
v2LSptbFd/bEwh+AKsXGnNoALLwyjRYEO8wYaxSp+x8
|
||||
--- vjwuX+eM6Hbzd5URUnhWYArVBNd7q/5+DM6FiTc/0OQ
|
||||
<«×è ݵ.ŒÇ|þÏî2y%Û—ô¡@´@—¦£‹LRQ4SΈhâqSzÝßÜc¥”€Cßå„2)æ>«‰,ðý
|
||||
-> ssh-ed25519 QSDXqg GGeJqXdtwwxIlkG/yl4DfkKykQ3uJyWqLguJ680vZlY
|
||||
LS19/W+IHFSAeog3c2qAzvgE2VDWF81B5ehqo2xoCVk
|
||||
-> ssh-ed25519 n8n9DQ 8xOzOWPQEwAAslYAg71Hf8sf67+QGFKeX280ueXrYVk
|
||||
ZdzT710/gB1N7eosXQbyRdyzQvQDuLeCFS6ocpkvooU
|
||||
-> ssh-ed25519 BTp6UA RyRdwb7gHk74LgqEmWUJ8SpiS94IHczpO2ZokCFO0QY
|
||||
c3t3vZyRqSIWiFnt0slV8AjACKW44PgUvwijLTNigck
|
||||
--- emrYR6UhtLGsqpz7q+KAivD5e0sAf6zaA5qh3vD/13A
|
||||
Ùüù‚×H@ø|²aè›>á3C‰Œ&*µ_8
|
||||
~ç7RÛ†)°$<24>aÄçü–]éD©y±±}ß.Ê:
|
|
@ -1,12 +1,10 @@
|
|||
age-encryption.org/v1
|
||||
-> ssh-ed25519 QSDXqg UVHeL31U+0oPD6WkJcgaGJKn6RCLchQwi4RbV3t1ViY
|
||||
HndUvlRyE4ewwN0I+fjR8/V8i2hpdm2T77T5mkGDwqs
|
||||
-> X25519 471FQ92/VNQ0rMpA4TYpa4XLhl5neg/Rvly+1qMznxk
|
||||
mfGl3rlfbcwYgUusDDt6tpdD5p+/Qqlx47NHDmluqfM
|
||||
-> ssh-ed25519 n8n9DQ Or6JhMO4kbyvjReIeHPk+Qy/mZv3Aq0jmuL+lxGCdk4
|
||||
Z1i3PDxXR3hBxH5JjhQip0KbLJOBDDtG2fftvjA7Urs
|
||||
-> ssh-ed25519 BTp6UA TfNnysrP9cXIHILbD9anYH70wfiloFcZUTX2yavpfkM
|
||||
/Jn50hf5TK8DnOZGniK1Klung9O4SVal3QhUjxXweq0
|
||||
--- m4HRBmPMsLUnS4n1IJwi0+bVPAIdXjgGUDS91dd5kbk
|
||||
Û;)—©ã#^VðA¿<41>®!vˆ‚‚û)4ÇR4z¦‘<C2A6>ÇÔ²;üHl£_y|„Iü}¿…¤ÜÅ·'3ê]ˆ„?ì+í³£NkÅv¿7_
|
||||
“ã;,«Êµºa9[§Á
|
||||
-> ssh-ed25519 QSDXqg lB8SsqHApg4Bmrg+YP4Gns7+UtUb8jOjxEXzTRHT4A8
|
||||
psFKfMdO5j54Q1ISyA+FgWZCRHVmEWXNNkjweqWZ1qs
|
||||
-> ssh-ed25519 n8n9DQ WGAMIZbfqukK5mrTlKYy8rUNy+DEwxFijZiCxOGlX1o
|
||||
7/YPyZbUfJ7SB9T38JwgcW2LAnZSLgMFDbf6N7NbRbI
|
||||
-> ssh-ed25519 BTp6UA ZkEWRVFiMWnQQap3CGb+FihCw/y8funz6UFuxPYsmQU
|
||||
e9h5DaikoTLmsWPIC82DA7EUUOX7X1ZrSmBKeMk9T04
|
||||
--- Uif4qB0IT17YXBP1r36yXjUzO2rd6wuVi9wP0x0D1WY
|
||||
ËÔèw¥Zhõ(È‚ùSú¯ÝFø+;•LÛÕ%þúЛ“vÌ<76>4›ßh?ÞàýdǤ’¯_
|
||||
ŸÎô&Ê%Kì<4B>-ïO+ªš™Díü¯"R™¨A›#_£Ø.
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -1,11 +1,9 @@
|
|||
age-encryption.org/v1
|
||||
-> ssh-ed25519 QSDXqg MSy+VhPuDpa27TVJiIMdTjEoso41n768EhR3xzQQpgY
|
||||
vmzBPGJ0p10rq8I7dC3+aIZ0BnI9Y9s55kaPTVLM/Ls
|
||||
-> X25519 Y9q6F+lxCywb/EKVGr23o3lDariEA2gT1+mlUI75yBQ
|
||||
FS86DVHDG1iwb5EoY+agq7cgpTzC94QwUEL859UvNoM
|
||||
-> ssh-ed25519 n8n9DQ H67QnwjkUMV5vBXTVNonnNuNUfqDjeT1jlh+frpkUgk
|
||||
UyGk2g8qmkl8oLTFPaPg+drNMKHtbDoPrZ8AgMvUWzM
|
||||
-> ssh-ed25519 BTp6UA nuSVT7cP/UdyZPFzFjuo4kWcUzWM9zibc+4KN4gPBDM
|
||||
BBx6wq5MKk5KoFcGt3SEuDNC1jaITZpy3pCE/oUF7vE
|
||||
--- hCZOhBFtLe/I8tRJNtRB2F7fNxLTi7sOo5ZQxCNUha4
|
||||
R-¸¾ê\v¹á/ǨÿÌñ9¬
Ùgñø1·O8ŒÏ&x˜Ôj‘ÈÜ”ŒËâØÁùà8êXvL@6w‚ëj?šfô
|
||||
-> ssh-ed25519 QSDXqg LoWIvj4OQjNPaGbtQYSUEKtkqvcVa2pPisjyXL6ajy0
|
||||
ZfLdRcsWa4Nc6HdiWO1GCgSgHm7aZeUdEDCjUCn6CuY
|
||||
-> ssh-ed25519 n8n9DQ e7DWlUZdaDPgoS0Ylnxtf80IN+QMtCJ48oI4Z4U9+0I
|
||||
/2ZleHBcAkWh8Udt6D2QgBOCTKkqH3GIsGsGexpAaxA
|
||||
-> ssh-ed25519 BTp6UA bgTa1+cFzW07nPhe/5GKW1RreVO5IqIzvPZTYpnrGjY
|
||||
7F4HnAnHVZX+dfOpc5mPB4/TTgPgw8hiIyVTEbffRQw
|
||||
--- IrCqHtOIS3c5By3cBTPQAGpM2GzCu61AhiavRjozk7o
|
||||
<EFBFBD>hカ<15>ネ都・ケ5+RBi}マ黍<EFBE8F>ヘ瀨 ハN$wト:カ![オs<EFBDB5>ツ、゙<EFBDA4>ホア、<EFBDB1>.ョgR・ト「>
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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¡Y‚9ÎY®^€rã®ÔÑ6lƒ6*@G{vœRf÷°I–Ðù7
|
|
@ -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œÇ•
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue