Aquí NO vamos a “hacer otro proyecto”. Vamos a tomar tu estructura real:
index.php + /pages + /lib + /config
y le vamos a meter login/sesión/protección de páginas.
/pages) quedarán protegidas.password_hash / password_verify.En DBeaver ejecuta esto para agregar el campo del hash:
ALTER TABLE usuarios
ADD COLUMN password_hash VARCHAR(255) NULL;
/controllers/lib ya existe, ahí agregaremos auth.php/controllers, perfecto. Si no, la creas al mismo nivel donde está tu index.php.
Copia/pega exactamente estos archivos. Ya vienen con comentarios detallados.
Qué hace: Esta función se pone al inicio de cualquier página privada.
Si no hay sesión, redirige a login.php y corta el script.
<?php
// lib/auth.php
/**
* requireLogin()
* - Inicia sesión si aún no está iniciada
* - Verifica si existe $_SESSION['user_id']
* - Si NO existe, redirige a login.php y detiene el script (exit)
*/
function requireLogin()
{
// Sin session_start() no podemos leer $_SESSION
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Si no existe user_id => no está autenticado
if (!isset($_SESSION['user_id'])) {
header("Location: /login.php");
exit;
}
}
Qué hace: este es el “controlador” de autenticación. No es una vista: recibe la petición del form, valida, crea sesión y redirige.
?action=login → procesa el POST del formulario?action=logout → destruye sesión<?php
// controllers/AuthController.php
require_once __DIR__ . '/../lib/Conexion.php';
require_once __DIR__ . '/../lib/Functions.php';
// Necesitamos sesión porque guardaremos:
// - errores (login_error)
// - datos del usuario logueado (user_id, user_nombre, etc.)
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$action = $_GET['action'] ?? '';
// ==========================
// LOGIN
// ==========================
if ($action === 'login') {
// 1) Leer POST del formulario login.php
$correo = trim($_POST['correo'] ?? '');
$pass = trim($_POST['password'] ?? '');
// 2) Validación básica
if ($correo === '' || $pass === '') {
$_SESSION['login_error'] = "Faltan datos.";
header("Location: /login.php");
exit;
}
// 3) Buscar usuario por correo
$functions = new Functions();
// Nota didáctica: aquí concatenamos simple.
// Más adelante se ve prepared statements.
$get = $functions->execute("
SELECT id, nombre, correo, rol, activo, password_hash
FROM usuarios
WHERE correo = '$correo'
LIMIT 1
");
// 4) Si falló el SQL
if (($get['res'] ?? '') !== 'ok') {
$_SESSION['login_error'] = $get['res'] ?? 'Error desconocido';
header("Location: /login.php");
exit;
}
// 5) Si no encontró usuario
if ($get['query']->num_rows === 0) {
$_SESSION['login_error'] = "Usuario no encontrado.";
header("Location: /login.php");
exit;
}
// 6) Obtener fila
$user = $get['query']->fetch_assoc();
// 7) Validar activo
if ((int)$user['activo'] !== 1) {
$_SESSION['login_error'] = "Usuario inactivo.";
header("Location: /login.php");
exit;
}
// 8) Validar password con hash
$hash = $user['password_hash'] ?? '';
if ($hash === '' || !password_verify($pass, $hash)) {
$_SESSION['login_error'] = "Contraseña incorrecta.";
header("Location: /login.php");
exit;
}
// 9) Crear sesión (esto es “ya está logueado”)
$_SESSION['user_id'] = $user['id'];
$_SESSION['user_nombre'] = $user['nombre'];
$_SESSION['user_correo'] = $user['correo'];
$_SESSION['user_rol'] = $user['rol'];
unset($_SESSION['login_error']);
// 10) Ir al área interna
header("Location: /app.php");
exit;
}
// ==========================
// LOGOUT
// ==========================
if ($action === 'logout') {
session_unset();
session_destroy();
header("Location: /login.php");
exit;
}
// Si llega sin action válido:
header("Location: /login.php");
exit;
Qué hace: página pública con formulario.
Si el controlador dejó un error en $_SESSION['login_error'], aquí lo mostramos.
<?php
// login.php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Tomar error y borrarlo (para que no se quede pegado)
$err = $_SESSION['login_error'] ?? '';
unset($_SESSION['login_error']);
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Login</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-md-6 col-lg-5">
<div class="card shadow-sm">
<div class="card-body p-4">
<h3 class="mb-3 text-center">Iniciar sesión</h3>
<?php if ($err !== '') { ?>
<div class="alert alert-danger"><?php echo $err; ?></div>
<?php } ?>
<form method="post" action="/controllers/AuthController.php?action=login" class="row g-3">
<div class="col-12">
<label class="form-label">Correo</label>
<input type="email" name="correo" class="form-control" required>
</div>
<div class="col-12">
<label class="form-label">Contraseña</label>
<input type="password" name="password" class="form-control" required>
</div>
<div class="col-12 d-grid">
<button class="btn btn-primary" type="submit">Entrar</button>
</div>
<div class="col-12 text-center">
<a href="/index.php" class="text-decoration-none">Volver al inicio</a>
</div>
</form>
<hr>
<p class="text-muted small mb-0">
Para probar login necesitas un usuario con <code>password_hash</code> guardado en la BD.
</p>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
Qué hace: página privada.
Aquí es donde realmente se “controla” el acceso: al inicio se llama requireLogin().
<?php
// app.php
require_once __DIR__ . '/lib/auth.php';
requireLogin();
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Área interna</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container py-4">
<div class="card shadow-sm">
<div class="card-body">
<h3 class="mb-2">Área interna</h3>
<p class="text-muted">Si estás aquí, es porque hay sesión activa.</p>
<div class="alert alert-success">
Bienvenido: <strong><?php echo $_SESSION['user_nombre']; ?></strong><br>
Correo: <?php echo $_SESSION['user_correo']; ?><br>
Rol: <?php echo $_SESSION['user_rol']; ?>
</div>
<div class="d-flex gap-2">
<a class="btn btn-primary" href="/index.php">Ir al inicio</a>
<a class="btn btn-outline-danger" href="/logout.php">Cerrar sesión</a>
</div>
</div>
</div>
</div>
</body>
</html>
Qué hace: destruye la sesión y redirige a login.
<?php
// logout.php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
session_unset();
session_destroy();
header("Location: /login.php");
exit;
Este es tu index.php actual pero actualizado para:
mostrar botones según sesión y mostrar un aviso si hay sesión activa.
<?php
// index.php
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$logueado = isset($_SESSION['user_id']);
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Mini proyecto PHP + MySQL</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body class="bg-light">
<main class="container py-5">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h1 class="mb-1">Mini proyecto PHP + MySQL</h1>
<p class="text-muted mb-0">Accesos rápidos a ejercicios + login/sesión.</p>
</div>
<div class="d-flex gap-2">
<?php if (!$logueado) { ?>
<a class="btn btn-primary" href="login.php">Login</a>
<?php } else { ?>
<a class="btn btn-success" href="app.php">App</a>
<a class="btn btn-outline-danger" href="logout.php">Logout</a>
<?php } ?>
</div>
</div>
<?php if ($logueado) { ?>
<div class="alert alert-success">
Sesión activa: <strong><?php echo $_SESSION['user_nombre']; ?></strong>
(<?php echo $_SESSION['user_correo']; ?>) — Rol: <?php echo $_SESSION['user_rol']; ?>
</div>
<?php } else { ?>
<div class="alert alert-secondary">
No hay sesión iniciada. Entra a <strong>Login</strong> para acceder al área interna.
</div>
<?php } ?>
<div class="row g-3">
<div class="col-md-6">
<div class="card h-100 shadow-sm">
<div class="card-body">
<span class="badge text-bg-primary mb-2">SELECT</span>
<h5 class="card-title">Listado de usuarios</h5>
<p class="text-muted">Página protegida (pedirá login).</p>
<a href="pages/usuarios_list.php" class="btn btn-primary">Ver usuarios</a>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100 shadow-sm">
<div class="card-body">
<span class="badge text-bg-warning mb-2">GET</span>
<h5 class="card-title">Crear usuario (GET)</h5>
<p class="text-muted">Página protegida (pedirá login).</p>
<a href="pages/usuario_form_get.php" class="btn btn-warning">Abrir formulario GET</a>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100 shadow-sm">
<div class="card-body">
<span class="badge text-bg-success mb-2">POST</span>
<h5 class="card-title">Crear venta (POST)</h5>
<p class="text-muted">Página protegida (pedirá login).</p>
<a href="pages/venta_form_post.php" class="btn btn-success">Abrir formulario POST</a>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card h-100 shadow-sm">
<div class="card-body">
<span class="badge text-bg-info mb-2">Async</span>
<h5 class="card-title">Guardar venta async</h5>
<p class="text-muted">Se usa dentro del form de ventas (POST).</p>
<a href="pages/venta_form_post.php" class="btn btn-info text-white">Ir a ventas</a>
</div>
</div>
</div>
</div>
</main>
</body>
</html>
Aquí está el punto que te faltaba: no es “a ver tú decide”. Nosotros vamos a proteger TODAS tus páginas funcionales para que el proyecto “jale” con login.
require_once __DIR__ . '/../lib/auth.php';
requireLogin();
Archivos a modificar (todos en /pages):
Qué haces: lo pegas al inicio de cada archivo en /pages.
require_once __DIR__ . '/../lib/auth.php';
requireLogin();
venta_save_async.php no conviene redirigir (porque lo consume fetch).
Ahí, si no hay sesión, regresamos JSON con ok=false.
<?php
// pages/venta_save_async.php
header('Content-Type: application/json; charset=utf-8');
// Para endpoints async: iniciamos sesión y validamos.
// Si no hay sesión NO redirigimos (fetch no entiende eso bien),
// respondemos JSON con ok=false.
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
if (!isset($_SESSION['user_id'])) {
echo json_encode(["ok" => false, "msg" => "No autorizado. Inicia sesión."]);
exit;
}
require_once __DIR__ . '/../lib/Conexion.php';
require_once __DIR__ . '/../lib/Functions.php';
$functions = new Functions();
$usuario_id = (int)($_POST['usuario_id'] ?? 0);
$producto_id = (int)($_POST['producto_id'] ?? 0);
$cantidad = (int)($_POST['cantidad'] ?? 0);
if ($usuario_id <= 0 || $producto_id <= 0 || $cantidad <= 0) {
echo json_encode(["ok" => false, "msg" => "Faltan datos válidos."]);
exit;
}
$getPrecio = $functions->execute("SELECT precio FROM productos WHERE id = $producto_id LIMIT 1");
if ($getPrecio['res'] !== 'ok') {
echo json_encode(["ok" => false, "msg" => $getPrecio['res']]);
exit;
}
if ($getPrecio['query']->num_rows === 0) {
echo json_encode(["ok" => false, "msg" => "Producto no encontrado."]);
exit;
}
$row = $getPrecio['query']->fetch_assoc();
$precio = (float)$row['precio'];
$total = $precio * $cantidad;
$ins = $functions->execute("
INSERT INTO ventas (usuario_id, producto_id, cantidad, total, fecha)
VALUES ($usuario_id, $producto_id, $cantidad, $total, NOW())
");
if ($ins['res'] === 'ok') {
echo json_encode(["ok" => true, "msg" => "Venta guardada. ID: " . $ins['lastId']]);
} else {
echo json_encode(["ok" => false, "msg" => $ins['res']]);
}
Para probar login necesitas un usuario existente con un hash guardado. Hacemos 2 pasos: generar hash y actualizarlo en la BD.
A) Generar hash (PHP temporal):
<?php
echo password_hash("123456", PASSWORD_DEFAULT);
B) Pegarlo en un UPDATE (DBeaver):
UPDATE usuarios
SET password_hash = 'PEGA_AQUI_EL_HASH'
WHERE correo = 'ana.lopez@mail.com';
/login.php con ese correo + 123456./app.php./pages/usuarios_list.php: debe dejarte (porque ya hay sesión)./pages/usuarios_list.php: debe mandarte a login.