Add Linkki broadcast image
Build custom container images / build (map[base_image:php:8-fpm-alpine build_args:PHP_VERSION=8 context:php8-pgsql fingerprint_command:{ apk info -v | LC_ALL=C sort; find /usr/local/lib/php/extensions /usr/local/etc/php/conf.d -type f -exec sha256sum {} + | LC_ALL=C sort; } name:ph… (push) Successful in 48s
Build custom container images / build (map[base_image:postgres:18 build_args:PG_VERSION=18 POSTGIS_VERSION=3 VCHORD_VERSION=0.5.3 context:postgres fingerprint_command:{ dpkg-query -W -f='${binary:Package}=${Version}\n' | LC_ALL=C sort; find /usr/lib/postgresql -type f -exec sha256su… (push) Successful in 55s
Build custom container images / build (map[base_image:python:3 build_args:PYTHON_VERSION=3 context:python-tools fingerprint_command:{ dpkg-query -W -f='${binary:Package}=${Version}\n' | LC_ALL=C sort; pip freeze | LC_ALL=C sort; } name:python-tools oci_labels:org.opencontainers.ima… (push) Successful in 54s
Build custom container images / build (map[base_image:python:3.12-slim build_args:PYTHON_VERSION=3.12-slim context:linkki-tiedotus fingerprint_command:{ dpkg-query -W -f='${binary:Package}=${Version}\n' | LC_ALL=C sort; pip freeze | LC_ALL=C sort; } name:linkki-tiedotus oci_labels:… (push) Successful in 39s

This commit is contained in:
ajp_anton
2026-08-19 20:51:26 +00:00
parent 7e6dea8b24
commit 8ec3cde4f2
56 changed files with 13441 additions and 0 deletions
+169
View File
@@ -0,0 +1,169 @@
import base64
import getpass
import hashlib
import hmac
import json
import os
from pathlib import Path
from webapp.config import ConfigFileError
PBKDF2_ALGORITHM = "sha256"
PBKDF2_ITERATIONS = 600000
SALT_BYTES = 16
SUPPORTED_TRANSLATION_LANGUAGES = ["en", "fi", "sv"]
SUPPORTED_UI_LANGUAGES = ["auto", "fi", "sv", "en"]
PERMISSION_KEYS = [
"edit_content",
"edit_targets",
"save_announcements",
"open_telegram",
"post_now",
"schedule",
"unschedule",
"unlink_telegram",
"delete_telegram",
"delete_storage",
]
def load_users(users_file: Path) -> dict[str, dict]:
if not users_file.exists():
return {}
try:
payload = json.loads(users_file.read_text(encoding="utf-8"))
except json.JSONDecodeError as error:
raise ConfigFileError(users_file, f"Invalid JSON: {error.msg}") from error
users = payload.get("users", [])
result: dict[str, dict] = {}
changed = False
for user in users:
username = str(user.get("username", "")).strip()
password_hash = str(user.get("password_hash", "")).strip()
if username and password_hash:
permissions_payload, permissions_changed = normalize_permissions_payload(user.get("permissions", {}))
if permissions_changed:
user["permissions"] = permissions_payload
changed = True
language = normalize_ui_language(user.get("language", "en"))
if user.get("language") != language:
user["language"] = language
changed = True
result[username] = {
"password_hash": password_hash,
"permissions": load_permissions(permissions_payload),
"language": language,
}
if changed:
users_file.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
return result
def load_permissions(payload: object) -> dict[str, bool]:
source = payload if isinstance(payload, dict) else {}
permissions: dict[str, bool] = {}
for key in PERMISSION_KEYS:
permissions[key] = as_permission_bool(source.get(key, False))
return permissions
def normalize_permissions_payload(payload: object) -> tuple[dict[str, object], bool]:
source = dict(payload) if isinstance(payload, dict) else {}
changed = not isinstance(payload, dict)
normalized = {key: source[key] for key in source if key in PERMISSION_KEYS}
if len(normalized) != len(source):
changed = True
for key in PERMISSION_KEYS:
normalized_value = as_permission_bool(source.get(key, False))
if source.get(key) != normalized_value:
changed = True
normalized[key] = normalized_value
return normalized, changed
def as_permission_bool(value: object) -> bool:
if isinstance(value, bool):
return value
if isinstance(value, int):
return value != 0
if isinstance(value, str):
lowered = value.strip().lower()
return lowered in {"1", "true", "yes", "on"}
return False
def normalize_ui_language(value: object) -> str:
if isinstance(value, str):
lowered = value.strip().lower()
if lowered in SUPPORTED_UI_LANGUAGES:
return lowered
return "en"
def normalize_translation_language(value: object) -> str:
if isinstance(value, str):
lowered = value.strip().lower()
if lowered in SUPPORTED_TRANSLATION_LANGUAGES:
return lowered
return "en"
def set_user_language(users_file: Path, username: str, language: str) -> None:
normalized = normalize_ui_language(language)
try:
payload = json.loads(users_file.read_text(encoding="utf-8"))
except json.JSONDecodeError as error:
raise ConfigFileError(users_file, f"Invalid JSON: {error.msg}") from error
users = payload.get("users", [])
updated = False
for user in users:
if str(user.get("username", "")).strip() == username:
user["language"] = normalized
updated = True
break
if not updated:
raise ValueError(f"Unknown user: {username}")
users_file.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
def hash_password(password: str) -> str:
salt = os.urandom(SALT_BYTES)
digest = hashlib.pbkdf2_hmac(PBKDF2_ALGORITHM, password.encode("utf-8"), salt, PBKDF2_ITERATIONS)
return "$".join(
[
"pbkdf2_sha256",
str(PBKDF2_ITERATIONS),
base64.urlsafe_b64encode(salt).decode("ascii"),
base64.urlsafe_b64encode(digest).decode("ascii"),
]
)
def verify_password(password: str, password_hash: str) -> bool:
algorithm, iterations_text, salt_text, digest_text = password_hash.split("$", 3)
if algorithm != "pbkdf2_sha256":
return False
iterations = int(iterations_text)
salt = base64.urlsafe_b64decode(salt_text.encode("ascii"))
expected_digest = base64.urlsafe_b64decode(digest_text.encode("ascii"))
actual_digest = hashlib.pbkdf2_hmac(PBKDF2_ALGORITHM, password.encode("utf-8"), salt, iterations)
return hmac.compare_digest(actual_digest, expected_digest)
def hash_password_cli() -> None:
password = getpass.getpass("Password: ")
password_repeat = getpass.getpass("Repeat password: ")
if password != password_repeat:
raise ValueError("Passwords did not match.")
print(hash_password(password))
if __name__ == "__main__":
hash_password_cli()