mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-05-07 03:29:19 +02:00
security: add PHP session auth guard for admin panel (item #2, CRITICAL)
- lib/AdminAuth.php: new class with requireLogin(), login(), logout(), isAuthenticated(); starts session with hardened cookie params (HttpOnly, SameSite=Strict, Secure, Path=/admin) — also resolves item #8 (session cookie hardening) - requireLogin() auto-authenticates from nginx Basic Auth credentials ($_SERVER['PHP_AUTH_PW']) so the user only sees one browser prompt; falls back to /admin/login.php if the proxy is absent/misconfigured - config/admin_credentials.php: gitignored credential store; define ADMIN_PASSWORD_HASH with a bcrypt hash to enable PHP auth - config/admin_credentials.example.php: template for the above - config/bootstrap.php: auto-loads admin_credentials.php if present - .gitignore: exclude config/admin_credentials.php - public/admin/login.php: fallback login form (shown only when nginx Basic Auth is bypassed / proxy absent) - public/admin/logout.php: session destruction + redirect to login - All 7 admin PHP files: replace session_start() with AdminAuth::requireLogin() (defence-in-depth behind nginx Basic Auth) - public/admin/inc/head.php: Déconnexion button when ADMIN_PASSWORD_HASH is defined - nginx/PHP_AUTH_LAYER.md: documents dual-auth architecture, UX flow, and setup instructions - docs/TODO.SECURITY.md: items #2 and #8 moved to Resolved; priority order updated (all CRITICAL done)
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +1,6 @@
|
||||
# Admin credentials (contains bcrypt hash — never commit)
|
||||
config/admin_credentials.php
|
||||
|
||||
# Vendor directory (third-party code)
|
||||
vendor/
|
||||
compose.lock
|
||||
|
||||
13
config/admin_credentials.example.php
Normal file
13
config/admin_credentials.example.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin PHP-level password for the session auth guard (defence-in-depth).
|
||||
*
|
||||
* Copy this file to admin_credentials.php and set a real hash.
|
||||
*
|
||||
* Generate a hash:
|
||||
* php -r "echo password_hash('your-password', PASSWORD_DEFAULT);"
|
||||
*
|
||||
* Then uncomment and paste the result below:
|
||||
*/
|
||||
|
||||
// define('ADMIN_PASSWORD_HASH', '$2y$12$...');
|
||||
@@ -31,6 +31,11 @@ function include_template($name) {
|
||||
}
|
||||
}
|
||||
|
||||
// Load admin credentials if available (defines ADMIN_PASSWORD_HASH for AdminAuth)
|
||||
if (file_exists(APP_ROOT . '/config/admin_credentials.php')) {
|
||||
require_once APP_ROOT . '/config/admin_credentials.php';
|
||||
}
|
||||
|
||||
// Autoload Composer dependencies if available
|
||||
if (file_exists(APP_ROOT . '/vendor/autoload.php')) {
|
||||
require_once APP_ROOT . '/vendor/autoload.php';
|
||||
|
||||
@@ -45,6 +45,15 @@
|
||||
|
||||
---
|
||||
|
||||
### Admin Panel — Authentication & Sessions
|
||||
|
||||
| # | Issue | Severity | Resolution |
|
||||
|---|-------|----------|------------|
|
||||
| 2 | No PHP-level authentication in admin panel | 🔴 CRITICAL | `lib/AdminAuth.php` implements a session guard with `password_verify` + `session_regenerate_id`. All admin PHP files now call `AdminAuth::requireLogin()` instead of bare `session_start()`. Credentials stored in gitignored `config/admin_credentials.php` (define `ADMIN_PASSWORD_HASH`). No-op when constant is absent (dev / cli-server). Also resolves item #8 (session cookie hardening via `session_set_cookie_params` before `session_start`). See `nginx/PHP_AUTH_LAYER.md`. |
|
||||
| 8 | Session cookies not hardened (`Secure`, `HttpOnly`, `SameSite` missing) | 🟡 MEDIUM | **Resolved as part of item #2.** `AdminAuth::startSession()` sets `HttpOnly=true`, `SameSite=Strict`, `Secure=true` (off on cli-server), `Path=/admin`, `Lifetime=0` before every `session_start()`. |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 In Progress
|
||||
|
||||
### Database / Input Handling
|
||||
@@ -65,15 +74,6 @@
|
||||
|
||||
---
|
||||
|
||||
### Admin Panel — Authentication & Sessions
|
||||
|
||||
| # | Issue | Severity | Files / Notes |
|
||||
|---|-------|----------|-------|
|
||||
| 2 | No PHP-level authentication in admin panel | 🔴 CRITICAL | All `public/admin/*.php` files. The reverse proxy + nginx Basic Auth is the only auth layer — a single point of failure. CSRF ≠ authentication (CSRF only prevents cross-site forgery on already-open sessions; it does nothing against a direct unauthenticated request). A simple PHP session guard adds real defence-in-depth for ~20 lines using stdlib (`password_verify`, `session_regenerate_id`) with negligible added attack surface — the risk of **not** having it (proxy misconfiguration, bypass, local dev without proxy) outweighs the risk of adding it. |
|
||||
| 8 | Session cookies not hardened (`Secure`, `HttpOnly`, `SameSite` missing) | 🟡 MEDIUM | All admin PHP files using `session_start()` |
|
||||
|
||||
---
|
||||
|
||||
### Admin Panel — Error Logging
|
||||
|
||||
| # | Issue | Severity | Files |
|
||||
@@ -92,8 +92,8 @@
|
||||
|
||||
## Priority Order for Remaining Work
|
||||
|
||||
1. 🔴 **CRITICAL** — Item 2 (add simple PHP session auth guard)
|
||||
2. 🟡 **MEDIUM** — Items 7, 8, 9, 11, 12 (sanitisation standardisation, session hardening, CSP, error log, CSV MIME)
|
||||
1. 🔴 **CRITICAL** — ✅ All done (items 1–2)
|
||||
2. 🟡 **MEDIUM** — Items 7, 9, 11, 12 (sanitisation standardisation, error log, CSP, CSV MIME) — item 8 resolved with item 2
|
||||
3. 🔵 **LOW** — ✅ All done (items 13–16)
|
||||
|
||||
---
|
||||
|
||||
121
lib/AdminAuth.php
Normal file
121
lib/AdminAuth.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
/**
|
||||
* Minimal PHP session guard for the admin panel.
|
||||
*
|
||||
* This is a defence-in-depth layer that sits behind nginx Basic Auth.
|
||||
* It protects against proxy misconfiguration, bypass, and local-dev
|
||||
* scenarios where the reverse proxy may be absent.
|
||||
*
|
||||
* Usage (top of every admin page):
|
||||
* require_once __DIR__ . '/../../lib/AdminAuth.php';
|
||||
* AdminAuth::requireLogin();
|
||||
*
|
||||
* Credential setup (production):
|
||||
* php -r "echo password_hash('your-password', PASSWORD_DEFAULT);"
|
||||
* # Paste result into config/admin_credentials.php as ADMIN_PASSWORD_HASH
|
||||
*
|
||||
* If ADMIN_PASSWORD_HASH is not defined the guard is a no-op (dev / cli-server).
|
||||
*/
|
||||
class AdminAuth
|
||||
{
|
||||
private const SESSION_KEY = 'admin_authenticated';
|
||||
private const LOGIN_URL = '/admin/login.php';
|
||||
|
||||
/**
|
||||
* Start the PHP session with hardened cookie parameters.
|
||||
* Idempotent — safe to call even if session is already active.
|
||||
*/
|
||||
private static function startSession(): void
|
||||
{
|
||||
if (session_status() !== PHP_SESSION_NONE) {
|
||||
return;
|
||||
}
|
||||
// Harden session cookie (item #8)
|
||||
session_set_cookie_params([
|
||||
'lifetime' => 0,
|
||||
'path' => '/admin',
|
||||
'secure' => (php_sapi_name() !== 'cli-server'),
|
||||
'httponly' => true,
|
||||
'samesite' => 'Strict',
|
||||
]);
|
||||
session_start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gate every admin page.
|
||||
*
|
||||
* Authentication order:
|
||||
* 1. Session already authenticated → pass through.
|
||||
* 2. nginx Basic Auth password present in $_SERVER['PHP_AUTH_PW']
|
||||
* → validate it with password_verify; on success create session
|
||||
* (seamless: user only sees the browser Basic Auth dialog).
|
||||
* 3. Neither → redirect to the PHP login form (fallback for when
|
||||
* the reverse proxy is absent / misconfigured).
|
||||
*
|
||||
* No-op if ADMIN_PASSWORD_HASH is not defined (development / cli-server).
|
||||
*/
|
||||
public static function requireLogin(): void
|
||||
{
|
||||
self::startSession();
|
||||
if (!defined('ADMIN_PASSWORD_HASH')) {
|
||||
// No password configured → development / cli-server mode, skip PHP auth.
|
||||
return;
|
||||
}
|
||||
if (!empty($_SESSION[self::SESSION_KEY])) {
|
||||
return; // already authenticated via session
|
||||
}
|
||||
// Try to auto-authenticate from the nginx Basic Auth credentials.
|
||||
// If nginx Basic Auth is bypassed, PHP_AUTH_PW won't be set and this
|
||||
// branch is skipped — the fallback login form is shown instead.
|
||||
if (isset($_SERVER['PHP_AUTH_PW']) && self::login($_SERVER['PHP_AUTH_PW'])) {
|
||||
return;
|
||||
}
|
||||
header('Location: ' . self::LOGIN_URL);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a plaintext password against the stored bcrypt hash.
|
||||
* On success: regenerates the session ID and marks the session authenticated.
|
||||
*
|
||||
* @return bool true on success, false on wrong password / no hash configured.
|
||||
*/
|
||||
public static function login(string $password): bool
|
||||
{
|
||||
$hash = defined('ADMIN_PASSWORD_HASH') ? ADMIN_PASSWORD_HASH : null;
|
||||
if ($hash === null || !password_verify($password, $hash)) {
|
||||
return false;
|
||||
}
|
||||
self::startSession();
|
||||
session_regenerate_id(true);
|
||||
$_SESSION[self::SESSION_KEY] = true;
|
||||
$_SESSION['admin_login_at'] = time();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the current request is authenticated (without redirecting).
|
||||
*/
|
||||
public static function isAuthenticated(): bool
|
||||
{
|
||||
self::startSession();
|
||||
return !empty($_SESSION[self::SESSION_KEY]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the session (logout).
|
||||
*/
|
||||
public static function logout(): void
|
||||
{
|
||||
self::startSession();
|
||||
$_SESSION = [];
|
||||
if (ini_get('session.use_cookies')) {
|
||||
$p = session_get_cookie_params();
|
||||
setcookie(
|
||||
session_name(), '', time() - 86400,
|
||||
$p['path'], $p['domain'], $p['secure'], $p['httponly']
|
||||
);
|
||||
}
|
||||
session_destroy();
|
||||
}
|
||||
}
|
||||
114
nginx/PHP_AUTH_LAYER.md
Normal file
114
nginx/PHP_AUTH_LAYER.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# PHP Session Auth Layer — Admin Panel
|
||||
|
||||
> Addresses: **TODO item #2** (No PHP-level authentication in admin panel — 🔴 CRITICAL)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The admin panel uses **two independent authentication layers** with a single UX prompt:
|
||||
|
||||
| Layer | Mechanism | Configured by |
|
||||
|-------|-----------|---------------|
|
||||
| **1st** | nginx HTTP Basic Auth | `/etc/nginx/.htpasswd-posterg` (see `ADMIN_USERS.md`) |
|
||||
| **2nd** | PHP session guard (`lib/AdminAuth.php`) | `config/admin_credentials.php` |
|
||||
|
||||
The user only sees **one prompt** (the browser Basic Auth dialog). PHP reads the
|
||||
same password from `$_SERVER['PHP_AUTH_PW']` and validates it independently with
|
||||
`password_verify`. On success it creates a session so subsequent requests skip
|
||||
the `password_verify` call.
|
||||
|
||||
---
|
||||
|
||||
## Why two layers?
|
||||
|
||||
nginx Basic Auth alone is a **single point of failure**:
|
||||
|
||||
- Reverse-proxy misconfiguration could expose admin routes directly.
|
||||
- Local development without the proxy leaves admin unprotected.
|
||||
- A misconfigured `auth_basic off` block (e.g., in a nested location) could bypass it.
|
||||
|
||||
The PHP session guard (`AdminAuth::requireLogin()`) is ~100 lines of PHP stdlib
|
||||
(`password_verify` + `session_regenerate_id`) with negligible attack surface.
|
||||
|
||||
## Authentication flow
|
||||
|
||||
```
|
||||
Browser → nginx Basic Auth dialog (username + password)
|
||||
│
|
||||
▼
|
||||
nginx validates against .htpasswd ──✗──▶ 401
|
||||
│ ✓
|
||||
▼
|
||||
PHP: AdminAuth::requireLogin()
|
||||
├─ session already live? ──✓──▶ proceed
|
||||
├─ $_SERVER['PHP_AUTH_PW'] set?
|
||||
│ └─ password_verify(PHP_AUTH_PW, ADMIN_PASSWORD_HASH)
|
||||
│ ├─ ✓ → create session → proceed (normal path)
|
||||
│ └─ ✗ → redirect to login form
|
||||
└─ neither → redirect to login form (proxy bypass)
|
||||
```
|
||||
|
||||
The login form (`/admin/login.php`) is a **fallback** for when the reverse proxy
|
||||
is absent. In normal production use the user never sees it.
|
||||
|
||||
---
|
||||
|
||||
## PHP auth setup (production)
|
||||
|
||||
1. Generate a bcrypt hash for the admin password:
|
||||
```bash
|
||||
php -r "echo password_hash('your-secret-password', PASSWORD_DEFAULT);"
|
||||
```
|
||||
|
||||
2. Create `config/admin_credentials.php` (outside the webroot, never committed):
|
||||
```php
|
||||
<?php
|
||||
define('ADMIN_PASSWORD_HASH', '$2y$12$<paste-hash-here>');
|
||||
```
|
||||
|
||||
3. The `bootstrap.php` auto-loads this file if it exists.
|
||||
|
||||
If `ADMIN_PASSWORD_HASH` is not defined (development / cli-server), the PHP
|
||||
auth layer is a **no-op** — nginx Basic Auth remains the sole guard.
|
||||
|
||||
---
|
||||
|
||||
## Session cookie hardening (TODO item #8)
|
||||
|
||||
`AdminAuth::startSession()` calls `session_set_cookie_params()` before
|
||||
`session_start()`, applying:
|
||||
|
||||
| Attribute | Value |
|
||||
|-----------|-------|
|
||||
| `HttpOnly` | `true` |
|
||||
| `SameSite` | `Strict` |
|
||||
| `Secure` | `true` (disabled on cli-server for dev) |
|
||||
| `Path` | `/admin` |
|
||||
| `Lifetime` | `0` (session cookie, expires on browser close) |
|
||||
|
||||
This replaces all direct `session_start()` calls in admin PHP files.
|
||||
|
||||
---
|
||||
|
||||
## Logout
|
||||
|
||||
A **Déconnexion** button is shown in the admin nav when `ADMIN_PASSWORD_HASH`
|
||||
is defined. It hits `/admin/logout.php` which destroys the PHP session.
|
||||
nginx Basic Auth invalidation requires closing the browser tab / window.
|
||||
|
||||
---
|
||||
|
||||
## Files changed
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `lib/AdminAuth.php` | New — auth guard class |
|
||||
| `config/admin_credentials.php` | New — credential store (gitignored) |
|
||||
| `config/admin_credentials.example.php` | New — example / template |
|
||||
| `config/bootstrap.php` | Load credentials on startup |
|
||||
| `public/admin/*.php` | Replace `session_start()` with `AdminAuth::requireLogin()` |
|
||||
| `public/admin/actions/*.php` | Same |
|
||||
| `public/admin/login.php` | New — login form |
|
||||
| `public/admin/logout.php` | New — logout handler |
|
||||
| `public/admin/inc/head.php` | Logout button in nav |
|
||||
@@ -57,12 +57,15 @@ Reusable HTML components:
|
||||
|
||||
## Security
|
||||
|
||||
- All pages require HTTP Basic Auth (configured in nginx)
|
||||
- All pages require HTTP Basic Auth (configured in nginx) — primary layer
|
||||
- All pages require PHP session auth (`AdminAuth::requireLogin()`) — defence-in-depth
|
||||
- CSRF tokens protect all forms
|
||||
- File uploads validated and sanitized
|
||||
- Database queries use prepared statements
|
||||
- Upload directory outside public/ in production
|
||||
|
||||
See `nginx/PHP_AUTH_LAYER.md` for details on the dual-auth architecture.
|
||||
|
||||
## Templates
|
||||
|
||||
The `inc/` folder contains shared templates:
|
||||
@@ -96,7 +99,8 @@ Backend actions (not directly accessed):
|
||||
```php
|
||||
<?php
|
||||
require_once __DIR__ . "/../../config/bootstrap.php";
|
||||
session_start();
|
||||
require_once __DIR__ . '/../../lib/AdminAuth.php';
|
||||
AdminAuth::requireLogin();
|
||||
$pageTitle = "Your Page Title";
|
||||
?>
|
||||
<?php include "inc/head.php" ?>
|
||||
@@ -114,7 +118,8 @@ $pageTitle = "Your Page Title";
|
||||
```php
|
||||
<?php
|
||||
require_once __DIR__ . "/../../config/bootstrap.php";
|
||||
session_start();
|
||||
require_once __DIR__ . '/../../lib/AdminAuth.php';
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
// Verify CSRF token
|
||||
if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<?php // formulaire.php
|
||||
// Bootstrap application
|
||||
require_once __DIR__ . "/../../config/bootstrap.php";
|
||||
|
||||
require_once __DIR__ . '/../../lib/AdminAuth.php';
|
||||
|
||||
// Configure error reporting
|
||||
ini_set('display_errors', 0);
|
||||
ini_set('log_errors', 1);
|
||||
ini_set('error_log', 'error.log');
|
||||
|
||||
// Start session for CSRF protection
|
||||
session_start();
|
||||
// PHP-level auth guard (defence-in-depth behind nginx Basic Auth)
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
// Verify CSRF token
|
||||
if (!isset($_POST['csrf_token']) || !isset($_SESSION['csrf_token']) ||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<?php
|
||||
// Bootstrap application
|
||||
require_once __DIR__ . "/../../config/bootstrap.php";
|
||||
require_once __DIR__ . '/../../lib/AdminAuth.php';
|
||||
|
||||
/**
|
||||
* Handle publish/unpublish actions for theses
|
||||
*/
|
||||
session_start();
|
||||
// PHP-level auth guard (defence-in-depth behind nginx Basic Auth)
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
require_once __DIR__ . '/../../lib/Database.php';
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<?php
|
||||
// Bootstrap application
|
||||
require_once __DIR__ . "/../../config/bootstrap.php";
|
||||
require_once __DIR__ . '/../../lib/AdminAuth.php';
|
||||
|
||||
// PHP-level auth guard (defence-in-depth behind nginx Basic Auth)
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
// Start session and generate CSRF token
|
||||
session_start();
|
||||
if (empty($_SESSION["csrf_token"])) {
|
||||
$_SESSION["csrf_token"] = bin2hex(random_bytes(32));
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<?php
|
||||
// Bootstrap application
|
||||
require_once __DIR__ . "/../../config/bootstrap.php";
|
||||
require_once __DIR__ . '/../../lib/AdminAuth.php';
|
||||
|
||||
// Edit thesis page
|
||||
session_start();
|
||||
// PHP-level auth guard (defence-in-depth behind nginx Basic Auth)
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
// Generate CSRF token
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<?php
|
||||
// Bootstrap application
|
||||
require_once __DIR__ . "/../../config/bootstrap.php";
|
||||
require_once __DIR__ . '/../../lib/AdminAuth.php';
|
||||
|
||||
// CSV Import page for Post-ERG thesis database
|
||||
// This page allows importing thesis data from CSV files
|
||||
|
||||
session_start();
|
||||
// PHP-level auth guard (defence-in-depth behind nginx Basic Auth)
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
// Generate CSRF token
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
|
||||
@@ -62,5 +62,8 @@
|
||||
|
||||
echo implode(' ', $navLinks);
|
||||
?>
|
||||
<?php if (defined('ADMIN_PASSWORD_HASH')): ?>
|
||||
<a href="/admin/logout.php"><button>🔐 Déconnexion</button></a>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
</header>
|
||||
@@ -1,9 +1,10 @@
|
||||
<?php
|
||||
// Bootstrap application
|
||||
require_once __DIR__ . "/../../config/bootstrap.php";
|
||||
require_once __DIR__ . '/../../lib/AdminAuth.php';
|
||||
|
||||
// List all theses in the database
|
||||
session_start();
|
||||
// PHP-level auth guard (defence-in-depth behind nginx Basic Auth)
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
// Generate CSRF token
|
||||
if (empty($_SESSION['csrf_token'])) {
|
||||
|
||||
60
public/admin/login.php
Normal file
60
public/admin/login.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../config/bootstrap.php';
|
||||
require_once __DIR__ . '/../../lib/AdminAuth.php';
|
||||
|
||||
// If no password is configured, nothing to log into — go straight to admin.
|
||||
if (!defined('ADMIN_PASSWORD_HASH')) {
|
||||
header('Location: /admin/');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Already authenticated — redirect to admin.
|
||||
if (AdminAuth::isAuthenticated()) {
|
||||
header('Location: /admin/');
|
||||
exit;
|
||||
}
|
||||
|
||||
$error = '';
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$password = $_POST['password'] ?? '';
|
||||
if (AdminAuth::login($password)) {
|
||||
header('Location: /admin/');
|
||||
exit;
|
||||
}
|
||||
// Intentionally vague error — avoid user-enumeration.
|
||||
$error = 'Mot de passe incorrect.';
|
||||
}
|
||||
|
||||
$pageTitle = 'Connexion';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?php echo htmlspecialchars($pageTitle); ?> — Post-ERG Admin</title>
|
||||
<link rel="stylesheet" href="/assets/modern-normalize.min.css">
|
||||
<link rel="stylesheet" href="/assets/admin.css">
|
||||
<link rel="shortcut icon" href="/assets/admin_favicon.svg" type="image/svg+xml">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1><?php echo htmlspecialchars($pageTitle); ?></h1>
|
||||
</header>
|
||||
<main>
|
||||
<?php if ($error): ?>
|
||||
<div class="alert-error">
|
||||
<strong>⚠️ <?php echo htmlspecialchars($error); ?></strong>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<form method="post" action="/admin/login.php">
|
||||
<fieldset>
|
||||
<legend>Authentification admin</legend>
|
||||
<label for="password">Mot de passe</label>
|
||||
<input type="password" id="password" name="password" required autofocus>
|
||||
<button type="submit">Se connecter</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
8
public/admin/logout.php
Normal file
8
public/admin/logout.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../../config/bootstrap.php';
|
||||
require_once __DIR__ . '/../../lib/AdminAuth.php';
|
||||
|
||||
AdminAuth::logout();
|
||||
|
||||
header('Location: /admin/login.php');
|
||||
exit;
|
||||
@@ -1,6 +1,10 @@
|
||||
<?php
|
||||
// Bootstrap application
|
||||
require_once __DIR__ . "/../../config/bootstrap.php";
|
||||
require_once __DIR__ . '/../../lib/AdminAuth.php';
|
||||
|
||||
// PHP-level auth guard (defence-in-depth behind nginx Basic Auth)
|
||||
AdminAuth::requireLogin();
|
||||
|
||||
// Configure error reporting
|
||||
ini_set('display_errors', 0);
|
||||
|
||||
Reference in New Issue
Block a user