config/db.php liga-se com 'auth' => 'shared' em vez de 'own': login, sessão, papéis e guardas passam a correr sobre shared/platform-tools/functions_auth.php (auth_attempt/auth_login/ auth_at_least/auth_is/auth_session_start/auth_remember_*) em vez da lógica local duplicada. Os ~30 ficheiros em views/ que chamam require_login()/require_admin()/require_staff()/require_sysadmin()/ is_admin()/etc. não mudam — só o que está por baixo dessas funções. - roles: staff=40, client=10 registados para preservar a hierarquia sysadmin>admin>staff>client que os antigos in_array() garantiam. `users` já tinha exactamente as colunas por omissão do auth_schema(), não foi preciso mapear nada. - sessão: o bloco manual de ini_set()/session_start() em config/db.php foi substituído pela opção 'session' do platform_connect() — mesmo nome de cookie (FIT_SESS), mesmo SameSite=Lax, secure/httponly passam a ser geridos por auth_session_start(). - «Manter sessão iniciada»: config/functions_remember.php (a reimplementação local do selector/validator) foi eliminado a favor do módulo partilhado (auth_remember_issue/resume/clear via 'remember' => true), que já cobria o mesmo desenho. - auth_attempts: tinha (ip, scope) do throttle próprio da app (config/functions_throttle.php), não o `identifier` que o throttle partilhado exige. Migração aditiva (migration_20260925_shared_auth_attempts.sql) torna a tabela conforme sem tocar no throttle da app, que continua a ser o único usado para login/registo/recuperação — o throttle partilhado não é chamado, para não duplicar a mesma defesa com duas contagens. - last_login_ua (sem equivalente partilhado) passa a ser gravado por fit_stamp_login_ua(), chamado a seguir a auth_login(). Testado ponta-a-ponta num contentor scratch na VPS (site-fitme:8.4, cópia da BD de produção + migração aplicada): visitante anónimo é redirigido, password errada dá o mesmo erro genérico e conta para o throttle (bloqueia à 5ª tentativa, 429), login correcto regenera a sessão, grava last_login_at/ip/ua, audita "auth.login" e (com "lembrar-me") emite o cookie am_remember; esse cookie sozinho, numa sessão nova, repõe a sessão certa com rotação do validator; logout limpa sessão+cookie+token e audita "auth.logout"; platform.php (guard=require_sysadmin) e views/admin/dashboard.php respondem 302 sem sessão e 200 com sessão de sysadmin. php -l limpo em todos os ficheiros alterados. Migração já aplicada em produção (aditiva). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
139 lines
9.7 KiB
PHP
139 lines
9.7 KiB
PHP
<?php
|
|
// ─── Bootstrap chain ─────────────────────────────────────────────────────────
|
|
// Loaded by every page right after config/db.php (which opens $pdo and starts
|
|
// the FIT_SESS session). Mirrors the fotografia/Andmore fleet shape, trimmed to
|
|
// what fitme3070 needs this pass: settings, generic CRUD, local auth, the
|
|
// migrations runner, and the centralized notify façade. Domain CRUD
|
|
// (plataforma_fitness, team, modalidades, horário, galerias) is added per
|
|
// feature in a follow-up under config/functions_*.php.
|
|
|
|
if (session_status() === PHP_SESSION_NONE) session_start();
|
|
|
|
require_once __DIR__ . '/config.php'; // APP_ENV + hub notify URL + error_handler
|
|
require_once __DIR__ . '/functions_settings.php'; // setting() + system_settings cache
|
|
require_once __DIR__ . '/functions_secrets.php'; // setting_or_env()/setting_secret() + AES-GCM at rest
|
|
require_once __DIR__ . '/functions_db.php'; // generic DB helpers
|
|
require_once __DIR__ . '/functions_audit.php'; // registo de auditoria (platform-tools) + audit()
|
|
require_once __DIR__ . '/functions_trash.php'; // lixeira: soft delete + eliminação só após 30 dias
|
|
require_once __DIR__ . '/functions_user.php'; // register/reset/user creators (auth itself: platform-tools)
|
|
require_once __DIR__ . '/auth.php'; // require_login/admin/staff/sysadmin(), role helpers
|
|
// «Manter sessão iniciada»: shared/platform-tools/functions_remember.php,
|
|
// ligado por platform_connect(['remember' => true]) em config/db.php — já não
|
|
// há um functions_remember.php local (auditoria 1.3).
|
|
require_once __DIR__ . '/functions_throttle.php'; // per-IP attempt throttle for the auth endpoints
|
|
require_once __DIR__ . '/functions_migrations.php';// platform_update.php migrations runner
|
|
|
|
// Outbound delivery happens in this app — PHPMailer over SMTP, no central relay.
|
|
require_once __DIR__ . '/functions_queue.php'; // message_queue + enqueue_*/flush_message
|
|
require_once __DIR__ . '/functions_csrf.php';
|
|
require_once __DIR__ . '/functions_html.php'; // allowlist HTML sanitiser (clean_richtext/clean_map_embed)
|
|
require_once __DIR__ . '/ci_compat.php'; // CI4 view-helper shim (script_tag/link_tag/view/esc/session)
|
|
require_once __DIR__ . '/functions_gyms.php'; // multi-gym: gyms, URL folders, admin gym switcher
|
|
require_once __DIR__ . '/functions_site.php'; // public-site data (site_info/offer/team/modalidades/horario)
|
|
require_once __DIR__ . '/functions_images.php'; // upload resize/compress (gallery/team/modalidades caps)
|
|
require_once __DIR__ . '/functions_team.php'; // team CRUD
|
|
require_once __DIR__ . '/functions_modalidades.php'; // modalidades CRUD
|
|
require_once __DIR__ . '/functions_galerias.php'; // galleries + images CRUD
|
|
require_once __DIR__ . '/functions_work_hours.php'; // work hours CRUD
|
|
require_once __DIR__ . '/functions_horario.php'; // class schedule CRUD
|
|
require_once __DIR__ . '/functions_events.php'; // time-limited events + landing pages + leads
|
|
require_once __DIR__ . '/functions_info.php'; // contactos (fitme3070_info) editor
|
|
require_once __DIR__ . '/functions_memberships.php'; // mensalidades + pedidos de inscrição
|
|
require_once __DIR__ . '/functions_reviews.php'; // reviews Google (Places API) + cache
|
|
require_once __DIR__ . '/functions_trials.php'; // pedidos de aula experimental
|
|
require_once __DIR__ . '/functions_privacy.php'; // mapa e fotos da Google só a pedido / servidos localmente
|
|
require_once __DIR__ . '/functions_forms.php'; // anti-spam + bloqueio de IP dos formulários públicos
|
|
|
|
// ─── DB-backed settings → legacy constants ───────────────────────────────────
|
|
// Resolved once from the settings cache so call sites read plain constants.
|
|
define('APP_NAME', (string) setting('app_name', 'FitMe 3070'));
|
|
// APP_BASE_URL is per-install (deployment topology). .env only — never the DB,
|
|
// so a reused dump can't drag another install's URL in.
|
|
// Auto-detect from the request if .env doesn't have it (dev/staging safety net).
|
|
$_aburl = env('APP_BASE_URL', '');
|
|
if ($_aburl === '' && isset($_SERVER['HTTP_HOST'])) {
|
|
$scriptDir = dirname($_SERVER['SCRIPT_NAME'] ?? '');
|
|
$_aburl = (request_is_https() ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST'] . rtrim($scriptDir, '/') . '/';
|
|
}
|
|
define('APP_BASE_URL', $_aburl);
|
|
define('EMAIL_FROM', (string) setting('email_from', 'noreply@andmore.pt'));
|
|
define('EMAIL_FROM_NAME', (string) setting('email_from_name', 'FitMe 3070'));
|
|
define('EMAIL_SUBJECT_PREFIX', (string) setting('email_subject_prefix', '[FitMe 3070] '));
|
|
|
|
// ─── SMTP ───────────────────────────────────────────────────────────
|
|
// .env wins, then system_settings (smtp_password encrypted at rest). These
|
|
// constants are what the diagnostics page shows; the actual send resolves
|
|
// through platform_smtp_config($pdo), which is DB-first — that is the value an
|
|
// operator can change from the backoffice without shell access.
|
|
// An empty SMTP_HOST makes PHPMailer fall back to PHP mail().
|
|
define('SMTP_HOST', setting_or_env('smtp_host', 'SMTP_HOST'));
|
|
define('SMTP_PORT', (int) (setting_or_env('smtp_port', 'SMTP_PORT', '587') ?: 587));
|
|
define('SMTP_SECURE', setting_or_env('smtp_secure', 'SMTP_SECURE', 'tls'));
|
|
define('SMTP_USERNAME', setting_or_env('smtp_username', 'SMTP_USERNAME'));
|
|
define('SMTP_PASSWORD', setting_secret('smtp_password', 'SMTP_PASSWORD'));
|
|
|
|
// ─── Twilio (SMS / WhatsApp) ───────────────────────────────────────
|
|
// Optional. Left empty the sms/whatsapp drivers refuse with a clear message
|
|
// instead of half-sending; email is unaffected.
|
|
define('TWILIO_SID', setting_or_env('twilio_sid', 'TWILIO_SID'));
|
|
define('TWILIO_TOKEN', setting_secret('twilio_token', 'TWILIO_TOKEN'));
|
|
define('TWILIO_SMS_FROM', setting_or_env('twilio_sms_from', 'TWILIO_SMS_FROM'));
|
|
define('TWILIO_WHATSAPP_FROM', setting_or_env('twilio_whatsapp_from', 'TWILIO_WHATSAPP_FROM'));
|
|
|
|
// ─── Dev safety nets ───────────────────────────────────────────────
|
|
// Only consulted when APP_ENV !== 'prod', so a misconfigured production install
|
|
// can never silently swallow mail.
|
|
// EMAIL_DEV_PRINT — skip SMTP entirely, write to logs/message_dev.log.
|
|
// EMAIL_DEV_REDIRECT — send everything to this address instead of the real one.
|
|
define('EMAIL_DEV_PRINT', filter_var(env('EMAIL_DEV_PRINT', 'false'), FILTER_VALIDATE_BOOLEAN));
|
|
define('EMAIL_DEV_REDIRECT', (string) env('EMAIL_DEV_REDIRECT', ''));
|
|
|
|
// ─── URL helper ───────────────────────────────────────────────────────────────
|
|
// Always use for Location: redirects and internal links.
|
|
function base_url(string $path = ''): string {
|
|
return APP_BASE_URL . ltrim($path, '/');
|
|
}
|
|
|
|
// ─── Date helper (PT-PT) ──────────────────────────────────────────────────────
|
|
function format_date_pt(string $date): string {
|
|
if (!$date) return '';
|
|
return (new DateTime($date))->format('d/m/Y');
|
|
}
|
|
|
|
// ─── Email template loader ────────────────────────────────────────────────────
|
|
// {{placeholder}} HTML bodies under templates/emails/. Built locally, delivered
|
|
// by id.andmore.pt via the enqueue_email()/flush_message() façade.
|
|
function load_email_template(string $template, array $vars): string {
|
|
$path = __DIR__ . '/../templates/emails/' . $template . '.html';
|
|
if (!is_file($path)) {
|
|
throw new RuntimeException("Email template not found: {$template}");
|
|
}
|
|
$html = file_get_contents($path);
|
|
$brand = htmlspecialchars(APP_NAME, ENT_QUOTES, 'UTF-8');
|
|
$logo = (string) setting('logo_light_filename', '');
|
|
$logoTag = $logo !== ''
|
|
? '<img src="' . htmlspecialchars(base_url('assets/img/logo/' . rawurlencode($logo)), ENT_QUOTES, 'UTF-8')
|
|
. '" alt="' . $brand . '" style="max-height:60px;border:0;">'
|
|
: '<p style="margin:0;color:#fff;font-size:22px;font-weight:bold;letter-spacing:1px;">' . $brand . '</p>';
|
|
$html = str_replace('{{logo_html}}', $logoTag, $html);
|
|
foreach ($vars as $key => $value) {
|
|
$html = str_replace('{{' . $key . '}}', htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'), $html);
|
|
}
|
|
return $html;
|
|
}
|
|
|
|
// ─── User-agent prettifier (last-login display) ──────────────────────────────
|
|
// Mirrors fotografia's helper so views/user/index.php can show a short label.
|
|
function parse_user_agent(string $ua): string {
|
|
if ($ua === '') return '—';
|
|
$browser = 'Navegador';
|
|
foreach (['Edg' => 'Edge', 'OPR' => 'Opera', 'Chrome' => 'Chrome', 'Firefox' => 'Firefox', 'Safari' => 'Safari'] as $needle => $label) {
|
|
if (stripos($ua, $needle) !== false) { $browser = $label; break; }
|
|
}
|
|
$os = 'SO';
|
|
foreach (['Windows' => 'Windows', 'Android' => 'Android', 'iPhone' => 'iOS', 'iPad' => 'iOS', 'Mac OS' => 'macOS', 'Linux' => 'Linux'] as $needle => $label) {
|
|
if (stripos($ua, $needle) !== false) { $os = $label; break; }
|
|
}
|
|
return "$browser / $os";
|
|
}
|