Refactorisation - Hook useApi centralisé (hooks/useApi.js) — supprime 200 lignes dupliquées - Helpers backend : get_or_404, _sync_domain_from_state, _sync_zone_from_state, _stable_id - Constantes de rôles RBAC dans auth.py (ROLE_GLOBAL_ADMIN, etc.) - handle_apply_domain découpé en _sync_domain_row / _sync_mailboxes / _sync_aliases (169→32 lignes) - oidc_callback découpé en _exchange_oidc_token / _provision_oidc_user (159→54 lignes) - Extraction composants : AccountModal, AliasModal, DnssecResultModal, DnssecManageModal - Suppression MailingLists.jsx (inutilisé) Corrections - Notification de connexion différée après validation TOTP - Création domaine : validation complète avant tout appel API (quota bal > quota global bloqué) - Création domaine : service Mail non tenté si création DNS échoue - Alias goto : validation email bloquante (frontend + backend) - Alias : vérification conflit avec compte mail existant (409) - delete_domain : notifie l'agent mail pour suppression dans netadmin_mail - Clés DNSSEC archivées dans key_dir/old/ avant activation et à la suppression de zone - _get_ds_records : exclut le dossier old/ (évitait la lecture de doublons) - Modale TOTP : remplace window.confirm par une vraie modale - "Mon compte" accessible sans rôle - Suppression champ "Serveur API" de la sidebar
178 lines
5.0 KiB
Python
178 lines
5.0 KiB
Python
"""
|
|
config.py — Chargement centralisé de la configuration NetAdmin
|
|
"""
|
|
import os
|
|
import secrets
|
|
from pathlib import Path
|
|
from dataclasses import dataclass, field
|
|
from typing import List
|
|
|
|
try:
|
|
import yaml
|
|
_HAS_YAML = True
|
|
except ImportError:
|
|
_HAS_YAML = False
|
|
print("[WARN] PyYAML not installed — using defaults. Run: pip install pyyaml")
|
|
|
|
|
|
@dataclass
|
|
class ServerConfig:
|
|
host: str = "0.0.0.0"
|
|
port: int = 8000
|
|
cors_origins: List[str] = field(default_factory=lambda: ["*"])
|
|
behind_proxy: bool = False
|
|
|
|
|
|
@dataclass
|
|
class RedisTLSConfig:
|
|
enabled: bool = True
|
|
ca_cert: str = "/etc/netadmin/certs/ca.crt"
|
|
client_cert: str = "/etc/netadmin/certs/backend.crt"
|
|
client_key: str = "/etc/netadmin/certs/backend.key"
|
|
check_hostname: bool = True
|
|
|
|
|
|
@dataclass
|
|
class RedisConfig:
|
|
host: str = "localhost"
|
|
port: int = 6380
|
|
password: str = ""
|
|
db: int = 0
|
|
ack_timeout: int = 10
|
|
tls: RedisTLSConfig = field(default_factory=RedisTLSConfig)
|
|
|
|
|
|
@dataclass
|
|
class SmtpConfig:
|
|
enabled: bool = False
|
|
host: str = "localhost"
|
|
port: int = 587
|
|
username: str = ""
|
|
password: str = ""
|
|
from_addr: str = "netadmin@localhost"
|
|
from_name: str = "NetAdmin"
|
|
use_tls: bool = True # STARTTLS
|
|
use_ssl: bool = False # SSL/TLS direct (port 465)
|
|
# Notification triggers
|
|
notify_login: bool = True # send email on successful login
|
|
notify_role_added: bool = True # send email when a role is assigned
|
|
|
|
|
|
@dataclass
|
|
class SyncConfig:
|
|
interval: int = 300 # seconds between polls, 0 = disabled
|
|
|
|
|
|
@dataclass
|
|
class DnsConfig:
|
|
default_ns: list = field(default_factory=lambda: ["ns1.infolix.fr", "ns2.infolix.fr"])
|
|
default_soa_admin: str = "hostmaster"
|
|
dnssec_enabled: bool = False # whether DNSSEC signing is available on the agent
|
|
keygen_cmd: str = "dnssec-keygen"
|
|
key_dir: str = "/etc/bind/keys"
|
|
|
|
|
|
@dataclass
|
|
class DatabaseConfig:
|
|
host: str = "localhost"
|
|
port: int = 3306
|
|
user: str = "netadmin"
|
|
password: str = ""
|
|
name: str = "netadmin"
|
|
|
|
|
|
@dataclass
|
|
class MailConfig:
|
|
pass # mail data stored in shared netadmin.db
|
|
|
|
|
|
@dataclass
|
|
class OidcConfig:
|
|
enabled: bool = False
|
|
issuer: str = ""
|
|
client_id: str = ""
|
|
client_secret: str = ""
|
|
scopes: List[str] = field(default_factory=lambda: ["openid", "email", "profile"])
|
|
auto_provision: bool = True
|
|
redirect_uri: str = "http://localhost:3000/auth/callback"
|
|
use_pkce: bool = True
|
|
admin_group: str = ""
|
|
|
|
|
|
@dataclass
|
|
class AuthConfig:
|
|
session_secret: str = ""
|
|
session_ttl_hours: int = 8
|
|
oidc: OidcConfig = field(default_factory=OidcConfig)
|
|
# db_path removed — auth tables live in the shared netadmin.db
|
|
|
|
|
|
@dataclass
|
|
class AppConfig:
|
|
server: ServerConfig = field(default_factory=ServerConfig)
|
|
redis: RedisConfig = field(default_factory=RedisConfig)
|
|
sync: SyncConfig = field(default_factory=SyncConfig)
|
|
smtp: SmtpConfig = field(default_factory=SmtpConfig)
|
|
dns: DnsConfig = field(default_factory=DnsConfig)
|
|
mail_default_aliases: dict = field(default_factory=lambda: {
|
|
"abuse": "postmaster@localhost",
|
|
"hostmaster": "postmaster@localhost",
|
|
"postmaster": "admin@localhost",
|
|
"webmaster": "admin@localhost",
|
|
})
|
|
database: DatabaseConfig = field(default_factory=DatabaseConfig)
|
|
mail: MailConfig = field(default_factory=MailConfig)
|
|
auth: AuthConfig = field(default_factory=AuthConfig)
|
|
|
|
|
|
def _merge(obj, data: dict):
|
|
if not isinstance(data, dict):
|
|
return
|
|
for key, value in data.items():
|
|
if not hasattr(obj, key):
|
|
continue
|
|
current = getattr(obj, key)
|
|
if hasattr(current, '__dataclass_fields__'):
|
|
_merge(current, value)
|
|
else:
|
|
setattr(obj, key, value)
|
|
|
|
|
|
def _find_config_file():
|
|
for path in [
|
|
os.environ.get("NETADMIN_CONFIG", ""),
|
|
"/etc/netadmin/config.yaml",
|
|
str(Path(__file__).parent / "config.yaml"),
|
|
]:
|
|
if path and Path(path).exists():
|
|
return Path(path)
|
|
return None
|
|
|
|
|
|
def _load() -> AppConfig:
|
|
config = AppConfig()
|
|
config_file = _find_config_file()
|
|
if config_file is None:
|
|
print("[INFO] No config.yaml found — using defaults")
|
|
return _finalise(config)
|
|
print(f"[INFO] Loading configuration from: {config_file}")
|
|
if not _HAS_YAML:
|
|
return _finalise(config)
|
|
try:
|
|
with open(config_file) as f:
|
|
raw = yaml.safe_load(f) or {}
|
|
_merge(config, raw)
|
|
except Exception as e:
|
|
print(f"[ERROR] Failed to parse {config_file}: {e}")
|
|
return _finalise(config)
|
|
|
|
|
|
def _finalise(config: AppConfig) -> AppConfig:
|
|
if not config.auth.session_secret:
|
|
config.auth.session_secret = secrets.token_hex(32)
|
|
print("[INFO] session_secret not set — generated a random one (sessions won't survive restarts)")
|
|
return config
|
|
|
|
|
|
cfg: AppConfig = _load()
|