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>
This commit is contained in:
andreandClaude Opus 4.8 committed 2026-06-12 10:44:58 +01:00
commit e68d357177
2330 files changed
+215968

No files matched your search

+10
View File
@@ -0,0 +1,10 @@
<?php
// Password recovery lives on id.andmore.pt — every local form/handler was
// removed when this install became SSO-only. This file is a bounce-only stub.
require __DIR__ . '/../../config/db.php';
require_once __DIR__ . '/../../config/functions.php';
$return_to = base_url('views/auth/login.php');
header('Location: ' . ANDMORE_ID_URL . 'auth/forgot_password.php?return_to=' . urlencode($return_to));
exit;
+69
View File
@@ -0,0 +1,69 @@
<?php
// SSO-only platform: there is no local login form. All credentials live on
// id.andmore.pt. This file exists solely so old links / bookmarks pointing
// at /views/auth/login.php still land the visitor on the central form.
//
// Also acts as the post-login landing for the id.andmore.pt return_to: the
// am_sso cookie is now in the browser, sso_resolve_session() picks it up,
// and we forward to the dashboard. The `?from=sso` flag prevents an infinite
// loop if resolution fails here — we render a clean error instead of
// bouncing back to id.andmore.pt (which would just send us back here again).
//
// Carries through the optional claim intent (?next=session&session_id=X)
// from the homepage "vagas disponíveis" cards so that after authenticating
// the user lands on their dashboard scrolled to the open session.
require __DIR__ . '/../../config/db.php';
require_once __DIR__ . '/../../config/functions.php';
// Visitor may already have an am_sso cookie — resolve it instead of bouncing
// (saves a round-trip).
if (empty($_SESSION['user_id']) && identity_is_federated()) {
sso_resolve_session($pdo);
}
$next = $_GET['next'] ?? '';
$session_id = (int) ($_GET['session_id'] ?? 0);
$claim_intent = ($next === 'session' && $session_id > 0);
$from_sso = isset($_GET['from']) && $_GET['from'] === 'sso';
$dashboard_for = function (string $role) use ($claim_intent, $session_id): string {
if ($claim_intent && $role === 'client') {
return 'views/user/dashboard.php?focus_session=' . $session_id;
}
return post_login_destination($role);
};
if (!empty($_SESSION['user_id'])) {
header('Location: ' . base_url($dashboard_for($_SESSION['user']['role'] ?? '')));
exit;
}
// We came back from id.andmore.pt but sso_resolve_session() couldn't pick up
// a valid session. Bouncing to id.andmore.pt again would loop forever — show
// a clean error instead so the user can see what's wrong.
if ($from_sso) {
http_response_code(500);
$am_cookie_present = !empty($_COOKIE[SSO_COOKIE_NAME] ?? '');
?><!doctype html><html lang="pt"><head><meta charset="utf-8"><title>Erro de autenticação</title>
<style>body{font-family:system-ui,-apple-system,sans-serif;max-width:560px;margin:80px auto;padding:0 1.5rem;color:#1c1c1a;background:#f7f5f1;line-height:1.6}h1{font-weight:300;font-size:1.6rem;margin-bottom:.5rem}code{background:rgba(0,0,0,.05);padding:2px 6px;border-radius:3px;font-size:.9em}.muted{color:#6b6863;font-size:.9rem}a{color:#5a7a62}</style>
</head><body>
<h1>Não foi possível concluir o login</h1>
<p>O servidor Andmore ID autenticou-o, mas esta plataforma não conseguiu carregar a sua sessão.</p>
<p class="muted">
Cookie <code>am_sso</code>: <?= $am_cookie_present ? 'presente' : '<strong>em falta</strong>' ?>.<br>
Plataforma: <code><?= htmlspecialchars(PLATFORM_SLUG) ?></code> · Modo: <code><?= htmlspecialchars(IDENTITY_MODE) ?></code>.
</p>
<p><a href="<?= htmlspecialchars(base_url('index.php')) ?>">Voltar ao início</a> · <a href="<?= htmlspecialchars(base_url('views/auth/logout.php')) ?>">Tentar novamente</a></p>
</body></html><?php
exit;
}
// First arrival — bounce to id.andmore.pt for credentials, tagging the
// return URL with ?from=sso so the loop-guard above fires on the way back.
$return_to = base_url('views/auth/login.php?from=sso'
. ($claim_intent ? '&next=session&session_id=' . $session_id : ''));
$_login_qs = '?return_to=' . urlencode($return_to) . '&platform=' . urlencode(PLATFORM_SLUG);
header('Location: ' . ANDMORE_ID_URL . 'auth/login.php' . $_login_qs);
exit;
+26
View File
@@ -0,0 +1,26 @@
<?php
require __DIR__ . '/../../config/db.php';
require __DIR__ . '/../../config/functions.php';
// Always clear the local session first.
$_SESSION = [];
session_destroy();
// Federated mode: always bounce through Andmore ID's logout endpoint so both
// the central sso_sessions row and the am_sso cookie are destroyed AND
// id.andmore.pt's own PHP session is cleared. The old check for the am_sso
// cookie was a bug: if the cookie happened to be absent from $_COOKIE (e.g.
// due to a Secure-flag or SameSite edge case), the central logout was skipped,
// leaving id.andmore.pt's session alive. That session then caused a redirect
// loop — id.andmore.pt auto-logged the user in and bounced them back to the
// login stub, which immediately bounced them back to id.andmore.pt, ad inf.
//
// Return to the public landing page (not the login stub) so the post-logout
// redirect from id.andmore.pt lands on a real page and the chain stops.
if (identity_is_federated()) {
header('Location: ' . ANDMORE_ID_URL . 'auth/logout.php?return_to=' . urlencode(APP_BASE_URL));
exit;
}
header('Location: ' . base_url('views/auth/login.php'));
exit;
+35
View File
@@ -0,0 +1,35 @@
<?php
// SSO-only platform: there is no local registration form. Accounts live on
// id.andmore.pt and are mirrored locally by sso_resolve_session() the first
// time the user lands on this platform. This file exists only so old
// /views/auth/register.php links bounce through to the central registration.
require __DIR__ . '/../../config/db.php';
require_once __DIR__ . '/../../config/functions.php';
// Already authenticated? Skip the bounce and land on the right dashboard.
if (empty($_SESSION['user_id']) && identity_is_federated()) {
sso_resolve_session($pdo);
}
$next = $_GET['next'] ?? '';
$session_id = (int) ($_GET['session_id'] ?? 0);
$claim_intent = ($next === 'session' && $session_id > 0);
$dashboard_for = function (string $role) use ($claim_intent, $session_id): string {
if ($claim_intent && $role === 'client') {
return 'views/user/dashboard.php?focus_session=' . $session_id;
}
return post_login_destination($role);
};
if (!empty($_SESSION['user_id'])) {
header('Location: ' . base_url($dashboard_for($_SESSION['user']['role'] ?? '')));
exit;
}
$return_to = base_url('views/auth/login.php'
. ($claim_intent ? '?next=session&session_id=' . $session_id : ''));
header('Location: ' . ANDMORE_ID_URL . 'auth/register.php?return_to=' . urlencode($return_to));
exit;
+10
View File
@@ -0,0 +1,10 @@
<?php
// Password reset happens on id.andmore.pt — bounce anyone landing here with
// an old reset link so they don't get a "page not working" experience.
require __DIR__ . '/../../config/db.php';
require_once __DIR__ . '/../../config/functions.php';
header('Location: ' . ANDMORE_ID_URL . 'auth/forgot_password.php?return_to='
. urlencode(base_url('views/auth/login.php')));
exit;
+86
View File
@@ -0,0 +1,86 @@
<?php
require __DIR__ . '/../../config/db.php';
require __DIR__ . '/../../config/functions.php';
$error = '';
$success = '';
// Self-locks after success by writing system_settings.setup_completed_at —
// relying only on "is there a sysadmin?" left the door open if anyone ever
// truncated the users table.
$setupDoneAt = (string) setting('setup_completed_at', '');
$alreadyDone = ($setupDoneAt !== '' || admin_exists($pdo));
if ($alreadyDone) {
$success = 'Já existe uma conta de administrador. Esta página já não é necessária — elimine views/auth/setup.php.';
} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') {
csrf_check();
$name = trim($_POST['name'] ?? '');
$email = trim($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
$confirm = $_POST['confirm'] ?? '';
if (!$name || !$email || !$password) {
$error = 'Todos os campos são obrigatórios.';
} elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$error = 'Endereço de email inválido.';
} elseif (strlen($password) < 8) {
$error = 'A palavra-passe deve ter pelo menos 8 caracteres.';
} elseif ($password !== $confirm) {
$error = 'As palavras-passe não coincidem.';
} else {
$newId = auth_create_sysadmin($pdo, $name, $email, $password);
// Latch the gate so a future `users` truncate can't re-open setup.
db_update_setting($pdo, 'setup_completed_at', date('Y-m-d H:i:s'), $newId);
// Best-effort self-destruct.
@unlink(__FILE__);
$alreadyDone = true;
$success = 'Conta de sysadmin criada. <strong>Elimine este ficheiro (views/auth/setup.php) agora.</strong> <a href="' . base_url('views/auth/login.php') . '">Ir para o login &rarr;</a>';
}
}
include __DIR__ . '/../../includes/header.php';
?>
<div class="container" style="max-width:480px; margin-top:80px;">
<div class="card shadow-sm">
<div class="card-body p-4">
<h3 class="mb-1">Configuração inicial</h3>
<p class="text-muted small mb-4">Crie a conta de administrador. Elimine este ficheiro depois.</p>
<?php if ($error): ?>
<div class="alert alert-danger"><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<?php if ($success): ?>
<div class="alert alert-success"><?= $success ?></div>
<?php elseif (!$alreadyDone): ?>
<form method="POST">
<?= csrf_field() ?>
<div class="mb-3">
<label class="form-label" for="setup-name">Nome</label>
<input type="text" id="setup-name" name="name" class="form-control" required
autocomplete="name"
value="<?= htmlspecialchars($_POST['name'] ?? '') ?>">
</div>
<div class="mb-3">
<label class="form-label" for="setup-email">Email</label>
<input type="email" id="setup-email" name="email" class="form-control" required
autocomplete="email" inputmode="email"
value="<?= htmlspecialchars($_POST['email'] ?? '') ?>">
</div>
<div class="mb-3">
<label class="form-label" for="setup-password">Palavra-passe <span class="text-muted small">(mín. 8 caracteres)</span></label>
<input type="password" id="setup-password" name="password" class="form-control" required minlength="8"
autocomplete="new-password">
</div>
<div class="mb-4">
<label class="form-label" for="setup-confirm">Confirmar palavra-passe</label>
<input type="password" id="setup-confirm" name="confirm" class="form-control" required
autocomplete="new-password">
</div>
<button type="submit" class="btn btn-primary w-100">Criar conta de administrador</button>
</form>
<?php endif; ?>
</div>
</div>
</div>
<?php include __DIR__ . '/../../includes/footer.php'; ?>