164 Commits
Author SHA1 Message Date
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
andreandClaude Opus 5 3626080599 Fix submodule update: declare branch, use a relative URL
"Atualizar submódulo" failed on the server with:

    fatal: Unable to find refs/remotes/origin/HEAD revision in submodule
    path 'shared/platform-tools'

Two causes, both in .gitmodules.

1. No `branch` key. platform_submodules() falls back to the submodule's
   refs/remotes/origin/HEAD symbolic ref when .gitmodules does not name a
   branch. That ref is written by `git remote set-head`, which a plain clone
   on the server never ran, so the lookup had nothing to resolve. Declaring
   `branch = main` removes the fallback entirely. platform-tools' default
   branch is `main`, not `master`.

2. url used http://, which returns 523 from the origin. Switched to the
   relative form fotografia.andmore.pt already uses: it resolves against this
   repo's own origin, so it follows whatever host and scheme the parent was
   cloned with and carries no credentials of its own.

.gitmodules now matches the fotografia sibling exactly. The pull that brings
this commit in also runs `git submodule sync --recursive` (git_sync_submodules),
which is what rewrites the stored URL — no manual step on the server.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 22:03:44 +01:00
andreandClaude Opus 5 aec7b47820 Replace Andmore ID SSO with local login
fitme3070.pt delegated all authentication to hub.andmore.pt: views/auth/*
were bounce stubs, and because this domain is outside the .andmore.pt cookie
scope the real handoff was a one-time HTTPS ticket redeem. Bring identity back
in-house — passwords live in users.password_hash, which the SSO migration
never dropped.

Removed: config/sso.php, IDENTITY_MODE, every SSO_* constant and env key,
identity_is_federated() and the cookie/ticket paths in the auth views and the
public-site bootstrap. ANDMORE_ID_URL stays — it is still the hub /notify
endpoint for outbound email/SMS, which the reset mails ride on, and has
nothing to do with authentication now.

Added, ported from the fotografia.andmore.pt sibling rather than invented:

  - auth_login() with transparent password_needs_rehash upgrades and the
    last_login trail; auth_register_client(); auth_set_password(); the
    single-use 2h reset tokens.
  - Real login / register / forgot_password / reset_password pages on a new
    anonymous shell (includes/auth_header.php). includes/header.php renders
    the backoffice sidebar and a logout button, which is wrong for a visitor
    who is not signed in yet.
  - safe_return_path(), so a crafted ?return_to= cannot bounce a freshly
    authenticated user off-site.
  - Per-IP throttling on login, register and reset-request. Successes count
    too, so a guessed password cannot be parlayed into stuffing the rest of
    the user list from the same IP.
  - views/user/set_password.php: admin override for anyone whose email is
    undeliverable. Prefers mailing an invite link so the backoffice never
    handles the password. A plain admin cannot act on a sysadmin, and nobody
    can use it on themselves — self-service goes through profile.php, which
    demands the current password so a hijacked session cannot lock the owner
    out.

Accounts provisioned during the SSO era have password_hash NULL. They get in
via the reset email, or by registering again with the same address:
auth_register_client() claims a passwordless row, but only a plain `client`
one, so knowing a staff address does not let anyone self-assign the role.

users.andmore_id is deliberately left in place. No code reads it any more, but
it records which central identity each row was linked to.

Deployment: apply database/migration_20260913_local_auth.sql (password_resets
+ auth_attempts) in the SAME sysadmin session that pulls this commit — the
forgot-password flow needs that table, and after the pull a login requires a
local password.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 21:00:57 +01:00
andreandClaude Opus 4.8 dc8a36e126 style: align hero image to top with object-position
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 21:50:26 +01:00
andre e6d4eabcc8 attempt to commit manually 2026-06-16 13:27:20 +01:00
andreandClaude Opus 4.8 3d460e50d1 Retire hub-content API endpoint via delete_ rename
Rename api/hub-content.php -> api/delete_hub-content.php so the old
content-pull path returns 404, disabling the hub social-post endpoint
without removing the implementation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 12:19:24 +01:00
andreandClaude Opus 4.8 03102cf1a9 Fix horário calendar save: surface real error, validate sala
The save AJAX endpoint swallowed DB exceptions behind a generic
"Erro ao guardar" with no logging, making failures undiagnosable.
Now log the real exception (same sink as the global handler) and
echo the detail back in dev.

Also validate `local` against the allowed sala set instead of only
its length, so an out-of-enum value returns a clean 422 instead of
a 500 (horario.local is a strict ENUM).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 12:16:11 +01:00
Platform Update d5ea2b963c Bump shared/platform-tools to 603ff84 2026-06-13 12:31:44 +01:00
Platform Update a9edb51a3c Bump shared/platform-tools to eaea122 2026-06-13 12:18:18 +01:00
andreandClaude Fable 5 026a3a2d32 Add remote-ops agent stub for the hub fleet control plane
One-line require of the platform-tools submodule agent. Needs HUB_OPS_SECRET
in .env and "Operações remotas" enabled on id.andmore.pt to respond.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 12:15:56 +01:00
andreandClaude Opus 4.8 7885b0b6ac SSO: redeem ticket over HTTPS instead of the central DB
This install is on external hosting and can't reach the central DB's private
LAN address (192.168.1.3), so the direct-DB burn always failed with
ECONNREFUSED. Redeem via the hub's api/sso_redeem.php over HTTPS (HMAC with
HUB_CONTENT_SECRET, ticket folded into the signature) and provision the
returned user into the local DB.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 23:55:13 +01:00
andreandClaude Opus 4.8 9ad0c2b763 SSO: redeem one-time ticket on return from the hub
fitme3070.pt is off the .andmore.pt cookie scope, so am_sso never arrives
and login never provisioned the user. Redeem the ?sso_ticket=… the hub now
appends to the return URL instead.

- sso_redeem_ticket(): atomic single-use burn of sso_tickets in the central
  DB, bound to PLATFORM_SLUG, then provision the local row + FIT_SESS session.
- Extract _sso_provision_local() shared with the cookie path.
- views/auth/login.php redeems the ticket before the cookie fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 22:17:40 +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