Explorar el Código

Logins Reaady

main
Daniel Covington hace 1 semana
padre
commit
1d88c48df6
Se han modificado 14 ficheros con 2293 adiciones y 8 borrados
  1. +11
    -1
      .claude/settings.local.json
  2. +71
    -0
      app/Controllers/AuthController.php
  3. +52
    -0
      app/Views/auth/login.php
  4. +13
    -0
      app/Views/partials/header.php
  5. +21
    -0
      bootstrap/sentinel.php
  6. +7
    -1
      composer.json
  7. +1828
    -3
      composer.lock
  8. +66
    -0
      config/sentinel.php
  9. +5
    -3
      core/helpers.php
  10. +99
    -0
      database/migrations/20260517_000001_create_sentinel_tables.php
  11. +94
    -0
      public/css/site.css
  12. +2
    -0
      public/index.php
  13. +5
    -0
      routes/web.php
  14. +19
    -0
      scripts/create_user.php

+ 11
- 1
.claude/settings.local.json Ver fichero

@@ -4,7 +4,17 @@
"Bash(Get-ChildItem -Path \"c:\\\\Development\\\\PHP\\\\PHP-MVC-TERRITORY\" -Force)",
"Bash(Select-Object Mode, Name)",
"Bash(Format-Table -AutoSize)",
"PowerShell(Get-ChildItem -Path \"c:\\\\Development\\\\PHP\\\\PHP-MVC-TERRITORY\" -Force | Where-Object {$_.Name -match '^[A-Z]'} | Select-Object Mode, Name)"
"PowerShell(Get-ChildItem -Path \"c:\\\\Development\\\\PHP\\\\PHP-MVC-TERRITORY\" -Force | Where-Object {$_.Name -match '^[A-Z]'} | Select-Object Mode, Name)",
"Bash(Get-ChildItem -Recurse -Depth 3)",
"Bash(Select-Object -Property FullName, PSIsContainer)",
"PowerShell(Get-ChildItem -Path \"c:\\\\Development\\\\PHP\\\\PHP-MVC-TERRITORY\" -Recurse -Depth 3 | Select-Object -Property @{Name=\"Path\";Expression={$_.FullName.Substring\\(28\\)}}, PSIsContainer | Format-Table -AutoSize)",
"PowerShell(Get-ChildItem -Path \"c:\\\\Development\\\\PHP\\\\PHP-MVC-TERRITORY\" -Depth 1 | Where-Object { $_.PSIsContainer } | ForEach-Object { $_.Name })",
"Bash(composer install *)",
"PowerShell(docker exec php-mvc-territory-app-1 cat /var/www/html/vendor/cartalyst/sentinel/src/Native/SentinelBootstrapper.php 2>&1)",
"PowerShell(docker exec php-mvc-territory-app-1 cat /var/www/html/vendor/cartalyst/sentinel/src/Native/Facades/Sentinel.php 2>&1)",
"PowerShell(docker exec php-mvc-territory-app-1 cat /var/www/html/vendor/cartalyst/sentinel/src/Native/ConfigRepository.php 2>&1)",
"PowerShell(docker exec php-mvc-territory-app-1 cat /var/www/html/vendor/cartalyst/sentinel/src/config/config.php 2>&1)",
"Bash(docker-compose exec app bash -c ' *)"
]
}
}

+ 71
- 0
app/Controllers/AuthController.php Ver fichero

@@ -0,0 +1,71 @@
<?php

declare(strict_types=1);

namespace App\Controllers;

use Core\Controller;
use Core\Request;
use Core\Response;
use Cartalyst\Sentinel\Native\Facades\Sentinel;
use Cartalyst\Sentinel\Checkpoints\ThrottlingException;

class AuthController extends Controller
{
public function showLogin(): Response
{
if (Sentinel::check()) {
return $this->redirect('/');
}

return $this->view('auth.login', ['pageTitle' => 'Login']);
}

public function login(): Response
{
$request = Request::capture();

if (!verify_csrf_token($request->input('_token'))) {
return $this->view('auth.login', [
'pageTitle' => 'Login',
'error' => 'Invalid request. Please try again.',
]);
}

$credentials = [
'email' => (string) $request->input('email'),
'password' => (string) $request->input('password'),
];

$remember = (bool) $request->input('remember');

try {
if (Sentinel::authenticate($credentials, $remember)) {
return $this->redirect('/');
}
} catch (ThrottlingException $e) {
return $this->view('auth.login', [
'pageTitle' => 'Login',
'error' => 'Too many failed attempts. Please wait ' . $e->getDelay() . ' seconds.',
]);
}

return $this->view('auth.login', [
'pageTitle' => 'Login',
'error' => 'Invalid email or password.',
]);
}

public function logout(): Response
{
$request = Request::capture();

if (!verify_csrf_token($request->input('_token'))) {
return $this->redirect('/');
}

Sentinel::logout();

return $this->redirect('/login');
}
}

+ 52
- 0
app/Views/auth/login.php Ver fichero

@@ -0,0 +1,52 @@
<div class="auth-wrap">
<div class="auth-card">
<div class="auth-card-header">
<span class="eyebrow">Welcome back</span>
<h1 class="auth-card-title">Sign in</h1>
<p class="auth-card-subtitle">Enter your credentials to access your account.</p>
</div>

<?php if (!empty($error)): ?>
<div class="alert alert-error" role="alert">
<?= e($error) ?>
</div>
<?php endif; ?>

<form class="auth-form" method="POST" action="/login" novalidate>
<?= csrf_field() ?>

<div class="field">
<label for="email">Email address</label>
<input
class="input"
type="email"
id="email"
name="email"
value="<?= e($_POST['email'] ?? '') ?>"
autocomplete="email"
required
autofocus
>
</div>

<div class="field">
<label for="password">Password</label>
<input
class="input"
type="password"
id="password"
name="password"
autocomplete="current-password"
required
>
</div>

<div class="auth-remember">
<input type="checkbox" id="remember" name="remember" value="1">
<label for="remember">Keep me signed in</label>
</div>

<button class="button button-primary auth-submit" type="submit">Sign in</button>
</form>
</div>
</div>

+ 13
- 0
app/Views/partials/header.php Ver fichero

@@ -2,6 +2,8 @@

declare(strict_types=1);

use Cartalyst\Sentinel\Native\Facades\Sentinel;

$navigationItems = [
['label' => 'Home', 'href' => '/'],
['label' => 'Example JSON', 'href' => '/users/123'],
@@ -9,6 +11,8 @@ $navigationItems = [

$currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
$currentPath = is_string($currentPath) && $currentPath !== '' ? $currentPath : '/';

$currentUser = Sentinel::check();
?>
<!DOCTYPE html>
<html lang="en">
@@ -37,6 +41,15 @@ $currentPath = is_string($currentPath) && $currentPath !== '' ? $currentPath : '
<?= e($item['label']) ?>
</a>
<?php endforeach; ?>

<?php if ($currentUser): ?>
<form class="auth-nav-form" method="POST" action="/logout">
<?= csrf_field() ?>
<button class="auth-nav-btn" type="submit">Sign out</button>
</form>
<?php else: ?>
<a class="nav-link<?= $currentPath === '/login' ? ' is-active' : '' ?>" href="/login">Sign in</a>
<?php endif; ?>
</nav>
</div>
</header>

+ 21
- 0
bootstrap/sentinel.php Ver fichero

@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

use Illuminate\Database\Capsule\Manager as Capsule;
use Cartalyst\Sentinel\Native\Facades\Sentinel;
use Cartalyst\Sentinel\Native\SentinelBootstrapper;

prepareSqliteDatabase('sqlite:' . __DIR__ . '/../database/app.sqlite');

$capsule = new Capsule();
$capsule->addConnection([
'driver' => 'sqlite',
'database' => realpath(__DIR__ . '/../database/app.sqlite') ?: __DIR__ . '/../database/app.sqlite',
'prefix' => '',
]);
$capsule->setAsGlobal();
$capsule->bootEloquent();

$bootstrapper = new SentinelBootstrapper(__DIR__ . '/../config/sentinel.php');
Sentinel::instance($bootstrapper);

+ 7
- 1
composer.json Ver fichero

@@ -18,5 +18,11 @@
"migrate:fresh": "php scripts/migrate.php fresh",
"migrate:fresh-seed": "php scripts/migrate.php fresh --seed"
},
"require": {}
"require": {
"php": ">=8.2",
"cartalyst/sentinel": "^7.0",
"illuminate/database": "^10.0",
"illuminate/events": "^10.0",
"symfony/http-foundation": "^6.0"
}
}

+ 1828
- 3
composer.lock
La diferencia del archivo ha sido suprimido porque es demasiado grande
Ver fichero


+ 66
- 0
config/sentinel.php Ver fichero

@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

return [
'session' => 'cartalyst_sentinel',

'cookie' => 'cartalyst_sentinel',

'users' => [
'model' => 'Cartalyst\Sentinel\Users\EloquentUser',
],

'roles' => [
'model' => 'Cartalyst\Sentinel\Roles\EloquentRole',
],

'permissions' => [
'class' => 'Cartalyst\Sentinel\Permissions\StandardPermissions',
],

'persistences' => [
'model' => 'Cartalyst\Sentinel\Persistences\EloquentPersistence',
'single' => false,
],

// Only throttle is enabled; activation can be re-added when a registration flow exists.
'checkpoints' => [
'throttle',
],

'activations' => [
'model' => 'Cartalyst\Sentinel\Activations\EloquentActivation',
'expires' => 259200,
'lottery' => [2, 100],
],

'reminders' => [
'model' => 'Cartalyst\Sentinel\Reminders\EloquentReminder',
'expires' => 14400,
'lottery' => [2, 100],
],

'throttling' => [
'model' => 'Cartalyst\Sentinel\Throttling\EloquentThrottle',
'global' => [
'interval' => 900,
'thresholds' => [
10 => 1,
20 => 2,
30 => 4,
40 => 8,
50 => 16,
60 => 32,
],
],
'ip' => [
'interval' => 900,
'thresholds' => 5,
],
'user' => [
'interval' => 900,
'thresholds' => 5,
],
],
];

+ 5
- 3
core/helpers.php Ver fichero

@@ -94,9 +94,11 @@ function prepareSqliteDatabase(string $dsn): void
}
}

function e(?string $value): string
{
return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
if (!function_exists('e')) {
function e(?string $value): string
{
return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
}

function asset(string $path): string


+ 99
- 0
database/migrations/20260517_000001_create_sentinel_tables.php Ver fichero

@@ -0,0 +1,99 @@
<?php

declare(strict_types=1);

use Core\Database;
use Core\Migration;

return new class extends Migration
{
public function up(Database $database): void
{
$database->execute('
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
permissions TEXT,
last_login DATETIME,
first_name VARCHAR(255),
last_name VARCHAR(255),
created_at DATETIME,
updated_at DATETIME
)
');

$database->execute('
CREATE TABLE IF NOT EXISTS roles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slug VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
permissions TEXT,
created_at DATETIME,
updated_at DATETIME
)
');

$database->execute('
CREATE TABLE IF NOT EXISTS role_users (
user_id INTEGER NOT NULL,
role_id INTEGER NOT NULL,
created_at DATETIME,
updated_at DATETIME,
PRIMARY KEY (user_id, role_id)
)
');

$database->execute('
CREATE TABLE IF NOT EXISTS persistences (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
code VARCHAR(255) NOT NULL UNIQUE,
created_at DATETIME,
updated_at DATETIME
)
');

$database->execute('
CREATE TABLE IF NOT EXISTS activations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
code VARCHAR(255) NOT NULL,
completed INTEGER NOT NULL DEFAULT 0,
completed_at DATETIME,
created_at DATETIME,
updated_at DATETIME
)
');

$database->execute('
CREATE TABLE IF NOT EXISTS reminders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
code VARCHAR(255) NOT NULL,
completed INTEGER NOT NULL DEFAULT 0,
completed_at DATETIME,
created_at DATETIME,
updated_at DATETIME
)
');

$database->execute('
CREATE TABLE IF NOT EXISTS throttle (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
type VARCHAR(255) NOT NULL,
ip VARCHAR(255),
created_at DATETIME,
updated_at DATETIME
)
');
}

public function down(Database $database): void
{
foreach (['throttle', 'reminders', 'activations', 'persistences', 'role_users', 'roles', 'users'] as $table) {
$database->execute("DROP TABLE IF EXISTS {$table}");
}
}
};

+ 94
- 0
public/css/site.css Ver fichero

@@ -466,3 +466,97 @@ code {
font-size: 2.5rem;
}
}

/* ── Auth / Login ─────────────────────────────────────── */

.auth-wrap {
display: flex;
justify-content: center;
align-items: flex-start;
padding: 2rem 0;
}

.auth-card {
width: 100%;
max-width: 460px;
background: var(--surface);
border: 1px solid var(--surface-border);
box-shadow: var(--shadow-card);
border-radius: 2rem;
padding: 2.5rem 2.5rem 2rem;
display: grid;
gap: 1.5rem;
}

.auth-card-header {
display: grid;
gap: 0.4rem;
}

.auth-card-title {
margin: 0;
font-size: 2rem;
letter-spacing: -0.04em;
line-height: 1.1;
}

.auth-card-subtitle {
margin: 0;
color: var(--text-secondary);
font-size: 0.97rem;
line-height: 1.6;
}

.auth-form {
display: grid;
gap: 1.1rem;
}

.auth-remember {
display: flex;
align-items: center;
gap: 0.55rem;
font-size: 0.93rem;
color: var(--text-secondary);
cursor: pointer;
}

.auth-remember input[type="checkbox"] {
width: 1rem;
height: 1rem;
accent-color: var(--accent);
cursor: pointer;
}

.auth-submit {
width: 100%;
justify-content: center;
padding: 0.95rem;
font-size: 1rem;
margin-top: 0.25rem;
}

.auth-nav-form {
display: inline;
margin: 0;
padding: 0;
}

.auth-nav-btn {
background: none;
border: none;
padding: 0.7rem 1rem;
font: inherit;
font-weight: 600;
color: var(--text-secondary);
border-radius: 999px;
cursor: pointer;
transition: background-color 160ms ease, color 160ms ease, transform 160ms ease;
}

.auth-nav-btn:hover,
.auth-nav-btn:focus-visible {
color: var(--accent-strong);
background: rgba(29, 122, 109, 0.12);
transform: translateY(-1px);
}

+ 2
- 0
public/index.php Ver fichero

@@ -11,6 +11,8 @@ use Core\Router;

ensureSessionStarted();

require_once __DIR__ . '/../bootstrap/sentinel.php';

$app = new App();
$router = new Router();



+ 5
- 0
routes/web.php Ver fichero

@@ -3,6 +3,11 @@
declare(strict_types=1);

use App\Controllers\HomeController;
use App\Controllers\AuthController;

$router->get('/', [HomeController::class, 'index']);
$router->get('/users/{id}', [HomeController::class, 'user']);

$router->get('/login', [AuthController::class, 'showLogin']);
$router->post('/login', [AuthController::class, 'login']);
$router->post('/logout', [AuthController::class, 'logout']);

+ 19
- 0
scripts/create_user.php Ver fichero

@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

require __DIR__ . '/../vendor/autoload.php';

ensureSessionStarted();
require __DIR__ . '/../bootstrap/sentinel.php';

use Cartalyst\Sentinel\Native\Facades\Sentinel;

$user = Sentinel::registerAndActivate([
'email' => 'admin@example.com',
'password' => 'secret123',
'first_name' => 'Admin',
'last_name' => 'User',
]);

echo $user ? 'User created: ' . $user->email . PHP_EOL : 'Failed' . PHP_EOL;

Cargando…
Cancelar
Guardar

Powered by TurnKey Linux.