runv-server

server tooling for runv.club
Log | Files | Refs | README

commit 6da121f22f0c41023ccbe5b2f46565287127c8b6
parent 2959442a903f67337d71af32efa9a22b8fffeadc
Author: Pablo Murad <pablo@pablomurad.com>
Date:   Sat, 21 Mar 2026 20:16:36 -0300

fixed usr script

Diffstat:
M.gitignore | 12++++++++++--
Memail/docs/INTEGRATION.md | 19++++++++++++-------
Memail/lib/mailer.py | 4++++
Mscripts/admin/create_runv_user.py | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 126 insertions(+), 9 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -13,4 +13,12 @@ site/public/news/data/news.json site/public/news/feed.rss # Wiki: bytecode do gerador local -site/wiki/__pycache__/ -\ No newline at end of file +site/wiki/__pycache__/ + +# Diagnóstico local Mailgun (pode conter chaves / não subir) +email/testmail.py +email/testmail*.py + +# Cópias locais de segredos de email (nunca versionar) +runv-email.secrets.json +**/runv-email.secrets.json +\ No newline at end of file diff --git a/email/docs/INTEGRATION.md b/email/docs/INTEGRATION.md @@ -46,11 +46,11 @@ Com **Mailgun**, `sendmail` é ignorado para o transporte (usa API). Com **legad | Evento | Template(s) | Onde disparar | |--------|-------------|----------------| -| Novo pedido na fila `entre` | `admin_new_request` → admin; opcional `user_request_received` → visitante | Após `save_request_json` em [`terminal/entre_core.py`](../../terminal/entre_core.py) / [`entre_app.py`](../../terminal/entre_app.py). **Hoje** email admin via `sendmail_notify` + `admin_mail.txt` — com Mailgun, `sendmail_notify` tenta **primeiro** a API se o estado global o indicar. | -| Pedido aprovado (manual) | `user_approved` | Processo admin. | +| Novo pedido na fila `entre` | Corpo do email: [`terminal/templates/admin_mail.txt`](../../terminal/templates/admin_mail.txt) (não `email/templates/admin_new_request.txt`). Opcional: `user_request_received` existe em `email/templates/` mas **não** está ligado ao `entre`. | Após `save_request_json` em [`terminal/entre_core.py`](../../terminal/entre_core.py) / [`entre_app.py`](../../terminal/entre_app.py). Email admin via `sendmail_notify`; com Mailgun, tenta **primeiro** `lib.mailer.send_mail` se `/etc/runv-email.json` e `email_package_root` / `RUNV_EMAIL_ROOT` forem válidos. | +| Pedido aprovado (manual) | `user_approved` | Processo admin (manual / futuro). | | Pedido rejeitado | `user_rejected` (+ `reason`) | Idem. | -| Conta criada | `admin_user_created`, `user_account_created` | [`scripts/admin/create_runv_user.py`](../../scripts/admin/create_runv_user.py). | -| Conta removida | `admin_user_deleted`, `user_account_removed` | [`scripts/admin/del-user.py`](../../scripts/admin/del-user.py). | +| Conta criada | `admin_user_created` → admin; `user_account_created` → utilizador | [`scripts/admin/create_runv_user.py`](../../scripts/admin/create_runv_user.py): `--no-welcome-email` / `--no-admin-create-email` para desactivar cada ramo. | +| Conta removida | `admin_user_deleted`, `user_account_removed` | Templates em `email/templates/`; [`scripts/admin/del-user.py`](../../scripts/admin/del-user.py) **ainda não** envia estes emails (processo manual ou extensão futura). | | Erro operacional | `admin_error` | Scripts admin / cron. | | Quota | `user_quota_warning` | Monitorização / quotas. | | Teste | `system_test` | `configure_mailgun.py --test` (API) ou legado. | @@ -71,11 +71,14 @@ Recomenda-se o **mesmo** `admin_email` e remetente coerente com o Mailgun/domín ## `create_runv_user.py` / `del-user.py` -O **`create_runv_user.py`** envia por omissão um email de **boas-vindas** ao utilizador (`user_account_created`), com instruções para aceder por SSH com a **chave privada** correspondente à chave pública registada. Requer `/etc/runv-email.json` e módulo `email/` acessível; `--no-welcome-email` para desactivar; `--welcome-ssh-host` ou `RUNV_WELCOME_SSH_HOST` para um comando `ssh` explícito. +O **`create_runv_user.py`** envia por omissão: -Obtenha `admin_email` / `default_from` de `/etc/runv-email.json` — **não** hardcodar. +1. **Boas-vindas** ao utilizador (`user_account_created`), com instruções SSH; `--no-welcome-email` desactiva. +2. **Aviso ao admin** (`admin_user_created` para `admin_email` no JSON); `--no-admin-create-email` desactiva. + +Requer `/etc/runv-email.json` (com `default_from`, `admin_email` para o ramo admin), segredos Mailgun se aplicável, e pasta `email/` acessível (`email_package_root` ou `RUNV_EMAIL_ROOT`). Para o texto de boas-vindas, `--welcome-ssh-host` ou `RUNV_WELCOME_SSH_HOST` define o hostname SSH sugerido. -Ver exemplos na versão anterior deste documento para `send_admin_notice` / `send_user_notice` adicionais. +Obtenha `admin_email` / `default_from` de `/etc/runv-email.json` — **não** hardcodar. ## Checklist de integração @@ -83,3 +86,5 @@ Ver exemplos na versão anterior deste documento para `send_admin_notice` / `sen - [ ] `sudo python3 configure_mailgun.py --test` (ou legado) com sucesso. - [ ] Templates revistos (português, placeholders). - [ ] Nenhum segredo em logs ou `print()` (API key só em ficheiro 0600 ou env). + +Roteiro passo a passo no servidor: [VERIFICATION_CHECKLIST.md](VERIFICATION_CHECKLIST.md). diff --git a/email/lib/mailer.py b/email/lib/mailer.py @@ -181,6 +181,7 @@ def send_admin_notice( from_addr: str, sendmail: str | None = None, html_body: str | None = None, + _state: dict | None = None, **kwargs: object, ) -> None: """Renderiza template administrativo e envia para admin_email.""" @@ -192,6 +193,7 @@ def send_admin_notice( from_addr=from_addr, sendmail=sendmail, html=html_body, + _state=_state, ) @@ -203,6 +205,7 @@ def send_user_notice( from_addr: str, sendmail: str | None = None, html_body: str | None = None, + _state: dict | None = None, **kwargs: object, ) -> None: """Renderiza template para utilizador e envia para user_email.""" @@ -214,6 +217,7 @@ def send_user_notice( from_addr=from_addr, sendmail=sendmail, html=html_body, + _state=_state, ) diff --git a/scripts/admin/create_runv_user.py b/scripts/admin/create_runv_user.py @@ -1330,6 +1330,7 @@ def try_send_welcome_email( user_email, subject="[runv.club] Bem-vindo(a) — a sua conta foi criada", from_addr=from_addr, + _state=state, username=username, email=user_email, fingerprint=fingerprint, @@ -1342,6 +1343,91 @@ def try_send_welcome_email( log.warning("email de boas-vindas falhou (conta já criada): %s", e) +def try_send_admin_user_created_email( + *, + username: str, + user_email: str, + operator_info: str, + timestamp: str, + no_admin_create_email: bool, + dry_run: bool, + log: logging.Logger, +) -> None: + """ + Envia ``admin_user_created`` para ``admin_email`` em ``/etc/runv-email.json``. + Falhas só em log — a conta já foi criada. + """ + if no_admin_create_email: + log.info("email admin (conta criada): omitido (--no-admin-create-email)") + return + if dry_run: + log.info("email admin (conta criada): omitido (--dry-run)") + return + + state_file = Path("/etc/runv-email.json") + if not state_file.is_file(): + log.info( + "email admin (conta criada): %s ausente — omitido", + state_file, + ) + return + try: + state = json.loads(state_file.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as e: + log.warning("email admin (conta criada): estado inválido (%s): %s", state_file, e) + return + + admin = str(state.get("admin_email", "")).strip() + if not admin: + log.info( + "email admin (conta criada): admin_email vazio em %s — omitido", + state_file, + ) + return + + email_root = _resolve_email_package_root(state) + if email_root is None: + log.warning( + "email admin (conta criada): pasta email/ não encontrada " + "(RUNV_EMAIL_ROOT, email_package_root no JSON ou repositório em %s)", + _REPO_ROOT / "email", + ) + return + + root_s = str(email_root.resolve()) + if root_s not in sys.path: + sys.path.insert(0, root_s) + + try: + from lib.mailer import send_admin_notice + from lib.templates import ADMIN_USER_CREATED + except ImportError as e: + log.warning("email admin (conta criada): import lib.mailer falhou: %s", e) + return + + from_addr = str(state.get("default_from", "")).strip() + if not from_addr: + log.warning("email admin (conta criada): default_from ausente em %s", state_file) + return + + try: + send_admin_notice( + ADMIN_USER_CREATED, + admin, + subject=f"[runv.club] Conta criada — {username}", + from_addr=from_addr, + _state=state, + username=username, + email=user_email, + operator_info=operator_info, + timestamp=timestamp, + ) + log.info("email admin (conta criada) enviado para %s", admin) + print(f" admin (conta): email enviado para {admin}") + except Exception as e: + log.warning("email admin (conta criada) falhou (conta já criada): %s", e) + + def parse_args(argv: list[str] | None = None) -> argparse.Namespace: p = argparse.ArgumentParser( description=( @@ -1495,6 +1581,11 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: help="não enviar email de boas-vindas ao utilizador após criar a conta", ) p.add_argument( + "--no-admin-create-email", + action="store_true", + help="não enviar email ao admin (template admin_user_created) após criar a conta", + ) + p.add_argument( "--welcome-ssh-host", default=None, metavar="HOST", @@ -1804,6 +1895,15 @@ def main(argv: list[str] | None = None) -> int: dry_run=bool(args.dry_run), log=log, ) + try_send_admin_user_created_email( + username=user, + user_email=email, + operator_info=record.created_by, + timestamp=record.created_at, + no_admin_create_email=bool(args.no_admin_create_email), + dry_run=bool(args.dry_run), + log=log, + ) if not args.no_quota and qr.status in ("failed", "not_configured"): print(