commit 42f7676f4f34c395279a7e40212c29ac7f196557
parent e2e74bc0461b9fcaf1b180a0c5eba1d35d0c288d
Author: Pablo Murad <pablo@pablomurad.com>
Date: Sat, 21 Mar 2026 20:49:17 -0300
fixed a lot of stuff
Diffstat:
6 files changed, 144 insertions(+), 12 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -8,6 +8,9 @@ venv/
.env.local
guide.md
+# config do módulo entre: gerar com terminal/gen_config_toml.py (não versionar)
+terminal/config.toml
+
# Gerados no servidor / localmente (evita conflitos em git pull)
site/public/news/data/news.json
site/public/news/feed.rss
diff --git a/terminal/README.md b/terminal/README.md
@@ -11,7 +11,8 @@ Módulo **runv.club** para quem se liga por SSH ao utilizador Unix **`entre`**:
| `entre_app.py` | Programa principal (ForceCommand SSH). |
| `entre_core.py` | Validação, fila JSON, log, email. |
| `setup_entre.py` | Instalação no servidor (root): utilizador `entre`, shell `/bin/sh`, `--auth-mode` (`shared-password` \| `key-only` \| `empty-password` estilo tilde.town), PAM opcional, drop-in SSH, `sshd -t` + `sshd -T -C`, reload. |
-| `config.example.toml` | Modelo de configuração → copiar para `config.toml`. |
+| `config.example.toml` | Modelo versionado; **não** editar como `config.toml` no git. |
+| `gen_config_toml.py` | Gera `config.toml` a partir do example (evita conflitos em `git pull`). |
| `templates/*.txt` | Textos da experiência e do email ao admin. |
| `docs/USO.md` | **Instalação + uso** (admin, visitante, testes, checklist). |
| `docs/INSTALL.md` | Guia de instalação detalhado (Debian 13). |
@@ -26,7 +27,7 @@ Em linhas:
1. Como root: `python3 setup_entre.py` (ou `scripts/install.sh`) — por omissão `--auth-mode shared-password`, shell `/bin/sh`, validação `sshd -T -C …`.
2. **Onboarding sem senha (estilo tilde.town):** `sudo python3 setup_entre.py --auth-mode empty-password` — **PAM** por omissão; SSH por omissão **keyboard-interactive** (melhor no **OpenSSH do Windows**). Teste: `ssh entre@runv.club`. Se a sessão fechar, veja PAM e logs em [INSTALL.md](docs/INSTALL.md). Para o modo README tilde (**password** + senha vazia), use **`--empty-password-tilde-password-auth`** (Linux/Git Bash).
-3. Editar `/opt/runv/terminal/config.toml` (`admin_email`, etc.).
+3. Gerar ou ajustar `/opt/runv/terminal/config.toml` com `python3 gen_config_toml.py --install-root /opt/runv/terminal` (ou `--force` para repor o example). O ficheiro está em `.gitignore` no clone; só o **example** é versionado.
4. Modo default: `sudo passwd entre`. Modo `key-only`: `authorized_keys`.
5. Visitante: `ssh entre@runv.club` e seguir o fluxo até à despedida.
diff --git a/terminal/config.example.toml b/terminal/config.example.toml
@@ -1,4 +1,5 @@
-# Copie para config.toml ao instalar (ex.: /opt/runv/terminal/config.toml).
+# Não edite como config.toml em produção: gere com gen_config_toml.py ou setup_entre.py.
+# Ex.: python3 gen_config_toml.py --install-root /opt/runv/terminal
# Valores por omissão da fila e log alinham com setup_entre.py.
queue_dir = "/var/lib/runv/entre-queue"
diff --git a/terminal/docs/INSTALL.md b/terminal/docs/INSTALL.md
@@ -63,7 +63,16 @@ Opções úteis:
## 4. Configuração (`config.toml`)
-Edite **`/opt/runv/terminal/config.toml`**:
+O **`config.toml`** não deve ser versionado no repositório (está em **`.gitignore`** em `terminal/config.toml` no clone). Gere-o a partir do modelo:
+
+```bash
+sudo python3 /opt/runv/src/terminal/gen_config_toml.py --install-root /opt/runv/terminal
+```
+
+- **`--force`** — sobrescreve um `config.toml` já existente (perde edições locais nesse ficheiro).
+- O **`setup_entre.py`** chama a mesma lógica na primeira instalação (ou com **`--force-config`**).
+
+Edite **`/opt/runv/terminal/config.toml`** quando precisar de valores que não vêm do example:
- **`admin_email`** — endereço para notificações. Pode ficar vazio no TOML se **`admin_email`** estiver definido em **`/etc/runv-email.json`** (fallback usado pelo `entre_app.py`). Se ambos estiverem vazios, só fila + log.
- **`mail_from`** — remetente do email (cabeçalho `From`); por omissão **`noreply@runv.club`** (não use **`entre@runv.club`**: essa conta é só SSH). Valores antigos `entre@runv.club` no TOML são normalizados para noreply. Para outro remetente verificado no Mailgun, defina explicitamente no TOML.
diff --git a/terminal/gen_config_toml.py b/terminal/gen_config_toml.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python3
+"""
+Gera ``config.toml`` a partir de ``config.example.toml`` (sem editar o example no git).
+
+Uso típico no servidor após ``git pull`` (evita conflitos se ``config.toml`` não for versionado):
+
+ sudo python3 /opt/runv/src/terminal/gen_config_toml.py --install-root /opt/runv/terminal
+
+No clone local (ficheiro em ``terminal/config.toml``, ignorado pelo git):
+
+ python3 terminal/gen_config_toml.py
+
+O ``setup_entre.py`` chama a mesma função ao instalar o módulo.
+"""
+
+from __future__ import annotations
+
+import argparse
+import shutil
+import sys
+from pathlib import Path
+from typing import Final, Literal
+
+SCRIPT_DIR: Final[Path] = Path(__file__).resolve().parent
+
+
+def write_terminal_config_toml(
+ *,
+ example: Path,
+ out: Path,
+ force: bool,
+ dry_run: bool,
+) -> Literal["wrote", "skipped", "dry_run"]:
+ """
+ Copia ``example`` para ``out`` se ``out`` não existir ou ``force``.
+
+ Returns:
+ ``wrote``, ``skipped`` (já existia e não force), ou ``dry_run``.
+ """
+ if not example.is_file():
+ raise FileNotFoundError(f"modelo em falta: {example}")
+ if dry_run:
+ return "dry_run"
+ if out.is_file() and not force:
+ return "skipped"
+ out.parent.mkdir(parents=True, exist_ok=True)
+ shutil.copy2(example, out)
+ try:
+ out.chmod(0o640)
+ except OSError:
+ pass
+ return "wrote"
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(
+ description="Gera config.toml do módulo entre a partir de config.example.toml.",
+ )
+ parser.add_argument(
+ "--install-root",
+ type=Path,
+ default=SCRIPT_DIR,
+ help="directório do módulo (default: pasta deste script)",
+ )
+ parser.add_argument(
+ "--example",
+ type=Path,
+ default=None,
+ help="caminho explícito do config.example.toml (default: <install-root>/config.example.toml)",
+ )
+ parser.add_argument(
+ "--force",
+ action="store_true",
+ help="sobrescrever config.toml existente",
+ )
+ parser.add_argument("--dry-run", action="store_true")
+ args = parser.parse_args()
+
+ root = args.install_root.resolve()
+ example = args.example.resolve() if args.example else root / "config.example.toml"
+ out = root / "config.toml"
+
+ try:
+ result = write_terminal_config_toml(
+ example=example,
+ out=out,
+ force=bool(args.force),
+ dry_run=bool(args.dry_run),
+ )
+ except FileNotFoundError as e:
+ print(e, file=sys.stderr)
+ return 1
+
+ if result == "dry_run":
+ print(f"[dry-run] escreveria {out} a partir de {example}")
+ return 0
+ if result == "skipped":
+ print(f"Mantido {out} (use --force para substituir pelo example).")
+ return 0
+ print(f"Escrito {out} a partir de {example}.")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/terminal/setup_entre.py b/terminal/setup_entre.py
@@ -59,6 +59,8 @@ import time
from pathlib import Path
from typing import Final
+from gen_config_toml import write_terminal_config_toml
+
VERSION: Final[str] = "0.11"
ENTRE_USER: Final[str] = "entre"
INSTALL_ROOT: Final[Path] = Path("/opt/runv/terminal")
@@ -634,6 +636,7 @@ def copy_module(dest: Path, *, dry_run: bool) -> None:
"entre_app.py",
"entre_core.py",
"config.example.toml",
+ "gen_config_toml.py",
"README.md",
]
subdirs = ["templates", "docs", "systemd", "scripts", "data", "examples"]
@@ -659,16 +662,22 @@ def install_config(dest: Path, *, dry_run: bool, force: bool) -> None:
cfg = dest / "config.toml"
example = dest / "config.example.toml"
if dry_run:
- print(f"[dry-run] config em {cfg}")
+ print(f"[dry-run] config em {cfg} (gen_config_toml)")
+ return
+ if not example.is_file():
+ eprint(f"Aviso: {example} não encontrado.")
return
- if cfg.is_file() and not force:
- print(f"Mantido {cfg} existente (use --force-config para sobrescrever do example).")
+ try:
+ result = write_terminal_config_toml(
+ example=example, out=cfg, force=force, dry_run=False
+ )
+ except FileNotFoundError as e:
+ eprint(str(e))
return
- if example.is_file():
- shutil.copy2(example, cfg)
- print(f"Instalado {cfg} a partir do example.")
+ if result == "skipped":
+ print(f"Mantido {cfg} existente (use --force-config para regenerar do example).")
else:
- eprint(f"Aviso: {example} não encontrado.")
+ print(f"Instalado {cfg} (gen_config_toml a partir do example).")
def chmod_tree_templates(root: Path) -> None:
@@ -690,7 +699,11 @@ def print_final_instructions(
) -> None:
print()
print("== Concluído ==")
- print(f"1. Editar {install_root / 'config.toml'} (admin_email, etc.).")
+ print(
+ f"1. Opcional: {install_root / 'config.toml'} — regenere com "
+ f"python3 {install_root / 'gen_config_toml.py'} --install-root {install_root} "
+ "(ou --force para repor o example). Com /etc/runv-email.json, admin_email pode ficar vazio no TOML."
+ )
if auth_mode == AUTH_SHARED:
print("2. Acesso por palavra-passe Unix partilhada (definida só pelo root):")