Compare commits

..

59 commits

Author SHA1 Message Date
eyjhb
f9df7a63c5
synapse: remove todo 2025-01-09 20:55:56 +01:00
eyjhb
159c365382
miniflux: proxy all media 2025-01-09 20:55:45 +01:00
242fd429d8 Merge pull request 'miniflux: proxy images' (#3) from proxy_images into main
Reviewed-on: #3
2025-01-09 19:52:08 +00:00
baa7464e8c miniflux: proxy images 2025-01-09 18:24:32 +01:00
eyjhb
8695f46699
adds license 2025-01-07 09:40:24 +01:00
eyjhb
5c47834299
services.wger: enables AUTH_PROXY_HEADER 2025-01-02 17:21:48 +01:00
eyjhb
94e08fd2f0
services.wger: updated to newest version + added auth proxy header 2025-01-02 17:21:21 +01:00
eyjhb
5c65f7f922
authelia.nginx: add auth proxy headers to shared info 2025-01-02 17:18:31 +01:00
eyjhb
9fd8d7b900
authelia.nginx: unset authelia headers when not used
Prevent someone from impersinating users, by setting the header manually
2025-01-02 17:15:18 +01:00
eyjhb
02720387a4
services.miniflux: set base_url correctly 2024-12-31 13:37:52 +01:00
eyjhb
054143d378
services.miniflux: disable authelia for API endpoint 2024-12-31 13:37:18 +01:00
eyjhb
88673b582e
platforms.hetzner: fixed networkd warnings 2024-12-30 22:55:55 +01:00
eyjhb
98408e1970
services.lldap: fixes invalid path for lldap-cli 2024-12-30 22:53:49 +01:00
eyjhb
313a0b6939
server.postgres: fixed typo 2024-12-30 22:53:38 +01:00
eyjhb
46cb83674b
miniflux: initialised 2024-12-30 22:53:22 +01:00
eyjhb
062a3c0a12
gred.lldap.bootstrap: removed helper functions 2024-12-22 22:05:16 +01:00
eyjhb
570b41ac5d
gerd.lldap: bootstrap example.json 2024-12-22 21:59:45 +01:00
eyjhb
06f5d3a5fb
gerd.lldap: added initial bootstrap 2024-12-22 21:57:54 +01:00
eyjhb
5749b1cf66
gerd.lldap: moved files around 2024-12-22 21:56:46 +01:00
eyjhb
629f8f02d7
common-config.journald: only store in-memory, max 100MB, maximum 1d
Clears all logs on reboot, clears data after 100MB or after 1day,
whatever comes first.
2024-12-18 15:20:56 +01:00
eyjhb
d1cb03f213
server.nginx: disables access logging 2024-12-18 15:20:38 +01:00
eyjhb
e6d22c1063
gerd.nextcloud: 29 -> 30, nextcloud.oidc_login: 3.1.1 -> 3.2.0 2024-12-17 13:37:34 +01:00
eyjhb
f160a3ebb6
gerd: adds searx w/ authentication in front 2024-12-17 13:23:00 +01:00
eyjhb
3a0792caec
restic: add daily external backups of all safe/backups 2024-12-07 19:28:04 +01:00
eyjhb
d4d8236501
wger: removed files 2024-12-06 22:33:39 +01:00
eyjhb
68feacc010
wger: turned into a module 2024-12-06 22:32:10 +01:00
eyjhb
3121b57181
wger: ACTUALLY enable Redis 2024-12-06 18:43:05 +01:00
eyjhb
51bb437838
wger: more shit 2024-12-05 20:05:21 +01:00
eyjhb
ff3ea0735c
wger: no-gifs patch + guinicorn 2024-12-05 19:30:01 +01:00
eyjhb
6c33d36fc5
lldap: updated 2024-12-05 17:02:34 +01:00
eyjhb
e7af4737c1
wger: added redis, cache/compressed/combined, nginx serves static 2024-12-05 17:02:18 +01:00
eyjhb
97f9561631
wger: disable sync of ingredients (takes up too much space in DB) 2024-12-05 10:31:38 +01:00
eyjhb
02b66db30a
wger: stay on debug, otherwise weird stuff happens 2024-12-03 22:45:55 +01:00
eyjhb
5da6057228
wger: removed hardcoded constants 2024-12-03 22:08:25 +01:00
eyjhb
913a4a0b26
wger: adds a BUNCH of changes 2024-12-03 22:07:03 +01:00
eyjhb
50fee64475
secrets.wger: added secrets 2024-12-03 22:06:54 +01:00
eyjhb
73597c4cdb
wgerpkgs: changed version 2024-12-03 22:06:39 +01:00
eyjhb
68779da243
gerd: changed zramswap comment 2024-12-03 22:06:25 +01:00
eyjhb
57a5740f6f
authelia-nginx: chnaged how to protect websites 2024-12-03 22:06:04 +01:00
eyjhb
7cde74ce97
ssh.keys: add eyjhb 2nd key 2024-12-03 08:48:18 +01:00
eyjhb
4fd55efab9
gerd: zramswap coment 2024-12-03 08:46:16 +01:00
eyjhb
efb5711551
updated sources 2024-12-03 08:45:53 +01:00
eyjhb
96e004a541
authelia: tmp change 2024-12-03 08:45:45 +01:00
eyjhb
2f2993ac16
gerd.wger: init 2024-12-03 08:45:11 +01:00
eyjhb
80ef4fabc8
gerd: increase zramswap 2024-12-03 08:44:15 +01:00
eyjhb
878c83a8eb
member-website: fix typo 2024-11-20 19:22:37 +01:00
eyjhb
cbbed221c9
authelia-nginx: fix hardcoded domain 2024-11-20 19:21:22 +01:00
eyjhb
43e6fe30f0
teeworld: fix hardcorded domain 2024-11-20 19:21:07 +01:00
eyjhb
791012f209
murmur: adds hint about teeworlds server 2024-11-20 19:19:27 +01:00
eyjhb
7313bb8344
updated sources + patches + more patches 2024-11-05 18:31:01 +01:00
eyjhb
ce4d807d53
gerd.element: added latex + video rooms support 2024-11-05 17:12:40 +01:00
eyjhb
7e94a0e80c
rekeyed + added age backup key 2024-09-15 13:19:36 +02:00
eyjhb
41bb5bdb39
updated sources + updated teewords patch + removed stalwart patches 2024-09-09 23:22:35 +02:00
eyjhb
a93382acee
gerd.nextcloud: add gpoddersync as app 2024-09-09 21:30:17 +02:00
eyjhb
008c7ebfb9
gerd.lldap: added lldap-cli 2024-09-01 20:07:20 +02:00
eyjhb
29a638943f
gerd.murmur: increased bandwidth 2024-09-01 20:06:55 +02:00
eyjhb
ac813a2554
updated sources 2024-09-01 20:06:49 +02:00
eyjhb
f8c9599661
rekeyed secrets 2024-09-01 20:06:41 +02:00
eyjhb
6970d8c410
gerd.element: skip welcome page and added brand 2024-08-29 19:36:02 +02:00
70 changed files with 2372 additions and 179 deletions

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
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.

View file

@ -5,6 +5,7 @@ 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"

View file

@ -4,13 +4,14 @@
./../shared/applications/server/acme.nix
./../shared/applications/server/nginx.nix
./../shared/applications/server/postgresql.nix
./../shared/applications/server/postgresql.nix # INCLUDES DATABASE BACKUPS
./../shared/applications/server/restic.nix # EXTERNAL BACKUP
./../shared/applications/state/postgresql.nix
./../shared/applications/state/ssh.nix
./gerd/services/fricloud-website.nix
./gerd/services/member-website
./gerd/services/lldap.nix
./gerd/services/lldap
./gerd/services/authelia
./gerd/services/forgejo
./gerd/services/teeworlds.nix
@ -19,6 +20,9 @@
./gerd/services/cyberchef.nix
./gerd/services/nextcloud.nix
./gerd/services/stalwart
./gerd/services/wger
./gerd/services/searx.nix
./gerd/services/miniflux.nix
./gerd/services/element.nix
./gerd/services/matrix-synapse.nix
@ -37,6 +41,7 @@
"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/postgresql" = { mountpoint = "/srv/postgresql"; extra.options.quota = "5G"; };
"backup/postgresql" = { mountpoint = "/media/backup/postgresqlbackup"; extra.options.quota = "5G"; };
};
@ -55,5 +60,18 @@
};
};
# 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";
}

View file

@ -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 Remote-User $user;
proxy_set_header Remote-Groups $groups;
proxy_set_header Remote-Email $email;
proxy_set_header Remote-Name $name;
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;
## 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,15 +73,29 @@ 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://auth.fricloud.dk/?rd=$target_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} "";
'';
in {
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.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.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";
};
}

View file

@ -6,11 +6,17 @@ let
# configure element web client
pkg_element = pkgs.element-web.override {
conf = {
default_theme = "dark";
features.feature_latex_maths = true;
default_server_name = config.mine.shared.settings.domain;
embedded_pages.login_for_welcome = true;
disable_guests = true;
default_server_name = config.mine.shared.settings.domain;
brand = config.mine.shared.settings.brand;
default_theme = "dark";
features = {
feature_latex_maths = true;
feature_video_rooms = false;
};
};
};
in {

View file

@ -37,10 +37,6 @@ 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;

View file

@ -1,11 +1,26 @@
diff --git a/templates/user/auth/link_account.tmpl b/templates/user/auth/link_account.tmpl
index 8dd49ccd60..8cdce5e1ad 100644
index e8bb3d409c..aa6d18b97a 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">
<div class="column tw-flex tw-flex-col tw-gap-4 tw-max-w-2xl tw-m-auto">
- <div class="ui tab {{if not .user_exists}}active{{end}}"
+ <div class="ui tab"
data-tab="auth-link-signup-tab">

View file

@ -1,20 +1,20 @@
diff --git a/templates/user/auth/signin_inner.tmpl b/templates/user/auth/signin_inner.tmpl
index 9872096fbc..1076f90326 100644
index d4ba664e37..2c94eafc22 100644
--- a/templates/user/auth/signin_inner.tmpl
+++ b/templates/user/auth/signin_inner.tmpl
@@ -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">
@@ -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" .}}

View file

@ -0,0 +1,161 @@
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)

View file

@ -0,0 +1,79 @@
{
"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"
}
]
}

View file

@ -0,0 +1,136 @@
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
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():
if isinstance(v, str):
v = [v]
insertAttributes.append({"name": to_snakecase(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"})

View file

@ -0,0 +1,287 @@
#!/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 secrets
import json
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("lldapbootstrap").setLevel(logging.DEBUG)
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
class LLDAP:
def __init__(self, server_url: str, auth_token: str):
self._server_url: str = server_url
self._server_auth_token: str = auth_token
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 _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):
data = json.load(open("test2.json", "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__":
auth_token = os.getenv("LLDAP_TOKEN")
if not auth_token:
raise Exception("No LLDAP_TOKEN provided. please set")
x = LLDAP("https://ldap.fricloud.dk", auth_token)
x.run()

View file

@ -0,0 +1,154 @@
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
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():
if isinstance(v, str):
v = [v]
insertAttributes.append({"name": to_snakecase(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)

View file

@ -0,0 +1,10 @@
import re
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()

View file

@ -5,20 +5,26 @@ let
resetPasswordStartPatch = pkgs.writeText "lldap-reset-password-start.patch" ''
diff --git a/server/src/main.rs b/server/src/main.rs
index 71e4928..63be13c 100644
index 6f42473..b3746a1 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -158,7 +158,7 @@ async fn set_up_server(config: Configuration) -> Result<ServerBuilder> {
@@ -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 {
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,
'';
pkgLLDAPCli = pkgs.callPackage ./../../../../shared/pkgs/lldap-cli.nix {};
in {
environment.systemPackages = [
pkgLLDAPCli
];
services.lldap = {
enable = true;

View file

@ -56,7 +56,6 @@ in {
max_upload_size = max_upload_size;
# only authenticated media
# TODO: Should default to true at some point
enable_authenticated_media = true;
# retentien policies

View file

@ -8,6 +8,7 @@ import argparse
import logging
import json
import sys
import os
logging.basicConfig()
logger = logging.getLogger(__name__)
@ -94,10 +95,10 @@ def extract_secrets() -> dict[str, str]:
def index():
# extract user information
user_info = {
"username": request.headers.get("Remote-User"),
"name": request.headers.get("Remote-Name"),
"groups": request.headers.get("Remote-Groups"),
"email": request.headers.get("Remote-Email"),
"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")),
}
tmpl_firstpass = render_template_string(
tmpl_index,

View file

@ -1,4 +1,4 @@
{ config, pkgs, ... }:
{ config, lib, pkgs, ... }:
let
urlpath = "/members";
@ -9,6 +9,14 @@ 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 ]);
@ -18,10 +26,25 @@ in {
};
services.nginx.virtualHosts."${config.mine.shared.settings.domain}" = config.mine.shared.lib.authelia.mkProtectedWebsite {
endpoint = urlpath;
vhostConfig.locations."${urlpath}" = {
# extraConfig = "rewrite ^${urlpath}(.*)$ /$1 break;";
locations."${urlpath}" = config.mine.shared.lib.authelia.mkProtectedLocation {
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;
};
};
};
}

View file

@ -0,0 +1,57 @@
{ config, lib, pkgs, ... }:
let
svc_domain = "miniflux.${config.mine.shared.settings.domain}";
port = 6466;
in {
services.miniflux = {
enable = true;
config = {
# listen only on localhost
LISTEN_ADDR = "localhost:${builtins.toString port}";
# 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 = "http://localhost:${builtins.toString port}";
};
locations."/v1".proxyPass = "http://localhost:${builtins.toString port}";
};
# 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;
};
};
}

View file

@ -14,7 +14,11 @@ in {
environmentFile = config.age.secrets.murmur-env.path;
password = "$MURMUR_PASSWORD";
welcometext = "Welcome to Friclouds Mumble server!";
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;
};
# set superpassword on start from secrets

View file

@ -125,7 +125,7 @@ let
in {
services.nextcloud = {
enable = true;
package = pkgs.nextcloud29;
package = pkgs.nextcloud30;
datadir = stateDir;
config.adminpassFile = config.age.secrets.nextcloud-admin-pass.path;
@ -138,12 +138,12 @@ in {
# apps
extraAppsEnable = true;
extraApps = {
inherit (config.services.nextcloud.package.packages.apps) contacts calendar tasks;
inherit (config.services.nextcloud.package.packages.apps) contacts calendar tasks gpoddersync;
oidc_login = let
version = "3.1.1";
version = "3.2.0";
# TODO(eyJhb): add to niv
in pkgs.fetchNextcloudApp {
sha256 = "sha256-b/tKk+y+ZypCHGNDtunDua2msYD6/TzA0haoC0k85F4=";
sha256 = "sha256-DrbaKENMz2QJfbDKCMrNGEZYpUEvtcsiqw9WnveaPZA=";
url = "https://github.com/pulsejet/nextcloud-oidc-login/releases/download/v${version}/oidc_login.tar.gz";
license = "agpl3Only";
};

View file

@ -0,0 +1,48 @@
{ 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 = "searX";
description = "We host our own searX, 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;
};
};
}

View file

@ -26,12 +26,6 @@ in {
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;

View file

@ -11,10 +11,10 @@
password = "$TEEWORLDS_PASSWORD";
};
mine.shared.meta.teeworlds = {
mine.shared.meta.teeworlds = rec {
name = "Teeworlds";
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 = "";
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}";
secrets.auth = config.age.secrets.teeworlds-env.path;

View file

@ -0,0 +1,79 @@
{ config, ... }:
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@${svc_domain}>";
# use authelia for authentication (disable guest users + regisration)
AUTH_PROXY_HEADER = config.mine.shared.lib.authelia.protectedHeaders.username;
ALLOW_GUEST_USERS = false;
ALLOW_REGISTRATION = false;
};
# django specific settings
djangoSettings = rec {
# setup site stuff
SITE_URL = "https://${svc_domain}";
CSRF_TRUSTED_ORIGINS = [ "https://${svc_domain}" ];
ALLOWED_HOSTS = [ svc_domain ];
# 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 = "$EMAIL_HOST_PASSWORD";
EMAIL_FROM_ADDRESS = config.services.wger.wgerSettings.EMAIL_FROM;
EMAIL_PAGE_DOMAIN = SITE_URL;
};
};
# 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}";
};
# 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;
};
};
}

View file

@ -0,0 +1,138 @@
{
lib,
python3,
fetchFromGitHub,
callPackage,
writeText,
fetchpatch,
}:
let
frontend = callPackage ./frontend.nix {};
in python3.pkgs.buildPythonPackage rec {
pname = "wger";
version = "unstable-2024-12-30";
pyproject = true;
src = fetchFromGitHub {
owner = "wger-project";
repo = "wger";
rev = "30871d621fa6e732f07bd33d4112b99539974e5f";
hash = "sha256-WcycWbzKug8vUfNnUDhvgmj1kUCpT1P1YJBfdIC1H9g=";
};
build-system = [
python3.pkgs.hatchling
];
patches = [
./patches/pyproject.patch
./patches/manage.patch
./patches/exercises-no-gifs.patch
# adds support for proxy auth header
(fetchpatch {
url = "https://github.com/wger-project/wger/pull/1859/commits/d46d469fa802890d7162b07c098802810fc8417c.patch";
sha256 = "sha256-D+3FmiSokJe9iSJz7ZbRzS+kuP3yV64XhKnQ4Oh5x8c=";
})
];
# dependencies = with python3.pkgs; [
propagatedBuildInputs = with python3.pkgs; [
bleach
celery
django-crispy-bootstrap5
django
# django-activity-stream
(python3.pkgs.callPackage ./django-activity-stream.nix {})
django-axes
django-compressor
django-cors-headers
django-crispy-forms
# django-email-verification
(python3.pkgs.callPackage ./django-email-verification.nix {})
django-environ
django-filter
django-formtools
django-prometheus
# django-recaptcha
(python3.pkgs.callPackage ./django-recaptcha.nix {})
django-simple-history
# django-sortedm2m
(python3.pkgs.callPackage ./django-sortedm2m.nix {})
django-storages
djangorestframework
djangorestframework-simplejwt
drf-spectacular
easy-thumbnails
flower
fontawesomefree
icalendar
invoke
# openfoodfacts
(python3.pkgs.callPackage ./openfoodfacts.nix {})
pillow
reportlab
requests
tqdm
tzdata
# extra??
redis
django-redis
drf-spectacular-sidecar
(python3.pkgs.callPackage ./django-bootstrap-breadcrumbs.nix {})
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/${python3.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 python3 ../manage.py collectstatic --no-input
static=. WGER_SETTINGS=../tmp_settings.py python3 ../manage.py compress --force
popd
'';
postInstall = ''
rm -rf $out/${python3.sitePackages}/wger/core/static
cp -a tmpstatic $out/${python3.sitePackages}/wger/core/static
mkdir $out/share
cp -a $out/${python3.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";
};
}

View file

@ -0,0 +1,43 @@
{
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; [ ];
};
}

View file

@ -0,0 +1,47 @@
{
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; [ ];
};
}

View file

@ -0,0 +1,63 @@
{
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; [ ];
};
}

View file

@ -0,0 +1,46 @@
{
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; [ ];
};
}

View file

@ -0,0 +1,49 @@
{
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; [ ];
};
}

View file

@ -0,0 +1,63 @@
{
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";
};
}

View file

@ -0,0 +1,296 @@
{ 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.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:]]
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:]]
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
(pkgs.python3Packages.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 --reload --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;
};
};
};
}

View file

@ -0,0 +1,55 @@
{
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; [ ];
};
}

View file

@ -0,0 +1,22 @@
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 ''

View file

@ -0,0 +1,32 @@
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)

View file

@ -0,0 +1,36 @@
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()

View file

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

View file

@ -1,9 +1,13 @@
age-encryption.org/v1
-> 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œá>
-> 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

View file

@ -1,10 +1,12 @@
age-encryption.org/v1
-> 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·¨$
æ³µ
-> 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.¿ð±
„û§©–&.5—ÞÆxZ_!¶Ùð\ȸÒÈ=|è—ÍHtòìŠ KDëmúD&Ý«=

View file

@ -1,9 +1,11 @@
age-encryption.org/v1
-> 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:
-> 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

View file

@ -1,10 +1,11 @@
age-encryption.org/v1
-> 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öÏá&¶
4<>¡¹ÿéì¥UÉz )ºº2«  Ê’ «¤>íº8SßozRÈÁ@·Âè(UÒ´rܹË$åUVóÆßäw
-> 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ŽØà$õëÈÉ­

View file

@ -38,6 +38,16 @@
# matrix-synapse
matrix-synapse-config-authelia-secret.file = ./matrix-synapse/config-authelia-secret.age;
# wger
wger-env.file = ./wger/env.age;
# restic
restic-env.file = ./restic/env.age;
restic-pass.file = ./restic/pass.age;
# searx
searx-env.file = ./searx/env.age;
};
users.groups.secrets-lldap-bind-user-pass = {};

View file

@ -1,10 +1,12 @@
age-encryption.org/v1
-> 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
-> 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"š›Š!óºoL<E28093>ª‰

View file

@ -1,10 +1,11 @@
age-encryption.org/v1
-> ssh-ed25519 QSDXqg GGeJqXdtwwxIlkG/yl4DfkKykQ3uJyWqLguJ680vZlY
LS19/W+IHFSAeog3c2qAzvgE2VDWF81B5ehqo2xoCVk
-> ssh-ed25519 n8n9DQ 8xOzOWPQEwAAslYAg71Hf8sf67+QGFKeX280ueXrYVk
ZdzT710/gB1N7eosXQbyRdyzQvQDuLeCFS6ocpkvooU
-> ssh-ed25519 BTp6UA RyRdwb7gHk74LgqEmWUJ8SpiS94IHczpO2ZokCFO0QY
c3t3vZyRqSIWiFnt0slV8AjACKW44PgUvwijLTNigck
--- emrYR6UhtLGsqpz7q+KAivD5e0sAf6zaA5qh3vD/13A
Ùüù×H@ø­|² >á3­C‰Œ&*µ_8
~ç7RÛ†)°$<24>çü]éD©y±±}ß.Ê:
-> 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)æ>«‰,ðý

View file

@ -1,10 +1,12 @@
age-encryption.org/v1
-> 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+ªš™ü¯"R™¨A#_£Ø.
-> 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[§Á

Binary file not shown.

Binary file not shown.

View file

@ -1,9 +1,11 @@
age-encryption.org/v1
-> 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・ト「>
-> 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ô

Binary file not shown.

Binary file not shown.

11
secrets/restic/env.age Normal file
View file

@ -0,0 +1,11 @@
age-encryption.org/v1
-> ssh-ed25519 QSDXqg PybnzljzRzswiQPSo1I10lSPRjXHd8rVFSNDH1ZsUig
RzLFaSgJWuDDBS+eTmz0J2aVWjTWV50laojbkyzp4fM
-> X25519 ZCS4baMlt3oGpkHjdeQibFt4oxum00sHV55sW5yW+3I
oT/YlQ4sAYkOC4V6+PfK+CYgDT2l/fOlQJ+sVaBVYV4
-> ssh-ed25519 n8n9DQ anOLNIDopvdtK7A8BH/bzcz3plEzULJW73BvGS9aSmQ
YPzmwoT/Ltnu5GvicbCs9qqN5CjlsoHClN3seAQdRSo
-> ssh-ed25519 BTp6UA 8H6CnD8TJUP5acPMs/9Yvnc9cu2kx1blrK/oDlts4Fg
O6JIlYDxQB10liQ8tqIqi/Gya3k0v/pcIKbI0VBUyn0
--- PjFfEkfF3yWY4QolKjwCv6Mj30AcoL4cE0qKlgaUV5o
h8°^&C¹“ôÏ<C3B4>=“&âö«éÒò#Þo…Y+{j<>ÒÞ]Œ“Vïî<C3AF>ÝEîQ <09>[M±«òb»wsྼe¨£lГ0}3Ò´ÚCæ5HK<uïÄò{Ãa´ô,KoD[“ 탗_ך¨

12
secrets/restic/pass.age Normal file
View file

@ -0,0 +1,12 @@
age-encryption.org/v1
-> ssh-ed25519 QSDXqg +vE6VbbU5XgX0XkEWh9crm+5mdtURyQAqVffU7EXfFg
UAx9/0QBx+liLIHc2S6Z/JZmtIcBuzxYOlM9YMC4CfI
-> X25519 KcSKOsQyykcTUtcJYhkU+s6b9xzEQp5nxxdC2lmd8xU
oZUIGnUXg5bYCCqeNSHs2cXF4LnxGIYC0HyapGoaF0k
-> ssh-ed25519 n8n9DQ E47ziDXHHPcsQtaHPT17XkgoCcvCQcyFIluEycWfQWw
MHpLKSfAUAuVoCxcrpH87dJKnq0qK0Nvek9QIpdLPpE
-> ssh-ed25519 BTp6UA JlHAZaDZkZoL8jHepRFB6CpfmgNPD/gNeXBMXzQuVmY
XadtJ2aBU5f6mxAb7iCvBRvTr8skt+1OMIqJ2DOr8JI
--- m/WIZdO5VuSKn3rj6f0ZY5+P8dejPOf1N8niALApC08
}L<>vÆ"Ë{ã÷%¤ð²ñwáÏÌÑFk7ó¯0™Ð.ŒËÀÁ“b2VCêí{…ò‰åîêœ')ˆowóD/þÅ` ö¨*ìñr Ã
…ÌdDß±À

11
secrets/searx/env.age Normal file
View file

@ -0,0 +1,11 @@
age-encryption.org/v1
-> ssh-ed25519 QSDXqg fNc/NTcJ2bRYE0SIvKFIJG8mo9RrAvSxAB97RC+Gywg
10OeGwUb1GaxvS6gOMh4yIJsTAq1Q7QoLasHp0OSzEo
-> X25519 YSMcNwnLlqTImvRejhbowWVxTTo1bkhUL6BrQbXq7ns
LLIOfY+u/qT/PVBIniGh+WztTwmHGJuY8cgWuqGEkhg
-> ssh-ed25519 n8n9DQ Up+IltuGUo5c9MOGjrNV8tZH/CicwxKmRAJrlNL+bWw
P3d/iYWJNDl5FedbSXUrtVtgZ8YJTx0BHToIzZqMnxM
-> ssh-ed25519 BTp6UA 5fO9KgLtLJ2DEY4YuW5Ybt/BtziL1JmRUwJ0xYyMVhE
uOqXkfhkjG9ocjEzboWKe39+18q+Dr1WRIppQA4B/h4
--- rjsMU+9R21buoWlPfLWXxcBk5mHsSZ0H9uLgZTyS/9s
@~øWÅOì4@U.ÎcRÄšªbýÎ:øD¥<>ô΀ÕÅ)]XÑ—¤HßP]^þ­Ö<C2AD>úFœ4º=%8K@biçѼىžÝqÔfö ”j”1OZ_n¦

View file

@ -1,7 +1,13 @@
let
user_eyjhb = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPuma8g+U8Wh+4mLvZoV9V+ngPqxjuIG4zhsbaTeXq65 eyjhb@chronos";
user_rendal = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGee4uz+HDOj4Y4ANOhWJhoc4mMLP1gz6rpKoMueQF2J rendal@popper";
users = [ user_eyjhb user_rendal ];
user_eyjhb = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPuma8g+U8Wh+4mLvZoV9V+ngPqxjuIG4zhsbaTeXq65 eyjhb@chronos"
# BREAK IN CASE OF EMERGENCY (secret key for age below) hQIMA5HKM7J/GmnvARAAjjoDyDr3mSfrM+jmrIaXj40U/IaDJzNyFI0pnqurjR9MlmSmm0gMm/nbqZWDt3HKP6YsbMb+YvDGA2kNf7SJ7EWK/dldmXkfWojlJvGUwDtNam3rqoyTdyoscG7E43PukTC8us5LY4izLJxESKy0b6hUAY6vocD4zRKqY+EaKjgIa1AUpwOD/qMIosh1m2irUV9+li0RsxCPUt/e59Tdo/mj7gGZ7iXnxwLFM59d+rKTuh3r4cm2VDgm1zF0wnusCtkMNtJEnOE0Nu6TOK7YQ0EX7AHy3Wr2cItAskFzcZY2IFxaO3TXEYj08dvtIgCuyhtxxFGihw2HzTZMlsByT2lbVpPDthgNAAnMGtqKRwkPzem/S6vITwlP2J1/iIu0olbs1lDHixym2kXua46q93M3CFP6ZKPo6o0C12Tx9Cj/nHv9gfd2TsFPucqlwvZsWzT/WVRpzYRBgWXMB1/z62fK4JuGPOtppNbcVzUH86HDcZ9CiYh6NTGJmP8OWg832GT3L1VrcY3e63FOUEgusyBsUeHE54TRfg6MPxUPmkVuVB6+r/ika9HdM8d4/jve/Yu0OUmQFpSfBodGmxtE8Wmqd9heQ6n4CPopR5U2aFayC3ES/5/82qHZDlFvB7U5HPlH8gn0AJR+KmP7qnsqz+NI/f4/yIYXC52wIkeFHxDSwDkBudK/7gJFsmfEBNzzN+F9LwdA69CwDVDV9zCSrzsEHsAkTxJ/unkDvij3Dv4MqTFKWvqC+smlsR1/i/p7+tB1PZRUe9B9spBP76fygnpF3HqRmXkT2C+qEJ5nlGr3pK46oXP8d85nyTO3e5X97mM2hxmhKzpiX+aFjHN9LYLUQAmzHTPz0Lktnj7STf1ve8LZCvppvQzAuqTcbpaer9Qx4moNWr7NtJuPM0vT+adc6AbrY6bgdewisSP1QD34tVvNcUVLdICUSoTUI0eEfmQop5ba0ayFDsotTFZXNtl3Qmb/OpUit7f88wL98EjbK+904dtAOUs/9FE=
"age1xnll9l56j0clh9e9r7ha9sy7sjdcxnhtaxljz2p96ectktq33vgsvteua6"
];
user_rendal = [ "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGee4uz+HDOj4Y4ANOhWJhoc4mMLP1gz6rpKoMueQF2J rendal@popper" ];
users = user_eyjhb ++ user_rendal;
system_gerd = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJosDVq8j4V50/z6nj2OMBPhqda95HOS1hKLGvo8viLQ";
@ -42,4 +48,14 @@ in
# matrix-synapse
"matrix-synapse/config-authelia-secret.age".publicKeys = defaultAccess;
# wger
"wger/env.age".publicKeys = defaultAccess;
# restic
"restic/env.age".publicKeys = defaultAccess;
"restic/pass.age".publicKeys = defaultAccess;
# searx
"searx/env.age".publicKeys = defaultAccess;
}

View file

@ -1,10 +1,11 @@
age-encryption.org/v1
-> ssh-ed25519 QSDXqg 9NdZ2auQY3xgJyg6bk9IXc54E7s0iBIEaqTJPze6aVo
sO+XRHIh8+BzH21AuIdSN8V9eZK8bL7synmip7OF+e8
-> ssh-ed25519 n8n9DQ RT5uVILvBUdHPxCcJ1pYgiiCGTgIpn1UrAYnrpgy3RM
HfHBx5QVn8ahHk1NjxZWsfrD2W2D0E25mQrZGkiX6WQ
-> ssh-ed25519 BTp6UA AY1xVYOt+wTYPBSweXv7QvpJOsEawEBufpI6nR0+0Ds
OxwPG/E5EatvwUgvhqfW8dVG/r4fjbf0Nfawz/BBgD8
--- 6tjacKndq1+4t3+EEnl+Sr2qDIlrhvE4WqtxPXZTUVM
ô×°ëÝà8L󡈸zÆW™¼n—Ö”
voQOA¼S˜io×ÑO •h\î÷Ï<C3B7>É/hF18¿ #bƒ
-> ssh-ed25519 QSDXqg pfgiaD4SEf6zNTAkR2rEvIxbzj9ShEZRQzr4Jh5v0Ew
22KV1flM3fnMmrLultlW0gfnh7bH6AxFe+4R0ieEyrU
-> X25519 g/uqAKvSMq8DEmobebJQd6zul8tQtajy9RgBc9AC2UA
K5ICYY30PVRFxNYyKT0PTK7I3ALVNVydCeFYOMBGax0
-> ssh-ed25519 n8n9DQ HtNO+ClXwDyAG//RuTrZZIidcFqoq+M/Xt0jBHehnhA
umeo/xqseBvfKNy7qe8Wrkr4xCwymiW+zlMgxYvHGo0
-> ssh-ed25519 BTp6UA Xs2dPjjLaIBo6cWs/oFNiBtwWmc0WVm71LJkOSCBrV4
w3/cp0Z8ODoBTNgWl8E/z2xge1vdybpmAsc5poLrzdQ
--- 1Mem//qXpGotxgPb4N2hwxrNcKICgggVEKd/ghqZOaQ
[Ütºˆ6ˆm´àÙL…=þØ/ÎJ{Ã/ÕxEý¢QÇÃÞx¼¡gûF¶¡†ruÒ9¢<39>ñù|øzŠ„ùüúS´

View file

@ -1,10 +1,11 @@
age-encryption.org/v1
-> ssh-ed25519 QSDXqg ntHsb1DKwiuswQq/BKhTw0mmvlnvanpPhjdY3lD4NWo
uXVQHnjMlPBFPgbAB9oLARUMZ78EW15Kv7YgKXeL/YU
-> ssh-ed25519 n8n9DQ BSnWJrmod7N8R0HxhDE+M/pycuTXzE2WsYzWAi846Ww
G4c3sZ2holV5VBpPIibkfXXSW638uv26Cow2LFdEkLw
-> ssh-ed25519 BTp6UA 0mA1o+wWla0IFOqaZNVNdgzOqc1fawUde9dqEUi4tws
OGhqQu4ogpRo5jHZsYCyNb4VaQOfeKdOJpI8r8LcVVs
--- /RpUgDJ08NIK80JCW+UhEODKdXBpkPchxetQuTpcH94
âl©ÎY |-LX á@Gû[[ˆÆˆÈ;œO`ó.c‡ÚE#µJÅM]ÇzïK^ÎéJ…Ä{ÿh§½RAPvNî0¿¤³Gÿ'
èu#¹Ú_é
-> ssh-ed25519 QSDXqg zXCmzKXdRLnwMmIle54sOj4XVS5CI4FsNnA1EgorbzI
A5oJSf6jXfLE+5C2hAX6f6Q0Em5CuWnkZQ/E3ikynNA
-> X25519 NV19ktGN64nxgNQcnedYgL+WMSRhaTx5NIiJUaKCWkE
6iVlgBJxO3UzJYuJJ2riqFz6ziXzvaaKCsnopdvv/QM
-> ssh-ed25519 n8n9DQ 8hxpbyHwgoml0AwjuidIwKBLSLBpSsjkNpGe0Yl/ITI
oNddyuP5XFeWIG614RH2p5MGQPCUbKY+Be9EPSIOB/Y
-> ssh-ed25519 BTp6UA Vh2qtUDXAyLx2F4Pa/rQGOjR99qt0Tf/acshYncR0Fs
nxhW5+EiJ9nCimkyEZwvItjMu5b4YFcqgd9n258ruLU
--- nc8xoR3Obt7JEmk3XNdBtzl0woCpjuH7FRcXGS4Dr1A
TJyzb£Éc¡J¸)S Rcœ+$ âóÁ°û™Ú Iâ>fY<66>¶2ž s$<24>§N"Ó2ƒO¾^Ndø½ƒéj“Ä4±~™º>KNŠ|£5é}Œç

11
secrets/wger/env.age Normal file
View file

@ -0,0 +1,11 @@
age-encryption.org/v1
-> ssh-ed25519 QSDXqg KGoB/V0cCAZsfVmoLDmA5Xs2HOHqjg54TYqixYQduEw
sqDb6QnEbwEncAbxKLRLkjCQIwMLBTNMVcejFOwhZWM
-> X25519 o64XZRaiK7ZEquTMmXTyhpdArawiuXC+5W5seFrJclY
qTLXrNGMTPAXs5EzMuCiQ07Ho2LT1KTku2f1AlCHPlk
-> ssh-ed25519 n8n9DQ a8ESfbksuY++k52UJwTKJtb4/aiYzQqUgyYqfug5oyA
bZygFOW6YSg83CmZRpsNDux+UgOxCfja1eQ/R4NyLXM
-> ssh-ed25519 BTp6UA yFBZAlGtHV98t6UA8QbELjOW/Pu6KYVPjbXFvijl9m0
+eobFp5YNBsr2+10Huimwypn3S4/lc7zoX5Ldko9mhA
--- g7w825LgydJlmyZiqnIL0ofUsTn+e47rFmSG8ft6Qqg
!lï•:^çÄÙƒ}R&Xº^_ã213·-éŒË£0Ån<C3BE>DK€­æ&Ù©Dþ:¾^½ÒUwÃÌóŸ 8(£‡ä X‡¾QZsÖªŒ<C2AA>â^(CÂ!ÍìÊ$ ™Üöý×(wÎ8t“ô¾<C3B4>Ñ!Úç²±Ð̈ït;¥ÃNgÚÛ§ˆ<C2A7>Ž[²f+Ù

View file

@ -22,6 +22,11 @@ in {
# only allow PFS-enabled ciphers with AES256
sslCiphers = "AES256+EECDH:AES256+EDH:!aNULL";
# disable access logs
commonHttpConfig= ''
access_log off;
'';
# setup a default site
virtualHosts.default = {
default = lib.mkDefault true;

View file

@ -11,7 +11,7 @@
compression = "zstd";
# default to backup all databadatabases
# default to backup all databases
databases = config.services.postgresql.ensureDatabases;
};

View file

@ -0,0 +1,30 @@
{ config, lib, ... }:
{
services.restic = {
# enable = true;
backups.main = {
repository = "b2:situla-${config.mine.shared.settings.brand_lower}:.";
passwordFile = config.age.secrets."restic-pass".path;
environmentFile = config.age.secrets."restic-env".path;
# take all `.*/safe/.*` and `.*/backup/.*` zfs volumes
paths = let
backupPaths = lib.filterAttrs (n: _:
(lib.hasInfix "/safe/" n) || (lib.hasInfix "/backup/" n)
) config.mine.zfsMounts;
in lib.attrValues backupPaths;
initialize = true;
runCheck = true;
pruneOpts = [
"--keep-last 7"
"--keep-weekly 4"
"--keep-monthly 6"
"--keep-yearly 2"
];
};
};
}

View file

@ -6,7 +6,13 @@
settings.PasswordAuthentication = false;
};
journald.extraConfig = "SystemMaxUse=100M";
journald = {
storage = "volatile";
extraConfig = ''
SystemMaxUse=100M
MaxRetentionSec=1d
'';
};
};
nix = {
@ -46,6 +52,7 @@
};
users.users.root.openssh.authorizedKeys.keys = [
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCpGGdH8BpCM9pUSANM9vYA4b/V2zGJK+GGpi8N/Qp+j32TRD6UsA0g42o4tL72Hsv3PKUU5vaZaXjeSSZYpwaNCe1aR7ehBesEvcgeiU66jBQ6JfMoArF+ZreveXQvtYeqcN6Iyijcu7vyqWIcybT5yOEiylQhB2bUd5lVR9KDAW3z6zhiVPxGmC8D09uZVxsGPfAPxyKvRs6Jkq0d67nDI9yUOtRJEdMvrDDhGzHQhKRuxl+NHtYCOa9octFZMcpEssmUOS97KNgBhglSZlz4a5PKUO7NmLZEgrCjw/aAKyepRenB3a7R/20lJvsN4YsIAR/rVH6bdrYhWKOjUrXm3PFPBs7CxdMP9qs4LEM1AMJ0dTw40AE94HfvilEV3HV+WSjen1dcHJNrSQiOAfXZPVjkkmnrum6p3R1gPcezhrGuWZv/RDgJIflo6Kd3heCe9gk1tV/lYswm5l9Cpg5gIUiMd01UfXI4FvxFQcE4AIBs8UHOhorIbjDbNTeZoBxXZFWMRUTVNR37hZRBnp/Ept0WOsIhlqi0V/oGRAilVy2a0Xs9dwX785W8Q9g5weT+fUR71huTjEEQnz7/VGcOPE64mD3yh7rmxYi6wMjoG6/NxzRBs4KRux5q+MAHxl0jDCgV+0fx78xtlH9Zb3/d5cgZ4TwPeIElS4g1b5FFBQ== eyjhb@key"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPuma8g+U8Wh+4mLvZoV9V+ngPqxjuIG4zhsbaTeXq65 eyjhb@chronos"
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGee4uz+HDOj4Y4ANOhWJhoc4mMLP1gz6rpKoMueQF2J rendal@popper"
];

View file

@ -18,4 +18,7 @@ in {
mine.shared.settings.domain_sld = "fricloud";
# mine.shared.settings.domain = "${config.mine.shared.settings.domain_sld}.${config.mine.shared.settings.domain_tld}";
mine.shared.settings.domain = "fricloud.dk";
mine.shared.settings.brand_lower = "fricloud";
mine.shared.settings.brand = "Fricloud";
}

42
shared/pkgs/lldap-cli.nix Normal file
View file

@ -0,0 +1,42 @@
{
stdenv
, lib
, fetchFromGitHub
, makeWrapper
, bash
, gnugrep
, gnused
, jq
, curl
}:
stdenv.mkDerivation rec {
pname = "lldap-cli";
version = "unstable-2024-08-31";
src = fetchFromGitHub {
owner = "Zepmann";
repo = "lldap-cli";
rev = "6eb61cef179696633cafe080a018cd085d3c3f64";
sha256 = "sha256-Jchj4vqlGWmjFtdMwZAnI4VyBh+/p6rgZrpA77xlSb4=";
};
buildInputs = [
bash
gnugrep
gnused
jq
curl
];
nativeBuildInputs = [
makeWrapper
];
installPhase = ''
mkdir -p $out/bin
cp lldap-cli $out/bin/lldap-cli
wrapProgram $out/bin/lldap-cli \
--prefix PATH : ${lib.makeBinPath buildInputs}
'';
}

View file

@ -57,8 +57,8 @@ in {
];
routes = [
{routeConfig = {Destination = "172.31.1.1";};}
{routeConfig = {Destination = "fe80::1";};}
{ Destination = "172.31.1.1"; }
{ Destination = "fe80::1"; }
];
};

View file

@ -8,16 +8,16 @@ in sources // {
src = sources.nixpkgs;
name = "nixpkgs-patched";
patches = [
# tmp teeworlds fetchpatch to inject secrets
# tmp - lldap: 0.5.1-unstable-2024-10-30 -> 0.6.1
(pkgs.fetchpatch {
url = "https://github.com/NixOS/nixpkgs/pull/334590.patch";
sha256 = "sha256-5Uf/jLV0CJFbWyPmkpF4kEVISvoG+fujvTAFIR0a2ek=";
})
# stalwart-mail
(pkgs.fetchpatch {
url = "https://github.com/NixOS/nixpkgs/pull/333507.patch";
sha256 = "sha256-HAbfKQRnOjdK/rJ5wuePw4hEVQoFz9N0YujxBxROGo0=";
url = "https://github.com/NixOS/nixpkgs/pull/359835.patch";
sha256 = "sha256-2C9l4v9MaUJyiaB+kslTsSjsqTZ7RlcfMNlRzZblMik=";
})
# tmp - stalwart-mail.webadmin: pin wasm-bindgen-cli version
# (pkgs.fetchpatch {
# url = "https://github.com/NixOS/nixpkgs/pull/353360.patch";
# sha256 = "sha256-WPNnvVmtySyEk58kVIYWVx3VN8MhX4v2ITLLnUGhpz4=";
# })
];
};
}

View file

@ -17,10 +17,10 @@
"homepage": "",
"owner": "nix-community",
"repo": "disko",
"rev": "276a0d055a720691912c6a34abb724e395c8e38a",
"sha256": "1bdf6p7p4rdzkchg67w5vhfg23qs3381cn4x7v16qnv6hqid0i8s",
"rev": "2814a5224a47ca19e858e027f7e8bff74a8ea9f1",
"sha256": "1ayxw37arc92frzq0080w7kixdmqbq4jm8a19nrgivb70ra1mqys",
"type": "tarball",
"url": "https://github.com/nix-community/disko/archive/276a0d055a720691912c6a34abb724e395c8e38a.tar.gz",
"url": "https://github.com/nix-community/disko/archive/2814a5224a47ca19e858e027f7e8bff74a8ea9f1.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"impermanence": {
@ -29,28 +29,22 @@
"homepage": "",
"owner": "nix-community",
"repo": "impermanence",
"rev": "23c1f06316b67cb5dabdfe2973da3785cfe9c34a",
"sha256": "1c99hc2mv0f5rjxj97wcypyrpi5i3xmpi3sd2fnw2481jxgqn5h3",
"rev": "3ed3f0eaae9fcc0a8331e77e9319c8a4abd8a71a",
"sha256": "1k30ig9b5bx51f0y617yvcn61bgpahf8r0i55mnl3hy6nqjbfw07",
"type": "tarball",
"url": "https://github.com/nix-community/impermanence/archive/23c1f06316b67cb5dabdfe2973da3785cfe9c34a.tar.gz",
"url": "https://github.com/nix-community/impermanence/archive/3ed3f0eaae9fcc0a8331e77e9319c8a4abd8a71a.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"nixos-mailserver": {
"branch": "nixos-24.05",
"repo": "git@gitlab.com:simple-nixos-mailserver/nixos-mailserver.git",
"rev": "29916981e7b3b5782dc5085ad18490113f8ff63b",
"type": "git"
},
"nixpkgs": {
"branch": "nixos-unstable",
"description": "Nix Packages collection",
"homepage": null,
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "c3aa7b8938b17aebd2deecf7be0636000d62a2b9",
"sha256": "1ds3yjcy52l8d3rkxr3b7h9c0c3nly079bgakjaasnfjj3xprrwr",
"rev": "ac35b104800bff9028425fec3b6e8a41de2bbfff",
"sha256": "1fbj7shlmviilmgz5z2gp59j6xwgdr01jfh75qhixx06kib4305p",
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/c3aa7b8938b17aebd2deecf7be0636000d62a2b9.tar.gz",
"url": "https://github.com/NixOS/nixpkgs/archive/ac35b104800bff9028425fec3b6e8a41de2bbfff.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
}
}