diff --git a/.htaccess b/.htaccess index 316b4b8..ddf23fe 100644 --- a/.htaccess +++ b/.htaccess @@ -58,6 +58,9 @@ RewriteRule ^site/assets/ - [L] RewriteRule ^assets/ - [L] # ── Friendly URLs ───────────────────────────────────────────────────────────── +# DEV ONLY: dev_login.php 404s sozinho se o platform-tools não tiver functions_dev.php. +RewriteCond %{REQUEST_FILENAME}.php -f +RewriteRule ^dev_login/?$ dev_login.php [L] RewriteRule ^home/?$ site/index.php [L] RewriteRule ^home/equipa/?$ site/equipa.php [L,QSA] RewriteRule ^home/modalidades/?$ site/modalidades.php [L,QSA] diff --git a/CLAUDE.md b/CLAUDE.md index f385e30..ca79486 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -132,3 +132,8 @@ or adapted for production, and these accounts must never exist in a production d ## Brand feed (2026-10-09) - `api/brand.php` is a one-line stub to `shared/platform-tools/api/brand.php` (public JSON: name, logos, colours; README "Brand feed"). Declared in `platform_connect(['brand' => ...])` in `config/db.php`; colours are the `:root` tokens (`--fitme-accent` #c2d500, `--fitme-dark` #0b0b0d, `--fitme-accent-dark` #a6b600). `logo_light` = light-coloured logo (`logo_crossfit_white.svg`), `logo_dark` = `logo_crossfit.svg`. - `site_logo_rel()` (config/functions.php) resolves logos for both the feed and `load_email_template()`: setting file in `uploads/public/logo/` (convention), then legacy `assets/img/logo/`, then the static logo. `views/sysadmin/system_settings.php` now uploads logos to `uploads/public/logo/` (dir created on demand, 0755; `uploads/.htaccess` blocks script execution); old files in `assets/img/logo/` still resolve as fallback and in the settings preview. Brand block in `config/db.php` already prefers the setting file via `site_logo_rel()`, else the static SVGs. + +## Dev toolbar partilhada (2026-10-09) +- DEV ONLY, do platform-tools (`functions_dev.php`, README "Dev tooling"; produção segue `main`, que não tem o ficheiro: tudo degrada para "não é dev"). +- Ficheiros: `config/dev_guard.php` (shim), `includes/dev_toolbar.php` (incluído no fim de `includes/footer.php`), `dev_login.php` (na RAIZ: o site não tem `public/`; `.htaccess` tem a regra `^dev_login/?$` com `-f`), `scripts/dev_accounts.php` (`platform_dev_accounts_run`). +- Contas `admin@mail.pt` / `sysadmin@mail.pt` (pw `123`). O login usa `auth_login()` + `fit_stamp_login_ua()` como o login real; CSRF do site = campo `_csrf` / `$_SESSION['csrf_token']` (verificação sem `exit`). diff --git a/config/dev_guard.php b/config/dev_guard.php new file mode 100644 index 0000000..f53cfd2 --- /dev/null +++ b/config/dev_guard.php @@ -0,0 +1,19 @@ + ['admin' => 'admin@mail.pt', 'sysadmin' => 'sysadmin@mail.pt'], + 'bootstrap' => function () { global $pdo; require __DIR__ . '/config/db.php'; require_once __DIR__ . '/config/functions.php'; }, + 'csrf_ok' => fn(): bool => is_string($_POST['_csrf'] ?? null) && is_string($_SESSION['csrf_token'] ?? null) + && $_SESSION['csrf_token'] !== '' && hash_equals($_SESSION['csrf_token'], $_POST['_csrf']), + 'login' => function (string $role, string $email): ?string { + global $pdo; + $st = $pdo->prepare('SELECT * FROM users WHERE email = ? AND role = ? AND is_active = 1 LIMIT 1'); + $st->execute([$email, $role]); + $user = $st->fetch(PDO::FETCH_ASSOC); + if (!$user) return null; // conta em falta: corra php scripts/dev_accounts.php + auth_login($pdo, $user); + fit_stamp_login_ua($pdo, (int) $user['id']); + return base_url(post_login_destination($role)); + }, +]); diff --git a/includes/dev_toolbar.php b/includes/dev_toolbar.php new file mode 100644 index 0000000..fdc4889 --- /dev/null +++ b/includes/dev_toolbar.php @@ -0,0 +1,11 @@ + base_url('dev_login'), + 'logout_url' => base_url('views/auth/logout.php'), + 'roles' => ['admin' => 'Admin', 'sysadmin' => 'Sysadmin'], + 'csrf_field' => 'csrf_field', +]); diff --git a/includes/footer.php b/includes/footer.php index b57fdee..bee17b4 100644 --- a/includes/footer.php +++ b/includes/footer.php @@ -105,5 +105,6 @@ main:after { margin-top: 2rem; } + diff --git a/scripts/dev_accounts.php b/scripts/dev_accounts.php index 936eb23..a94881e 100644 --- a/scripts/dev_accounts.php +++ b/scripts/dev_accounts.php @@ -1,81 +1,12 @@ 'users', 'id' => 'id', 'email' => 'email', 'name' => 'name', - 'password' => 'password_hash', 'role' => 'role', - 'flags' => ['is_active' => 1], // column => value that lets login work (insert AND update) - 'extra' => [], // column => value for new rows only; '{email}' is substituted - 'roles' => ['sysadmin@mail.pt' => 'sysadmin', 'admin@mail.pt' => 'admin'], -]; -// --------------------------------------------------------------------------- - -const DEV_PASSWORD = '123'; -function dev_fail(string $m): never { fwrite(STDERR, "REFUSED: $m\n"); exit(1); } - -$root = dirname(__DIR__); -$envFile = $root . '/.env'; -if (!is_file($envFile)) dev_fail('.env not found'); -$env = []; -foreach (file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $l) { - $l = trim($l); - if ($l === '' || $l[0] === '#' || !str_contains($l, '=')) continue; - [$k, $v] = explode('=', $l, 2); - $v = trim($v); - if ($v !== '' && $v[0] !== '"' && $v[0] !== "'" && ($h = strpos($v, ' #')) !== false) $v = rtrim(substr($v, 0, $h)); - $env[trim($k)] = trim($v, " \"'"); -} -foreach (array_keys($env) as $k) { if (($r = getenv($k)) !== false) $env[$k] = $r; } // real env wins, like the app - -$appEnv = strtolower($env['APP_ENV'] ?? ''); -if (!in_array($appEnv, ['dev', 'development', 'local'], true)) dev_fail('APP_ENV is not dev'); -$host = strtolower($env['DB_HOST'] ?? ''); $port = (int) ($env['DB_PORT'] ?? 3306); -$ok = in_array($host, ['localhost', '127.0.0.1', '::1'], true) || ($host === '192.168.1.3' && $port === 3181); -if (!$ok) dev_fail('DB host is not the dev server'); - -try { - $pdo = new PDO("mysql:host=$host;port=$port;dbname={$env['DB_NAME']};charset=utf8mb4", $env['DB_USER'] ?? '', $env['DB_PASS'] ?? '', - [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); -} catch (Throwable $e) { dev_fail('DB connect failed'); } - -$q = fn(string $c) => '`' . str_replace('`', '', $c) . '`'; -$cols = $pdo->prepare('SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?'); -$cols->execute([$T['table']]); -$have = $cols->fetchAll(PDO::FETCH_COLUMN); -$need = array_merge([$T['email'], $T['password'], $T['role']], array_keys($T['flags']), array_keys($T['extra'])); -if ($T['name']) $need[] = $T['name']; -$missing = array_diff($need, $have); -if (!$have) dev_fail("table {$T['table']} missing (schema behind)"); -if ($missing) dev_fail('schema behind, missing columns: ' . implode(', ', $missing)); - -$hash = password_hash(DEV_PASSWORD, PASSWORD_DEFAULT); -foreach ($T['roles'] as $email => $role) { - $set = [$T['password'] => $hash, $T['role'] => $role] + $T['flags']; - $s = $pdo->prepare("SELECT {$q($T['id'])} FROM {$q($T['table'])} WHERE {$q($T['email'])} = ? LIMIT 1"); - $s->execute([$email]); - $id = $s->fetchColumn(); - if ($id !== false) { - $sql = "UPDATE {$q($T['table'])} SET " . implode(', ', array_map(fn($c) => $q($c) . ' = ?', array_keys($set))) - . " WHERE {$q($T['id'])} = ?"; - $pdo->prepare($sql)->execute([...array_values($set), $id]); - echo "updated $email ($role)\n"; - } else { - $set[$T['email']] = $email; - if ($T['name']) $set[$T['name']] = ucfirst($role) . ' (dev)'; - foreach ($T['extra'] as $c => $v) $set[$c] = is_string($v) ? str_replace('{email}', $email, $v) : $v; - $sql = "INSERT INTO {$q($T['table'])} (" . implode(', ', array_map($q, array_keys($set))) . ') VALUES (' - . implode(', ', array_fill(0, count($set), '?')) . ')'; - $pdo->prepare($sql)->execute(array_values($set)); - echo "created $email ($role)\n"; - } -} +$_dev_shared = dirname(__DIR__) . '/shared/platform-tools/functions_dev.php'; +if (!is_file($_dev_shared)) { fwrite(STDERR, "REFUSED: platform-tools sem functions_dev.php\n"); exit(1); } +require_once $_dev_shared; +exit(platform_dev_accounts_run(['flags' => ['is_active' => 1]]));