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>
This commit is contained in:
1 parent
dc8a36e126
commit
aec7b47820
23 files changed
+1195
-710
No files matched your search
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
// Closes the shell opened by includes/auth_header.php.
|
||||
?>
|
||||
</main>
|
||||
<script src="<?= base_url('assets/vendor/bootstrap5/js/bootstrap.bundle.min.js') ?>"></script>
|
||||
<script>
|
||||
// Show/hide toggles on any button carrying data-toggle-password="<input id>".
|
||||
document.querySelectorAll('[data-toggle-password]').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var input = document.getElementById(btn.getAttribute('data-toggle-password'));
|
||||
if (!input) return;
|
||||
var hidden = input.type === 'password';
|
||||
input.type = hidden ? 'text' : 'password';
|
||||
btn.setAttribute('aria-label', hidden ? 'Ocultar palavra-passe' : 'Mostrar palavra-passe');
|
||||
var icon = btn.querySelector('i');
|
||||
if (icon) icon.className = hidden ? 'bi bi-eye-slash' : 'bi bi-eye';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
// Anonymous-safe shell for the auth pages (login / register / forgot / reset /
|
||||
// setup). Deliberately NOT includes/header.php: that one renders the whole
|
||||
// backoffice chrome — sidebar, topbar, logout button — and calls current_user(),
|
||||
// none of which makes sense for a visitor who isn't signed in yet.
|
||||
//
|
||||
// Pair with includes/auth_footer.php. Optional $page_title sets the <title>.
|
||||
$page_title = $page_title ?? 'Entrar';
|
||||
$app_name = defined('APP_NAME') ? APP_NAME : 'FitMe 3070';
|
||||
?><!doctype html>
|
||||
<html lang="pt">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<title><?= htmlspecialchars($page_title) ?> — <?= htmlspecialchars($app_name) ?></title>
|
||||
<link href="<?= base_url('assets/vendor/bootstrap5/css/bootstrap.min.css') ?>" rel="stylesheet">
|
||||
<link href="<?= base_url('assets/vendor/bootstrap-icons/bootstrap-icons.css') ?>" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bo-accent: #C2D500;
|
||||
--bo-accent-rgb: 194,213,0;
|
||||
--bo-dark: #0a0a0a;
|
||||
}
|
||||
body {
|
||||
min-height: 100vh; margin: 0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
padding: 2rem 1rem;
|
||||
background: var(--bo-dark);
|
||||
background-image: radial-gradient(circle at 50% 0%, rgba(var(--bo-accent-rgb), .16), transparent 60%);
|
||||
font-size: .925rem; color: #f5f5f5;
|
||||
}
|
||||
.auth-card {
|
||||
width: 100%; max-width: 420px;
|
||||
background: #fff; color: #0a0a0a;
|
||||
border-radius: 14px; padding: 2rem;
|
||||
box-shadow: 0 18px 50px rgba(0,0,0,.45);
|
||||
}
|
||||
.auth-brand {
|
||||
text-align: center; font-weight: 700; font-size: 1.25rem;
|
||||
letter-spacing: .5px; margin-bottom: .25rem;
|
||||
}
|
||||
.auth-brand i { color: var(--bo-accent); }
|
||||
.auth-sub { text-align: center; color: #6c757d; font-size: .875rem; margin-bottom: 1.5rem; }
|
||||
.btn-primary { background: var(--bo-accent); border-color: var(--bo-accent); color: var(--bo-dark); font-weight: 600; }
|
||||
.btn-primary:hover, .btn-primary:focus { background: #a8ba00; border-color: #a8ba00; color: var(--bo-dark); }
|
||||
.form-control:focus { border-color: var(--bo-accent); box-shadow: 0 0 0 .2rem rgba(var(--bo-accent-rgb), .25); }
|
||||
.auth-foot { color: #6c757d; }
|
||||
.auth-foot a, .auth-card a { color: #6a7a00; }
|
||||
/* 16px inputs stop iOS from auto-zooming on focus */
|
||||
@media (max-width: 575.98px) { .form-control { font-size: 16px; } .auth-card { padding: 1.5rem; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="auth-card">
|
||||
<div class="auth-brand"><i class="bi bi-lightning-charge-fill me-1"></i><?= htmlspecialchars($app_name) ?></div>
|
||||
Reference in new issue
Block a user