Commit Graph
10 Commits
Author SHA1 Message Date
andreandClaude Sonnet 5.5 aa55d44406 feat(sidebar): fleet sidebar standard (header row, user in sidebar, collapsible groups, mini mode, no-flash transitions)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-13 16:56:46 +01:00
andreandClaude Sonnet 5.5 4446224c19 feat(settings): save uploaded logos to uploads/public/logo
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-09 19:41:01 +01:00
andreandClaude Sonnet 5 f066eec7c7 feat(platform-tools): liga platform_connect() e o router de páginas de sistema
O bootstrap só reimplementava pedaços do platform-tools à mão (sessão
endurecida, IP do cliente) sem nunca chamar platform_connect() — sem
router, só 4 das ~9 páginas de sistema estavam ligadas (faltavam IP
bans, Geo, Origem das visitas, Hub), e o log de erro de arranque ainda
dizia "[fotografia bootstrap]", copiado sem adaptar.

Liga-se com 'auth' => 'own': a app mantém as suas próprias passwords e
sessão, porque a auth_attempts local (ip+scope) não tem a coluna
identifier que o throttle partilhado exige — trocar para 'shared'
partia o throttle. Ganha-se à mesma o bloqueio de IP da frota e o
router (platform.php / api/platform.php), com Comunicações e Fila de
mensagens declaradas como sysadmin_tools próprias (substituem o Estado
SMTP partilhado, que já cobrem). Os stubs de Backups/Actualizações/
Auditoria passam a reencaminhar para o router; o menu de Sistema passa
a incluir IP bans, Geo e Origem das visitas.

'conformance' => 'report' para nunca recusar pedidos — só reporta no
health endpoint — enquanto o resto da migração de autenticação fica
por decidir.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-25 12:16:43 +01:00
andreandClaude Opus 5.5 0e8b53f620 feat(audit): platform-tools atualizado (12f26fd) e registo de auditoria ligado
- shared/platform-tools: ea3616a → 12f26fd (27 commits). Traz o módulo de
  audit (f761d99, o pin da frota), «manter sessão iniciada», o registo de
  segurança da frota (o gancho que o login já tinha passa a funcionar) e a
  correção do IP do cliente encontrada nesta auditoria (último X-Forwarded-For
  / X-Real-IP), promovida ao partilhado. client_ip() da app delega nele.
- config/functions_audit.php: audit() sobre platform_audit(); tabela
  audit_log criada no primeiro uso, segredos ocultados, nunca parte a ação.
- Registado: login, falha de login, bloqueio por excesso de tentativas,
  logout, pedidos de recuperação, mudança/definição de palavra-passe,
  registo e criação de utilizadores, convites, perfil (antes/depois), todas
  as definições (db_update_setting, com valor anterior), cada passo da
  lixeira (apagar, restaurar, eliminar de vez) e ações nas mensagens.
- Sistema → Auditoria (views/sysadmin/audit.php), só sysadmin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 07:28:13 +01:00
andreandClaude Opus 5.5 895b3d58d7 fix(segurança): lixeira de 30 dias, CSRF em todas as ações, IP real, sessão e uploads
Correções da auditoria de 24/09 (audits/fitme3070-auditoria-2026-09-24.html).

Lixeira
- Tudo o que é importante passa a soft delete (deleted_at + deleted_by):
  equipa, modalidades, eventos, mensalidades, galerias, imagens, horário,
  horas de funcionamento, inscrições em eventos e pedidos de inscrição.
- config/functions_trash.php: um registo por tipo; apagar, restaurar, listar
  e eliminar leem todos dele. Eliminar de vez só com 30 dias na lixeira —
  a condição está no WHERE do DELETE, um POST forjado não apaga nada.
- Gestão → Lixeira (views/admin/trash/index.php): lista única com quem e
  quando, restauro a qualquer momento.
- Imagens de galeria: o site lista a pasta, por isso o ficheiro passa para
  .trash/ (negado pelo .htaccess) e volta no restauro.
- As listagens e o site público filtram deleted_at (horário também filtra
  modalidades apagadas). Migração: database/migration_20260925_trash.sql
  (só acrescenta colunas; tem de ser aplicada antes deste código).

CSRF
- csrf_check() recusa qualquer pedido que não seja POST (405). Antes saía
  sem verificar nada em GET, e dez ações de apagar/restaurar eram GET.
- Os dez handlers passaram a POST com token; links viraram formulários.

Autenticação e sessão
- client_ip(): X-Real-IP / último X-Forwarded-For, só vindo de proxy privado
  com TRUSTED_PROXY. O throttle contava o IP do proxy (limite global).
- Throttle também por conta (hash do email), além do IP.
- Sem oráculo de tempo: login verifica contra um hash de referência quando
  a conta não existe; a recuperação deixa o email na fila.
- Cookie de sessão Secure/HttpOnly/SameSite=Lax e use_strict_mode.
- Definições de sistema só para sysadmin; autocomplete=username.

Servidor e uploads
- .htaccess: nosniff, Referrer-Policy, X-Frame-Options, Permissions-Policy,
  CSP (frame-ancestors, base-uri, object-src, form-action); nega parciais
  _*.php, PHP soltos do platform-tools e *.md; 404 própria.
- Uploads validados pelo conteúdo (finfo/getimagesize); SVG só sem scripts,
  handlers on*=, foreignObject ou hrefs externos.

Testado numa cópia da BD de produção (fitme_audit_test) com um contentor
temporário: 34/34 verificações HTTP ponta-a-ponta.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 07:16:10 +01:00
andreandClaude Opus 5.5 0587be7d14 feat: borda verde a rodar nos planos, textura metálica, contactos numa só fonte
- Mensalidades: ao passar o rato, a borda do cartão ganha um brilho verde num
  conic-gradient que roda (@property --pc-a animável), centrado no cursor; o
  cartão inclina-se e desliza para o rato como os da equipa.
- «O que dizem de nós» e rodapé: textura discreta de metal preto mate (grão de
  pintura eletrostática, brilho difuso, riscas quase invisíveis), em SVG/CSS.
- Contactos: o site lê só fitme3070_info (Gestão → Contactos). Os campos
  contact_*/social_* de Sistema → Definições não eram usados e foram editados
  por engano — saem do formulário, com um aviso a apontar para Contactos.
  site_info() deixa o WhatsApp só com dígitos (wa.me não aceita + nem espaços).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 21:57:15 +01:00
andreandClaude Opus 5 0e034232e9 Survive a stale platform-tools instead of 500ing the whole site
Deploying the local-SMTP commit took fitme3070.pt down:

    PHP Warning: require_once(.../shared/platform-tools/compat_secrets.php):
    Failed to open stream: No such file or directory
    in .../config/functions_secrets.php on line 20

config/functions_secrets.php is loaded on every request, so a hard require_once
on a submodule file is a site-wide fatal — public pages included — whenever
shared/platform-tools lags the code that needs it. It lagged: the server is
still on 603ff84, which predates compat_secrets.php, because "Atualizar
submódulo" was itself broken (fixed in 3626080, which the site had to be up to
deploy). A missing file should not be able to close that loop.

Three requires hardened, each to the failure the caller can actually report:

  - config/functions_secrets.php: falls back to plaintext passthrough helpers
    and logs loudly. secrets_available() then returns false, which is exactly
    what the Comunicações page already renders a warning banner for — so
    nothing silently pretends to be encrypted while the site keeps serving.

  - config/functions_queue.php (_flush_email): throws instead of fataling. A
    fatal killed the request outright, so flush_message()'s catch never ran and
    the row sat in 'sending' forever with no explanation. It now lands in
    last_error where the Monitor page shows it.

  - views/sysadmin/comms.php: renders "Submódulo desactualizado" naming the
    missing files, rather than a blank 500 on the page you would open to
    diagnose mail in the first place.

Verified by loading config/functions_secrets.php with no shared/ directory
present: the bootstrap completes, the warning is logged once, secrets_available()
is false, and setting_secret()/setting_or_env() still return usable values.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 22:13:44 +01:00
andreandClaude Opus 5 d5cb50b432 Send mail locally again: message_queue + PHPMailer over SMTP
Outbound email was relayed to the hub's /notify endpoint, so this install had
no SMTP configuration of its own and no way to see what had been sent. Bring
delivery back in-house, following the fotografia.andmore.pt stack rather than
inventing a second shape for it.

  - Submodule bumped 603ff84 -> ea3616a, the pin fotografia already uses. That
    is what provides functions_mail.php (shared transport), functions_settings.php
    (platform_smtp_config + encrypted settings) and compat_secrets.php. Verified
    no function-name collisions with this repo's own config/functions_settings.php:
    platform-tools namespaces everything platform_*.

  - PHPMailer 7.1.1 vendored at vendor/PHPMailer/src/. platform_mail_available()
    looks for exactly this hand-vendored layout before falling back to composer's
    autoloader, so the host needs no shell and no composer install. LGPL-2.1
    license text included.

  - config/functions_queue.php replaced: was a thin facade over the hub relay,
    now fotografia's real queue. Rows are claimed atomically, retried with
    exponential backoff up to max_attempts, and park in 'failed' with the real
    SMTP rejection in last_error.

  - SMTP resolution matches the fleet: .env wins for the SMTP_* constants, while
    the actual send goes through platform_smtp_config($pdo), which is DB-first.
    That ordering is deliberate -- the database is the half an operator can
    change from the backoffice without shell access. Empty SMTP_HOST falls back
    to PHP mail().

  - Sistema -> Comunicações (views/sysadmin/comms.php): credentials plus a live
    SMTP connection test and a verdict on what would happen to a message sent
    right now. Secret-valued settings are write-only in the form and encrypted
    at rest (AES-256-GCM, key from .env, never in the DB dump). Which keys count
    as secret is platform_settings_is_secret()'s decision, not a list kept here.

  - Sistema -> Mensagens (views/sysadmin/messages.php): the queue monitor.
    Filters, per-message detail with last_error, and retry/cancel/archive.
    Retry flushes inline so the operator sees the outcome immediately.

  - cron.php drains the queue every 5 minutes, serialised with a lock file, and
    runs retention once a day. Over HTTP it answers 404 until CRON_TOKEN is set
    to something other than replace_me. Deliberately not a copy of fotografia's
    cron: the session-reminder, download-digest and photo-archive steps have no
    modules here and would be dead code.

  - config/functions_notify_client.php deleted, and ANDMORE_ID_URL with it --
    the hub relay was its only caller.

Verified without a database: PHP lint across the tree; PHPMailer builds a full
MIME message with RFC 2047 UTF-8 subject encoding from the three vendored files;
platform_mail_available() finds them; platform_mail_make() returns a correctly
configured mailer; the diagnosis states report Operacional / Não configurado.
NOT verified: the settings encryption round-trip -- this machine's PHP has no
openssl extension, so platform_secret_key() returns null and values pass through
in clear. The code degrades safely (stores the value rather than losing it) and
the Comunicações page warns when the key is unavailable.

Deployment: apply database/migration_20260913_message_queue.sql, set SMTP in
Comunicações or .env, set APP_SECRET_KEY, and schedule cron.php.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 22:08:38 +01:00
andre 3e3382df57 idk 2026-06-12 21:20:23 +01:00
andreandClaude Opus 4.8 e68d357177 Initial import of fitme3070.pt platform
Plain-PHP fleet platform with id.andmore.pt SSO. Wires shared/platform-tools
as a submodule (platform_update, backups, git/migration tools) like the other
platforms.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 10:44:58 +01:00