Sécurité et qualité : headers HTTP, permissions .env, lint PHPStan + PHP-CS-Fixer, réorganisation dossiers, scripts de déploiement
This commit is contained in:
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
# Credentials & config sensible
|
# Credentials
|
||||||
.env
|
.env
|
||||||
|
|
||||||
# Composer dependencies
|
# Composer dependencies
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
{"php":"8.3.6","version":"3.89.1:v3.89.1#f34967da2866ace090a2b447de1f357356474573","indent":" ","lineEnding":"\n","rules":{"binary_operator_spaces":{"default":"at_least_single_space"},"blank_line_after_opening_tag":true,"blank_line_between_import_groups":true,"blank_lines_before_namespace":true,"braces_position":{"allow_single_line_empty_anonymous_classes":true},"class_definition":{"inline_constructor_arguments":false,"space_before_parenthesis":true},"compact_nullable_type_declaration":true,"declare_equal_normalize":true,"lowercase_cast":true,"lowercase_static_reference":true,"modifier_keywords":true,"new_with_parentheses":{"anonymous_class":true},"no_blank_lines_after_class_opening":true,"no_extra_blank_lines":{"tokens":["use"]},"no_leading_import_slash":true,"no_whitespace_in_blank_line":true,"ordered_class_elements":{"order":["use_trait"]},"ordered_imports":{"imports_order":["class","function","const"],"sort_algorithm":"none"},"return_type_declaration":true,"short_scalar_cast":true,"single_import_per_statement":{"group_to_single_imports":false},"single_space_around_construct":{"constructs_followed_by_a_single_space":["abstract","as","case","catch","class","const_import","do","else","elseif","final","finally","for","foreach","function","function_import","if","insteadof","interface","namespace","new","private","protected","public","static","switch","trait","try","use","use_lambda","while"],"constructs_preceded_by_a_single_space":["as","else","elseif","use_lambda"]},"single_trait_insert_per_statement":true,"ternary_operator_spaces":true,"unary_operator_spaces":{"only_dec_inc":true},"blank_line_after_namespace":true,"constant_case":true,"control_structure_braces":true,"control_structure_continuation_position":true,"elseif":true,"function_declaration":{"closure_fn_spacing":"one"},"indentation_type":true,"line_ending":true,"lowercase_keywords":true,"method_argument_space":{"after_heredoc":false,"attribute_placement":"ignore","on_multiline":"ensure_fully_multiline"},"no_break_comment":true,"no_closing_tag":true,"no_multiple_statements_per_line":true,"no_space_around_double_colon":true,"no_spaces_after_function_name":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_line_after_imports":true,"spaces_inside_parentheses":true,"statement_indentation":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"encoding":true,"full_opening_tag":true,"strict_param":true,"declare_strict_types":true,"no_unused_imports":true,"single_quote":true},"hashes":{"templates\/post_view.php":"3c83bdaa22dc0c01bb0b043e0c07784d","templates\/header.php":"f4b64c4ecb4dadec166cb7935309096a","templates\/post_list.php":"191d1349cc7b0972a0ebbb99149590d3","templates\/layout.php":"7fbb10d6a3693b543efb83eee52b049c","templates\/footer.php":"3111b4701ea698ba11c3423260657e28","templates\/post_form.php":"07c1208cb64a796bd8f24d7f2fd62ccd","public\/login\/oidc.php":"8ec86d6f3af33f64d586109ec17f817d","public\/login\/config.php":"5b7b3e2937b349c76a2fd239c3ae06f8","public\/login\/magic.php":"54ef6b7ef80e608905e64e4fa8539846","public\/login\/index.php":"063d7b997bf8292d2b3f8c34dae3252f","public\/route.php":"f35dadd27dbdea162b67c42002519be9","public\/oidc\/callback.php":"793ff84451299c9984ac4742f02ca842","public\/oidc\/start.php":"87ddb61a0ef796d7303709ffa741c9c7","public\/oidc\/me.php":"d0439342011bb0e58ef8738b3b81cc2f","public\/index.php":"73a917520ea547ae8a122bd90098bf46","src\/Infrastructure\/DbAdapter.php":"3899a835130c146e2d30dbcca88d8f33","src\/Infrastructure\/Database.php":"6f2848ed70b29d9c2e2d259be611b9b0","src\/Infrastructure\/Session.php":"3538a1147cc81678c470d45ea8574a95","src\/Domain\/User.php":"02213454f7edf43f4afae3f2f81aaf01","src\/Http\/Csrf.php":"55631812cab4b1192f8e30c5d35fd5eb","src\/FileManager.php":"a51dda44f293f238aea295fd56b2fa99","src\/PostManager.php":"25f0179c4d96e9aa04218d54bf45a029","src\/helpers.php":"3a83a4872b1e3e3c58898b54f51e72b4","src\/db.php":"8888b7fbc9740eb3c60dd2374d0cb5d6","src\/auth.php":"d237017d90091f5fb75a133d08d5e544","src\/ConfigRepo.php":"c2dcee160a272d27725d480a90e76dcf","src\/Parsedown.php":"85da2b47eca1a703fdfe44753bf912df","src\/Service\/MailQueue.php":"7e040056aec64cfd780e3c9e7d04748a","src\/Service\/Validator.php":"7c267b8b9f3f1bac0f2520dd10364831","src\/Service\/UiFormRenderer.php":"065617191c6d680ce97588f4fa159688","src\/Service\/AuthService.php":"51c714164c6bd453154c024d9aa814e9","src\/Service\/MailService.php":"e1ef847a70551ae8887b86b3fb4167d0","src\/mailer.php":"17e6b19103c880cc9a6c6634486506c2","src\/Repository\/DictionnaryRepository.php":"f937e98cf0f27b59ae00e430b52a586d","src\/Repository\/ProfileRepository.php":"b1cd483652500ee4e2aaaa9e0330ff1d","src\/Repository\/UserRepository.php":"d75fed70910d0e6450a5b6c8ce9608c2","versions.php":"51a72261e1a507d3435b4a24e5f5fc09","config\/config.php":"a8b7698b01ab9b40eea655e8fcc194fc"}}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
Toutes les modifications notables sont documentées ici.
|
||||||
|
Format : [Keep a Changelog](https://keepachangelog.com/fr/1.0.0/)
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initialisation du dépôt git sur le serveur
|
||||||
|
- Réorganisation des dossiers (`includes/` → `src/` et `templates/`, `docs/`, `database/`)
|
||||||
|
- Scripts de déploiement (`sync.sh`, `commit.sh`, `deploy.sh`, `sync-server-config.sh`)
|
||||||
|
- Copie locale des configs serveur Apache et PHP-FPM
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Format des entrées :
|
||||||
|
## [x.y.z] - YYYY-MM-DD
|
||||||
|
### Added — nouvelles fonctionnalités
|
||||||
|
### Changed — modifications de fonctionnalités existantes
|
||||||
|
### Fixed — corrections de bugs
|
||||||
|
### Removed — suppressions
|
||||||
|
### Security — corrections de failles
|
||||||
|
-->
|
||||||
+5
-4
@@ -1,4 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
// config/config.php
|
// config/config.php
|
||||||
|
|
||||||
require_once BASE_PATH . '/vendor/autoload.php';
|
require_once BASE_PATH . '/vendor/autoload.php';
|
||||||
@@ -11,7 +13,7 @@ $dotenv->load();
|
|||||||
|
|
||||||
if (!$_ENV['APP_URL']) {
|
if (!$_ENV['APP_URL']) {
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
echo "Configuration manquante : définis APP_URL ou APP_URL dans le .env";
|
echo 'Configuration manquante : définis APP_URL ou APP_URL dans le .env';
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,12 +28,11 @@ $_ENV['APP_URL'] = APP_URL;
|
|||||||
* url('ressources/user/login.php')
|
* url('ressources/user/login.php')
|
||||||
* url('api/items', ['page'=>2])
|
* url('api/items', ['page'=>2])
|
||||||
*/
|
*/
|
||||||
function url(string $path = '', array $qs = []): string {
|
function url(string $path = '', array $qs = []): string
|
||||||
|
{
|
||||||
$u = APP_URL . ltrim($path, '/');
|
$u = APP_URL . ltrim($path, '/');
|
||||||
if ($qs) {
|
if ($qs) {
|
||||||
$u .= (str_contains($u, '?') ? '&' : '?') . http_build_query($qs);
|
$u .= (str_contains($u, '?') ? '&' : '?') . http_build_query($qs);
|
||||||
}
|
}
|
||||||
return $u;
|
return $u;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
parameters:
|
||||||
|
ignoreErrors:
|
||||||
|
-
|
||||||
|
message: "#^Unreachable statement \\- code above always terminates\\.$#"
|
||||||
|
count: 1
|
||||||
|
path: src/Repository/ProfileRepository.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: "#^Call to method getCode\\(\\) on an unknown class App\\\\Repository\\\\PDOException\\.$#"
|
||||||
|
count: 1
|
||||||
|
path: src/Repository/UserRepository.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: "#^Caught class App\\\\Repository\\\\PDOException not found\\.$#"
|
||||||
|
count: 1
|
||||||
|
path: src/Repository/UserRepository.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: "#^Instantiated class App\\\\Repository\\\\InvalidArgumentException not found\\.$#"
|
||||||
|
count: 1
|
||||||
|
path: src/Repository/UserRepository.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: "#^Method App\\\\Repository\\\\UserRepository\\:\\:nullIfEmpty\\(\\) is unused\\.$#"
|
||||||
|
count: 1
|
||||||
|
path: src/Repository/UserRepository.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: "#^Throwing object of an unknown class App\\\\Repository\\\\InvalidArgumentException\\.$#"
|
||||||
|
count: 1
|
||||||
|
path: src/Repository/UserRepository.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: "#^Throwing object of an unknown class App\\\\Repository\\\\PDOException\\.$#"
|
||||||
|
count: 1
|
||||||
|
path: src/Repository/UserRepository.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: "#^Class App\\\\Repository\\\\UserRepository constructor invoked with 0 parameters, 1 required\\.$#"
|
||||||
|
count: 1
|
||||||
|
path: src/Service/AuthService.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: "#^Call to an undefined method App\\\\Infrastructure\\\\Database\\:\\:getConnection\\(\\)\\.$#"
|
||||||
|
count: 1
|
||||||
|
path: src/Service/MailQueue.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: "#^Call to an undefined method App\\\\Infrastructure\\\\Database\\:\\:getConnection\\(\\)\\.$#"
|
||||||
|
count: 1
|
||||||
|
path: src/Service/MailService.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: "#^Comparison operation \"\\>\" between 200 and 0 is always true\\.$#"
|
||||||
|
count: 1
|
||||||
|
path: src/Service/MailService.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: "#^Constant BASE_PATH not found\\.$#"
|
||||||
|
count: 1
|
||||||
|
path: src/auth.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: "#^Parameter \\#1 \\$scope of method Jumbojett\\\\OpenIDConnectClient\\:\\:addScope\\(\\) expects array, string given\\.$#"
|
||||||
|
count: 1
|
||||||
|
path: src/auth.php
|
||||||
|
|
||||||
|
-
|
||||||
|
message: "#^Constant BASE_PATH not found\\.$#"
|
||||||
|
count: 1
|
||||||
|
path: src/db.php
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
includes:
|
||||||
|
- phpstan-baseline.neon
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
level: 5
|
||||||
|
paths:
|
||||||
|
- src
|
||||||
|
excludePaths:
|
||||||
|
- src/Parsedown.php
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once BASE_PATH . '/src/db.php';
|
|
||||||
require_once BASE_PATH . '/src/PostManager.php';
|
|
||||||
|
|
||||||
$postManager = new PostManager($db);
|
|
||||||
|
|
||||||
$errors = [];
|
|
||||||
$title = '';
|
|
||||||
$content = '';
|
|
||||||
$published = false;
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
||||||
$title = trim($_POST['title'] ?? '');
|
|
||||||
$content = trim($_POST['content'] ?? '');
|
|
||||||
$published = isset($_POST['published']);
|
|
||||||
|
|
||||||
if ($title === '') {
|
|
||||||
$errors[] = 'Le titre est obligatoire.';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($errors)) {
|
|
||||||
$postId = $postManager->create($title, $content, $published);
|
|
||||||
header("Location: index.php");
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="fr">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Nouveau post</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">
|
|
||||||
<h1 class="mb-4">Ajouter un nouveau post</h1>
|
|
||||||
|
|
||||||
<?php if (!empty($errors)): ?>
|
|
||||||
<div class="alert alert-danger">
|
|
||||||
<ul class="mb-0">
|
|
||||||
<?php foreach ($errors as $error): ?>
|
|
||||||
<li><?= htmlspecialchars($error) ?></li>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<form method="POST">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="title" class="form-label">Titre</label>
|
|
||||||
<input type="text" class="form-control" id="title" name="title" value="<?= htmlspecialchars($title) ?>" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="content" class="form-label">Contenu</label>
|
|
||||||
<textarea class="form-control" id="content" name="content" rows="6"><?= htmlspecialchars($content) ?></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-check mb-3">
|
|
||||||
<input class="form-check-input" type="checkbox" id="published" name="published" <?= $published ? 'checked' : '' ?>>
|
|
||||||
<label class="form-check-label" for="published">Publier immédiatement</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary">Enregistrer</button>
|
|
||||||
<a href="index.php" class="btn btn-secondary">Annuler</a>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
define('BASE_PATH', realpath(__DIR__ . '/../'));
|
define('BASE_PATH', realpath(__DIR__ . '/../'));
|
||||||
|
|
||||||
require_once BASE_PATH . '/src/helpers.php';
|
require_once BASE_PATH . '/src/helpers.php';
|
||||||
|
|||||||
+13
-7
@@ -4,33 +4,39 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||||
require_once dirname(__DIR__, 2) . '/app/bootstrap.php';
|
require_once dirname(__DIR__, 2) . '/app/bootstrap.php';
|
||||||
if (!defined('BASE_PATH')) { require_once dirname(__DIR__, 2) . '/config/config.php'; }
|
if (!defined('BASE_PATH')) {
|
||||||
|
require_once dirname(__DIR__, 2) . '/config/config.php';
|
||||||
|
}
|
||||||
require_once BASE_PATH . '/includes/db.php';
|
require_once BASE_PATH . '/includes/db.php';
|
||||||
require_once BASE_PATH . '/includes/csrf.php';
|
require_once BASE_PATH . '/includes/csrf.php';
|
||||||
require_once BASE_PATH . '/includes/ConfigRepo.php';
|
require_once BASE_PATH . '/src/ConfigRepo.php';
|
||||||
|
|
||||||
Session::startSecure(getenv('SESSION_NAME') ?: 'SID_IDENT');
|
Session::startSecure(getenv('SESSION_NAME') ?: 'SID_IDENT');
|
||||||
ensure_admin();
|
ensure_admin();
|
||||||
csrf_start();
|
csrf_start();
|
||||||
|
|
||||||
$cfg = config_repo_get();
|
$cfg = config_repo_get();
|
||||||
$msg = null; $err = null;
|
$msg = null;
|
||||||
|
$err = null;
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
if (!csrf_check($_POST['csrf'] ?? '')) { http_response_code(403); exit('CSRF'); }
|
if (!csrf_check($_POST['csrf'] ?? '')) {
|
||||||
|
http_response_code(403);
|
||||||
|
exit('CSRF');
|
||||||
|
}
|
||||||
|
|
||||||
$in = [
|
$in = [
|
||||||
'oidc_issuer' => trim((string)($_POST['oidc_issuer'] ?? '')),
|
'oidc_issuer' => trim((string)($_POST['oidc_issuer'] ?? '')),
|
||||||
'oidc_name' => trim((string)($_POST['oidc_name'] ?? '')),
|
'oidc_name' => trim((string)($_POST['oidc_name'] ?? '')),
|
||||||
'oidc_client_id' => trim((string)($_POST['oidc_client_id'] ?? '')),
|
'oidc_client_id' => trim((string)($_POST['oidc_client_id'] ?? '')),
|
||||||
'oidc_client_secret'=> trim((string)($_POST['oidc_client_secret'] ?? '')),
|
'oidc_client_secret' => trim((string)($_POST['oidc_client_secret'] ?? '')),
|
||||||
'oidc_redirect_uri' => trim((string)($_POST['oidc_redirect_uri'] ?? '')),
|
'oidc_redirect_uri' => trim((string)($_POST['oidc_redirect_uri'] ?? '')),
|
||||||
];
|
];
|
||||||
|
|
||||||
// validations simples
|
// validations simples
|
||||||
if ($in['allow_oidc']) {
|
if ($in['allow_oidc']) {
|
||||||
if ($in['oidc_issuer'] === '' || $in['oidc_client_id'] === '' || $in['oidc_client_secret'] === '' || $in['oidc_redirect_uri'] === '') {
|
if ($in['oidc_issuer'] === '' || $in['oidc_client_id'] === '' || $in['oidc_client_secret'] === '' || $in['oidc_redirect_uri'] === '') {
|
||||||
$err = "OIDC activé mais champs incomplets.";
|
$err = 'OIDC activé mais champs incomplets.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +54,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
env_set_pairs(BASE_PATH.'/.env', $envPairs);
|
env_set_pairs(BASE_PATH.'/.env', $envPairs);
|
||||||
|
|
||||||
$cfg = config_repo_get();
|
$cfg = config_repo_get();
|
||||||
$msg = "Configuration enregistrée.";
|
$msg = 'Configuration enregistrée.';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
|||||||
+28
-10
@@ -8,18 +8,27 @@ use App\Http\Csrf;
|
|||||||
|
|
||||||
// --- Helpers AVANT tout usage ---
|
// --- Helpers AVANT tout usage ---
|
||||||
if (!function_exists('env')) {
|
if (!function_exists('env')) {
|
||||||
function env(string $key, ?string $default = null): ?string {
|
function env(string $key, ?string $default = null): ?string
|
||||||
if (array_key_exists($key, $_ENV) && $_ENV[$key] !== '') return (string)$_ENV[$key];
|
{
|
||||||
|
if (array_key_exists($key, $_ENV) && $_ENV[$key] !== '') {
|
||||||
|
return (string)$_ENV[$key];
|
||||||
|
}
|
||||||
$v = getenv($key);
|
$v = getenv($key);
|
||||||
if ($v !== false && $v !== '') return (string)$v;
|
if ($v !== false && $v !== '') {
|
||||||
|
return (string)$v;
|
||||||
|
}
|
||||||
return $default;
|
return $default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!function_exists('db')) {
|
if (!function_exists('db')) {
|
||||||
function db(): \PDO { return \App\Infrastructure\Database::get(); }
|
function db(): \PDO
|
||||||
|
{
|
||||||
|
return \App\Infrastructure\Database::get();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!function_exists('url')) {
|
if (!function_exists('url')) {
|
||||||
function url(string $path = '/'): string {
|
function url(string $path = '/'): string
|
||||||
|
{
|
||||||
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||||||
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||||||
return $scheme . '://' . $host . $path;
|
return $scheme . '://' . $host . $path;
|
||||||
@@ -40,7 +49,9 @@ $maxPerWin = (int) env('MAGIC_MAX_PER_WINDOW', '5');
|
|||||||
$defaultReturn = '/';
|
$defaultReturn = '/';
|
||||||
$sanitize = static function (string $url) use ($defaultReturn): string {
|
$sanitize = static function (string $url) use ($defaultReturn): string {
|
||||||
$url = trim($url);
|
$url = trim($url);
|
||||||
if ($url === '' || !str_starts_with($url, '/')) return $defaultReturn;
|
if ($url === '' || !str_starts_with($url, '/')) {
|
||||||
|
return $defaultReturn;
|
||||||
|
}
|
||||||
return $url;
|
return $url;
|
||||||
};
|
};
|
||||||
$returnTo = $sanitize((string)($_GET['return_to'] ?? ($_SERVER['HTTP_REFERER'] ?? $defaultReturn)));
|
$returnTo = $sanitize((string)($_GET['return_to'] ?? ($_SERVER['HTTP_REFERER'] ?? $defaultReturn)));
|
||||||
@@ -49,7 +60,10 @@ $returnTo = $sanitize((string)($_GET['return_to'] ?? ($_SERVER['HTTP_REFERER'] ?
|
|||||||
$oidcEnabled = (bool) (env('OIDC_ISSUER') && env('OIDC_CLIENT_ID'));
|
$oidcEnabled = (bool) (env('OIDC_ISSUER') && env('OIDC_CLIENT_ID'));
|
||||||
$oidcLoginUrl = '/login/oidc' . ($returnTo ? ('?return_to=' . urlencode($returnTo)) : '');
|
$oidcLoginUrl = '/login/oidc' . ($returnTo ? ('?return_to=' . urlencode($returnTo)) : '');
|
||||||
$oidcAuto = (isset($_GET['sso']) && $_GET['sso'] === '1') || (env('OIDC_AUTO', '0') === '1');
|
$oidcAuto = (isset($_GET['sso']) && $_GET['sso'] === '1') || (env('OIDC_AUTO', '0') === '1');
|
||||||
if ($oidcEnabled && $oidcAuto) { header('Location: ' . $oidcLoginUrl, true, 302); exit; }
|
if ($oidcEnabled && $oidcAuto) {
|
||||||
|
header('Location: ' . $oidcLoginUrl, true, 302);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
// --- form: demande de lien magique ---
|
// --- form: demande de lien magique ---
|
||||||
$errors = [];
|
$errors = [];
|
||||||
@@ -65,13 +79,15 @@ if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
|
|||||||
} else {
|
} else {
|
||||||
// rate limit simple par email et IP
|
// rate limit simple par email et IP
|
||||||
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? ($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0');
|
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? ($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0');
|
||||||
if (strpos($ip, ',') !== false) $ip = trim(explode(',', $ip, 2)[0]);
|
if (strpos($ip, ',') !== false) {
|
||||||
|
$ip = trim(explode(',', $ip, 2)[0]);
|
||||||
|
}
|
||||||
|
|
||||||
$pdo = db();
|
$pdo = db();
|
||||||
$pdo->beginTransaction();
|
$pdo->beginTransaction();
|
||||||
try {
|
try {
|
||||||
// purge expirés / consommés
|
// purge expirés / consommés
|
||||||
$pdo->prepare("DELETE FROM auth_magic_links WHERE email = :e AND (expires_at < NOW() OR consumed_at IS NOT NULL)")
|
$pdo->prepare('DELETE FROM auth_magic_links WHERE email = :e AND (expires_at < NOW() OR consumed_at IS NOT NULL)')
|
||||||
->execute([':e' => $email]);
|
->execute([':e' => $email]);
|
||||||
|
|
||||||
// 1) cooldown: refuser si un envoi récent < coolMin
|
// 1) cooldown: refuser si un envoi récent < coolMin
|
||||||
@@ -130,7 +146,9 @@ if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
|
|||||||
$okMsg = "Un lien vient d'être envoyé. Vérifiez votre boîte de réception et le dossier spam/indésirables.";
|
$okMsg = "Un lien vient d'être envoyé. Vérifiez votre boîte de réception et le dossier spam/indésirables.";
|
||||||
|
|
||||||
} catch (\Throwable $ex) {
|
} catch (\Throwable $ex) {
|
||||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
if ($pdo->inTransaction()) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
}
|
||||||
$errors[] = $ex->getMessage();
|
$errors[] = $ex->getMessage();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-12
@@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
// projet : mug.a5l.fr
|
// projet : mug.a5l.fr
|
||||||
// fichier : pages/login/magic.php
|
// fichier : pages/login/magic.php
|
||||||
// version : 20251011
|
// version : 20251011
|
||||||
@@ -8,13 +9,17 @@ require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
|||||||
require_once dirname(__DIR__, 2) . '/app/bootstrap.php';
|
require_once dirname(__DIR__, 2) . '/app/bootstrap.php';
|
||||||
require_once dirname(__DIR__, 2) . '/config/config.php';
|
require_once dirname(__DIR__, 2) . '/config/config.php';
|
||||||
|
|
||||||
use App\Service\AuthService; // si tu as un service pour ouvrir une session
|
// si tu as un service pour ouvrir une session
|
||||||
|
|
||||||
if (!function_exists('db')) {
|
if (!function_exists('db')) {
|
||||||
function db(): PDO { return \App\Infrastructure\Database::get(); }
|
function db(): PDO
|
||||||
|
{
|
||||||
|
return \App\Infrastructure\Database::get();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!function_exists('url')) {
|
if (!function_exists('url')) {
|
||||||
function url(string $path = '/'): string {
|
function url(string $path = '/'): string
|
||||||
|
{
|
||||||
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||||||
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||||||
return $scheme . '://' . $host . $path;
|
return $scheme . '://' . $host . $path;
|
||||||
@@ -31,24 +36,32 @@ $pdo = db();
|
|||||||
$pdo->beginTransaction();
|
$pdo->beginTransaction();
|
||||||
try {
|
try {
|
||||||
// récupère lien non consommé et non expiré
|
// récupère lien non consommé et non expiré
|
||||||
$sql = "SELECT id, email, token, created_at, expires_at, consumed_at, return_to
|
$sql = 'SELECT id, email, token, created_at, expires_at, consumed_at, return_to
|
||||||
FROM auth_magic_links
|
FROM auth_magic_links
|
||||||
WHERE token = :t
|
WHERE token = :t
|
||||||
FOR UPDATE";
|
FOR UPDATE';
|
||||||
$stmt = $pdo->prepare($sql);
|
$stmt = $pdo->prepare($sql);
|
||||||
$stmt->execute([':t' => $token]);
|
$stmt->execute([':t' => $token]);
|
||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
if (!$row) throw new RuntimeException('Lien inconnu.');
|
if (!$row) {
|
||||||
if ($row['consumed_at'] !== null) throw new RuntimeException('Lien déjà utilisé.');
|
throw new RuntimeException('Lien inconnu.');
|
||||||
if (strtotime((string)$row['expires_at']) < time()) throw new RuntimeException('Lien expiré.');
|
}
|
||||||
|
if ($row['consumed_at'] !== null) {
|
||||||
|
throw new RuntimeException('Lien déjà utilisé.');
|
||||||
|
}
|
||||||
|
if (strtotime((string)$row['expires_at']) < time()) {
|
||||||
|
throw new RuntimeException('Lien expiré.');
|
||||||
|
}
|
||||||
|
|
||||||
// consomme le lien
|
// consomme le lien
|
||||||
$pdo->prepare("UPDATE auth_magic_links SET consumed_at = NOW() WHERE id = :id")->execute([':id' => $row['id']]);
|
$pdo->prepare('UPDATE auth_magic_links SET consumed_at = NOW() WHERE id = :id')->execute([':id' => $row['id']]);
|
||||||
$pdo->commit();
|
$pdo->commit();
|
||||||
|
|
||||||
// ouvre une session applicative « anonyme authentifiée par email »
|
// ouvre une session applicative « anonyme authentifiée par email »
|
||||||
if (session_status() !== PHP_SESSION_ACTIVE) session_start();
|
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
$_SESSION['auth'] = [
|
$_SESSION['auth'] = [
|
||||||
'method' => 'magic',
|
'method' => 'magic',
|
||||||
'email' => (string)$row['email'],
|
'email' => (string)$row['email'],
|
||||||
@@ -58,11 +71,15 @@ try {
|
|||||||
|
|
||||||
$dest = $row['return_to'] ?? '/';
|
$dest = $row['return_to'] ?? '/';
|
||||||
// sécurité: ne renvoyer que des chemins relatifs
|
// sécurité: ne renvoyer que des chemins relatifs
|
||||||
if (!is_string($dest) || !str_starts_with($dest, '/')) $dest = '/';
|
if (!is_string($dest) || !str_starts_with($dest, '/')) {
|
||||||
|
$dest = '/';
|
||||||
|
}
|
||||||
header('Location: ' . $dest, true, 303);
|
header('Location: ' . $dest, true, 303);
|
||||||
exit;
|
exit;
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
if ($pdo->inTransaction()) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
}
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
echo htmlspecialchars($e->getMessage(), ENT_QUOTES);
|
echo htmlspecialchars($e->getMessage(), ENT_QUOTES);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
// proxy vers pages/oidc/start.php avec flow=login
|
// proxy vers pages/oidc/start.php avec flow=login
|
||||||
$_GET['flow'] = 'login';
|
$_GET['flow'] = 'login';
|
||||||
require_once dirname(__DIR__) . '/oidc/start.php';
|
require_once dirname(__DIR__) . '/oidc/start.php';
|
||||||
|
|||||||
@@ -1,19 +1,27 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use App\Infrastructure\Database;
|
use App\Infrastructure\Database;
|
||||||
|
|
||||||
if (session_status() !== PHP_SESSION_ACTIVE) session_start();
|
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||||
require_once dirname(__DIR__, 2) . '/app/bootstrap.php';
|
require_once dirname(__DIR__, 2) . '/app/bootstrap.php';
|
||||||
require_once dirname(__DIR__, 2) . '/config/config.php';
|
require_once dirname(__DIR__, 2) . '/config/config.php';
|
||||||
|
|
||||||
if (!function_exists('env')) {
|
if (!function_exists('env')) {
|
||||||
function env(string $key, ?string $default = null): ?string {
|
function env(string $key, ?string $default = null): ?string
|
||||||
if (array_key_exists($key, $_ENV) && $_ENV[$key] !== '') return (string)$_ENV[$key];
|
{
|
||||||
|
if (array_key_exists($key, $_ENV) && $_ENV[$key] !== '') {
|
||||||
|
return (string)$_ENV[$key];
|
||||||
|
}
|
||||||
$v = getenv($key);
|
$v = getenv($key);
|
||||||
if ($v !== false && $v !== '') return (string)$v;
|
if ($v !== false && $v !== '') {
|
||||||
|
return (string)$v;
|
||||||
|
}
|
||||||
return $default;
|
return $default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,7 +76,9 @@ $post = [
|
|||||||
'client_id' => $OIDC_CLIENT_ID,
|
'client_id' => $OIDC_CLIENT_ID,
|
||||||
'code_verifier' => $codeVerifier,
|
'code_verifier' => $codeVerifier,
|
||||||
];
|
];
|
||||||
if ($OIDC_CLIENT_SECRET !== '') $post['client_secret'] = $OIDC_CLIENT_SECRET;
|
if ($OIDC_CLIENT_SECRET !== '') {
|
||||||
|
$post['client_secret'] = $OIDC_CLIENT_SECRET;
|
||||||
|
}
|
||||||
|
|
||||||
$ch = curl_init($tokenEndpoint);
|
$ch = curl_init($tokenEndpoint);
|
||||||
curl_setopt_array($ch, [
|
curl_setopt_array($ch, [
|
||||||
@@ -127,7 +137,9 @@ if (!$email && $idToken && substr_count($idToken, '.') === 2) {
|
|||||||
[, $p, ] = explode('.', $idToken, 3);
|
[, $p, ] = explode('.', $idToken, 3);
|
||||||
$payloadJson = base64_decode(strtr($p, '-_', '+/'), true);
|
$payloadJson = base64_decode(strtr($p, '-_', '+/'), true);
|
||||||
$payload = $payloadJson ? json_decode($payloadJson, true) : null;
|
$payload = $payloadJson ? json_decode($payloadJson, true) : null;
|
||||||
if (is_array($payload) && !empty($payload['email'])) $email = $payload['email'];
|
if (is_array($payload) && !empty($payload['email'])) {
|
||||||
|
$email = $payload['email'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$email) {
|
if (!$email) {
|
||||||
@@ -159,7 +171,9 @@ if ($flow === 'login' && $existingId) {
|
|||||||
];
|
];
|
||||||
$target = $_SESSION['oidc_return_to'] ?? '/';
|
$target = $_SESSION['oidc_return_to'] ?? '/';
|
||||||
unset($_SESSION['oidc_return_to'], $_SESSION['oidc_flow']);
|
unset($_SESSION['oidc_return_to'], $_SESSION['oidc_flow']);
|
||||||
if (!is_string($target) || $target === '' || $target[0] !== '/') $target = '/';
|
if (!is_string($target) || $target === '' || $target[0] !== '/') {
|
||||||
|
$target = '/';
|
||||||
|
}
|
||||||
header('Location: ' . $target, true, 303);
|
header('Location: ' . $target, true, 303);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-12
@@ -12,31 +12,48 @@ require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
|||||||
require_once dirname(__DIR__, 2) . '/app/bootstrap.php';
|
require_once dirname(__DIR__, 2) . '/app/bootstrap.php';
|
||||||
require_once dirname(__DIR__, 2) . '/config/config.php';
|
require_once dirname(__DIR__, 2) . '/config/config.php';
|
||||||
|
|
||||||
function maskToken(?string $t): string {
|
function maskToken(?string $t): string
|
||||||
if (!$t) return '';
|
{
|
||||||
|
if (!$t) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
$len = strlen($t);
|
$len = strlen($t);
|
||||||
if ($len <= 12) return str_repeat('•', $len);
|
if ($len <= 12) {
|
||||||
|
return str_repeat('•', $len);
|
||||||
|
}
|
||||||
return substr($t, 0, 6) . str_repeat('•', max(0, $len - 12)) . substr($t, -6);
|
return substr($t, 0, 6) . str_repeat('•', max(0, $len - 12)) . substr($t, -6);
|
||||||
}
|
}
|
||||||
function b64url_decode_str(string $s): string|false {
|
function b64url_decode_str(string $s): string|false
|
||||||
|
{
|
||||||
$s = strtr($s, '-_', '+/');
|
$s = strtr($s, '-_', '+/');
|
||||||
$pad = strlen($s) % 4;
|
$pad = strlen($s) % 4;
|
||||||
if ($pad) $s .= str_repeat('=', 4 - $pad);
|
if ($pad) {
|
||||||
|
$s .= str_repeat('=', 4 - $pad);
|
||||||
|
}
|
||||||
return base64_decode($s, true);
|
return base64_decode($s, true);
|
||||||
}
|
}
|
||||||
function decode_jwt(string $jwt): array {
|
function decode_jwt(string $jwt): array
|
||||||
if (substr_count($jwt, '.') !== 2) return [];
|
{
|
||||||
|
if (substr_count($jwt, '.') !== 2) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
[, $payload, ] = explode('.', $jwt, 3);
|
[, $payload, ] = explode('.', $jwt, 3);
|
||||||
$json = b64url_decode_str($payload);
|
$json = b64url_decode_str($payload);
|
||||||
if ($json === false) return [];
|
if ($json === false) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
$arr = json_decode($json, true);
|
$arr = json_decode($json, true);
|
||||||
return is_array($arr) ? $arr : [];
|
return is_array($arr) ? $arr : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$env = static function(string $k, ?string $d = null): ?string {
|
$env = static function (string $k, ?string $d = null): ?string {
|
||||||
if (array_key_exists($k, $_ENV) && $_ENV[$k] !== '') return (string)$_ENV[$k];
|
if (array_key_exists($k, $_ENV) && $_ENV[$k] !== '') {
|
||||||
|
return (string)$_ENV[$k];
|
||||||
|
}
|
||||||
$v = getenv($k);
|
$v = getenv($k);
|
||||||
if ($v !== false && $v !== '') return (string)$v;
|
if ($v !== false && $v !== '') {
|
||||||
|
return (string)$v;
|
||||||
|
}
|
||||||
return $d;
|
return $d;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -72,7 +89,9 @@ if ($debugEnabled && $claims === [] && $accTok && $issuer) {
|
|||||||
curl_close($ch);
|
curl_close($ch);
|
||||||
if ($resp !== false && $code === 200) {
|
if ($resp !== false && $code === 200) {
|
||||||
$tmp = json_decode((string)$resp, true);
|
$tmp = json_decode((string)$resp, true);
|
||||||
if (is_array($tmp)) $claims = $tmp;
|
if (is_array($tmp)) {
|
||||||
|
$claims = $tmp;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+17
-7
@@ -1,23 +1,33 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
if (session_status() !== PHP_SESSION_ACTIVE) session_start();
|
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
require_once dirname(__DIR__, 2) . '/vendor/autoload.php';
|
||||||
require_once dirname(__DIR__, 2) . '/app/bootstrap.php';
|
require_once dirname(__DIR__, 2) . '/app/bootstrap.php';
|
||||||
require_once dirname(__DIR__, 2) . '/config/config.php';
|
require_once dirname(__DIR__, 2) . '/config/config.php';
|
||||||
|
|
||||||
if (!function_exists('env')) {
|
if (!function_exists('env')) {
|
||||||
function env(string $key, ?string $default = null): ?string {
|
function env(string $key, ?string $default = null): ?string
|
||||||
if (array_key_exists($key, $_ENV) && $_ENV[$key] !== '') return (string)$_ENV[$key];
|
{
|
||||||
|
if (array_key_exists($key, $_ENV) && $_ENV[$key] !== '') {
|
||||||
|
return (string)$_ENV[$key];
|
||||||
|
}
|
||||||
$v = getenv($key);
|
$v = getenv($key);
|
||||||
if ($v !== false && $v !== '') return (string)$v;
|
if ($v !== false && $v !== '') {
|
||||||
|
return (string)$v;
|
||||||
|
}
|
||||||
return $default;
|
return $default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$flow = $_GET['flow'] ?? 'login'; // 'login' ou 'register'
|
$flow = $_GET['flow'] ?? 'login'; // 'login' ou 'register'
|
||||||
if (!in_array($flow, ['login','register'], true)) $flow = 'login';
|
if (!in_array($flow, ['login','register'], true)) {
|
||||||
|
$flow = 'login';
|
||||||
|
}
|
||||||
|
|
||||||
// return_to (URL relative uniquement)
|
// return_to (URL relative uniquement)
|
||||||
$defaultReturn = '/';
|
$defaultReturn = '/';
|
||||||
@@ -29,8 +39,8 @@ $_SESSION['oidc_flow'] = $flow;
|
|||||||
$_SESSION['oidc_return_to'] = $returnTo;
|
$_SESSION['oidc_return_to'] = $returnTo;
|
||||||
|
|
||||||
// --- OIDC conf ---
|
// --- OIDC conf ---
|
||||||
$issuer = rtrim((string)env('OIDC_ISSUER',''), '/');
|
$issuer = rtrim((string)env('OIDC_ISSUER', ''), '/');
|
||||||
$clientId = (string)env('OIDC_CLIENT_ID','');
|
$clientId = (string)env('OIDC_CLIENT_ID', '');
|
||||||
$redirectUri = (string)(env('OIDC_REDIRECT_URI') ?: url('oidc/callback'));
|
$redirectUri = (string)(env('OIDC_REDIRECT_URI') ?: url('oidc/callback'));
|
||||||
if (!$issuer || !$clientId || !$redirectUri) {
|
if (!$issuer || !$clientId || !$redirectUri) {
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
|
|||||||
+50
-48
@@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
define('BASE_PATH', realpath(__DIR__ . '/../'));
|
define('BASE_PATH', realpath(__DIR__ . '/../'));
|
||||||
|
|
||||||
require_once BASE_PATH . '/src/db.php';
|
require_once BASE_PATH . '/src/db.php';
|
||||||
@@ -50,7 +52,7 @@ switch ($action) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
header("Location: route.php");
|
header('Location: route.php');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,13 +64,13 @@ switch ($action) {
|
|||||||
|
|
||||||
case 'view':
|
case 'view':
|
||||||
if (!$id) {
|
if (!$id) {
|
||||||
echo "ID manquant.";
|
echo 'ID manquant.';
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$post = $postManager->get($id);
|
$post = $postManager->get($id);
|
||||||
if (!$post) {
|
if (!$post) {
|
||||||
echo "Post introuvable.";
|
echo 'Post introuvable.';
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,60 +81,60 @@ switch ($action) {
|
|||||||
if ($id) {
|
if ($id) {
|
||||||
$postManager->delete($id);
|
$postManager->delete($id);
|
||||||
}
|
}
|
||||||
header("Location: route.php");
|
header('Location: route.php');
|
||||||
exit;
|
exit;
|
||||||
|
|
||||||
case 'edit':
|
case 'edit':
|
||||||
if (!$id) {
|
if (!$id) {
|
||||||
echo "ID manquant.";
|
echo 'ID manquant.';
|
||||||
exit;
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$post = $postManager->get($id);
|
||||||
|
if (!$post) {
|
||||||
|
echo 'Post introuvable.';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$title = $_POST['title'] ?? $post['title'];
|
||||||
|
$content = $_POST['content'] ?? $post['content'];
|
||||||
|
$published_at = $_POST['published_at'] ?? date('Y-m-d\TH:i', strtotime($post['created_at']));
|
||||||
|
$published = isset($_POST['published']) ? true : $post['is_published'];
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
if (trim($title) === '') {
|
||||||
|
$errors[] = 'Le titre est obligatoire.';
|
||||||
}
|
}
|
||||||
|
|
||||||
$post = $postManager->get($id);
|
if (empty($errors)) {
|
||||||
if (!$post) {
|
$published_at_sql = str_replace('T', ' ', $_POST['published_at']);
|
||||||
echo "Post introuvable.";
|
$postManager->update($id, $title, $content, $published_at_sql, $published);
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
$title = $_POST['title'] ?? $post['title'];
|
if (!empty($_FILES['files']['name'][0])) {
|
||||||
$content = $_POST['content'] ?? $post['content'];
|
foreach ($_FILES['files']['tmp_name'] as $i => $tmpName) {
|
||||||
$published_at = $_POST['published_at'] ?? date('Y-m-d\TH:i', strtotime($post['created_at']));
|
if ($_FILES['files']['error'][$i] === UPLOAD_ERR_OK) {
|
||||||
$published = isset($_POST['published']) ? true : $post['is_published'];
|
$file = [
|
||||||
$errors = [];
|
'name' => $_FILES['files']['name'][$i],
|
||||||
|
'type' => $_FILES['files']['type'][$i],
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
'tmp_name' => $_FILES['files']['tmp_name'][$i],
|
||||||
if (trim($title) === '') {
|
'error' => $_FILES['files']['error'][$i],
|
||||||
$errors[] = 'Le titre est obligatoire.';
|
'size' => $_FILES['files']['size'][$i],
|
||||||
}
|
];
|
||||||
|
$fileManager->upload($id, $file);
|
||||||
if (empty($errors)) {
|
|
||||||
$published_at_sql = str_replace('T', ' ', $_POST['published_at']);
|
|
||||||
$postManager->update($id, $title, $content, $published_at_sql, $published);
|
|
||||||
|
|
||||||
if (!empty($_FILES['files']['name'][0])) {
|
|
||||||
foreach ($_FILES['files']['tmp_name'] as $i => $tmpName) {
|
|
||||||
if ($_FILES['files']['error'][$i] === UPLOAD_ERR_OK) {
|
|
||||||
$file = [
|
|
||||||
'name' => $_FILES['files']['name'][$i],
|
|
||||||
'type' => $_FILES['files']['type'][$i],
|
|
||||||
'tmp_name' => $_FILES['files']['tmp_name'][$i],
|
|
||||||
'error' => $_FILES['files']['error'][$i],
|
|
||||||
'size' => $_FILES['files']['size'][$i],
|
|
||||||
];
|
|
||||||
$fileManager->upload($id, $file);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
header("Location: route.php?action=view&id=$id");
|
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
$formAction = "route.php?action=edit&id=$id";
|
header("Location: route.php?action=view&id=$id");
|
||||||
$action = 'edit';
|
exit;
|
||||||
include BASE_PATH . '/templates/post_form.php';
|
}
|
||||||
break;
|
}
|
||||||
|
|
||||||
|
$formAction = "route.php?action=edit&id=$id";
|
||||||
|
$action = 'edit';
|
||||||
|
include BASE_PATH . '/templates/post_form.php';
|
||||||
|
break;
|
||||||
|
|
||||||
case 'list':
|
case 'list':
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -1,25 +1,31 @@
|
|||||||
<?php // includes/ConfigRepo.php
|
<?php
|
||||||
|
|
||||||
|
// includes/ConfigRepo.php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
function config_repo_get(): array {
|
function config_repo_get(): array
|
||||||
|
{
|
||||||
$pdo = db();
|
$pdo = db();
|
||||||
$row = $pdo->query("SELECT * FROM app_config WHERE id=1")->fetch(PDO::FETCH_ASSOC);
|
$row = $pdo->query('SELECT * FROM app_config WHERE id=1')->fetch(PDO::FETCH_ASSOC);
|
||||||
if (!$row) { return [
|
if (!$row) {
|
||||||
'allow_password'=>true,'allow_oidc'=>false,'registrations_open'=>true,
|
return [
|
||||||
'oidc_issuer'=>null,'oidc_name'=>null,'oidc_client_id'=>null,'oidc_client_secret'=>null,'oidc_redirect_uri'=>null
|
'allow_password' => true,'allow_oidc' => false,'registrations_open' => true,
|
||||||
]; }
|
'oidc_issuer' => null,'oidc_name' => null,'oidc_client_id' => null,'oidc_client_secret' => null,'oidc_redirect_uri' => null
|
||||||
|
];
|
||||||
|
}
|
||||||
return $row;
|
return $row;
|
||||||
}
|
}
|
||||||
|
|
||||||
function config_repo_save(array $in): void {
|
function config_repo_save(array $in): void
|
||||||
|
{
|
||||||
$pdo = db();
|
$pdo = db();
|
||||||
$sql = "INSERT INTO app_config
|
$sql = 'INSERT INTO app_config
|
||||||
(id, allow_password, allow_oidc, registrations_open, oidc_issuer, oidc_name, oidc_client_id, oidc_client_secret, oidc_redirect_uri, updated_at)
|
(id, allow_password, allow_oidc, registrations_open, oidc_issuer, oidc_name, oidc_client_id, oidc_client_secret, oidc_redirect_uri, updated_at)
|
||||||
VALUES (1,:pw,:oidc,:open,:iss,:name,:cid,:sec,:redir, now())
|
VALUES (1,:pw,:oidc,:open,:iss,:name,:cid,:sec,:redir, now())
|
||||||
ON CONFLICT (id) DO UPDATE SET
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
allow_password=:pw, allow_oidc=:oidc, registrations_open=:open,
|
allow_password=:pw, allow_oidc=:oidc, registrations_open=:open,
|
||||||
oidc_issuer=:iss, oidc_name=:name, oidc_client_id=:cid, oidc_client_secret=:sec, oidc_redirect_uri=:redir,
|
oidc_issuer=:iss, oidc_name=:name, oidc_client_id=:cid, oidc_client_secret=:sec, oidc_redirect_uri=:redir,
|
||||||
updated_at=now()";
|
updated_at=now()';
|
||||||
$stmt = $pdo->prepare($sql);
|
$stmt = $pdo->prepare($sql);
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
':pw' => (bool)$in['allow_password'],
|
':pw' => (bool)$in['allow_password'],
|
||||||
@@ -29,7 +35,7 @@ function config_repo_save(array $in): void {
|
|||||||
':name' => trim((string)($in['oidc_name'] ?? '')) ?: null,
|
':name' => trim((string)($in['oidc_name'] ?? '')) ?: null,
|
||||||
':cid' => trim((string)($in['oidc_client_id'] ?? '')) ?: null,
|
':cid' => trim((string)($in['oidc_client_id'] ?? '')) ?: null,
|
||||||
':sec' => trim((string)($in['oidc_client_secret'] ?? '')) ?: null,
|
':sec' => trim((string)($in['oidc_client_secret'] ?? '')) ?: null,
|
||||||
':redir'=> trim((string)($in['oidc_redirect_uri'] ?? '')) ?: null,
|
':redir' => trim((string)($in['oidc_redirect_uri'] ?? '')) ?: null,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,36 +43,59 @@ function config_repo_save(array $in): void {
|
|||||||
* Met à jour le fichier .env en conservant les autres lignes.
|
* Met à jour le fichier .env en conservant les autres lignes.
|
||||||
* $pairs = ['KEY'=>'value', ...] ; value null => supprime la clé.
|
* $pairs = ['KEY'=>'value', ...] ; value null => supprime la clé.
|
||||||
*/
|
*/
|
||||||
function env_set_pairs(string $envPath, array $pairs): void {
|
function env_set_pairs(string $envPath, array $pairs): void
|
||||||
if (!is_file($envPath)) { file_put_contents($envPath, ""); }
|
{
|
||||||
|
if (!is_file($envPath)) {
|
||||||
|
file_put_contents($envPath, '');
|
||||||
|
}
|
||||||
$lines = file($envPath, FILE_IGNORE_NEW_LINES);
|
$lines = file($envPath, FILE_IGNORE_NEW_LINES);
|
||||||
$map = [];
|
$map = [];
|
||||||
foreach ($lines as $i => $line) {
|
foreach ($lines as $i => $line) {
|
||||||
if (preg_match('/^\s*#/', $line) || trim($line)==='') { $map[$i] = $line; continue; }
|
if (preg_match('/^\s*#/', $line) || trim($line) === '') {
|
||||||
if (!str_contains($line, '=')) { $map[$i] = $line; continue; }
|
$map[$i] = $line;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!str_contains($line, '=')) {
|
||||||
|
$map[$i] = $line;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
[$k,$v] = explode('=', $line, 2);
|
[$k,$v] = explode('=', $line, 2);
|
||||||
$k = trim($k);
|
$k = trim($k);
|
||||||
if ($k==='') { $map[$i] = $line; continue; }
|
if ($k === '') {
|
||||||
|
$map[$i] = $line;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (array_key_exists($k, $pairs)) {
|
if (array_key_exists($k, $pairs)) {
|
||||||
if ($pairs[$k] === null) { $map[$i] = null; } // supprimé
|
if ($pairs[$k] === null) {
|
||||||
else { $map[$i] = $k.'='.env_quote((string)$pairs[$k]); }
|
$map[$i] = null;
|
||||||
|
} // supprimé
|
||||||
|
else {
|
||||||
|
$map[$i] = $k.'='.env_quote((string)$pairs[$k]);
|
||||||
|
}
|
||||||
unset($pairs[$k]);
|
unset($pairs[$k]);
|
||||||
} else {
|
} else {
|
||||||
$map[$i] = $line;
|
$map[$i] = $line;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// append keys restantes
|
// append keys restantes
|
||||||
foreach ($pairs as $k=>$v) {
|
foreach ($pairs as $k => $v) {
|
||||||
if ($v === null) continue;
|
if ($v === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
$map[] = $k.'='.env_quote((string)$v);
|
$map[] = $k.'='.env_quote((string)$v);
|
||||||
}
|
}
|
||||||
// re-écriture
|
// re-écriture
|
||||||
$out = [];
|
$out = [];
|
||||||
foreach ($map as $line) { if ($line === null) continue; $out[] = $line; }
|
foreach ($map as $line) {
|
||||||
|
if ($line === null) {
|
||||||
|
continue;
|
||||||
|
} $out[] = $line;
|
||||||
|
}
|
||||||
file_put_contents($envPath, implode(PHP_EOL, $out).PHP_EOL);
|
file_put_contents($envPath, implode(PHP_EOL, $out).PHP_EOL);
|
||||||
}
|
}
|
||||||
|
|
||||||
function env_quote(string $v): string {
|
function env_quote(string $v): string
|
||||||
|
{
|
||||||
if ($v === '' || preg_match('/\s|[#"\'=]/', $v)) {
|
if ($v === '' || preg_match('/\s|[#"\'=]/', $v)) {
|
||||||
// met entre guillemets et échappe
|
// met entre guillemets et échappe
|
||||||
$v = str_replace(['\\','"'], ['\\\\','\\"'], $v);
|
$v = str_replace(['\\','"'], ['\\\\','\\"'], $v);
|
||||||
@@ -75,11 +104,11 @@ function env_quote(string $v): string {
|
|||||||
return $v;
|
return $v;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensure_admin(): void {
|
function ensure_admin(): void
|
||||||
|
{
|
||||||
// adapte à ton système
|
// adapte à ton système
|
||||||
if (empty($_SESSION['user']['is_admin'])) {
|
if (empty($_SESSION['user']['is_admin'])) {
|
||||||
http_response_code(403);
|
http_response_code(403);
|
||||||
exit('Forbidden');
|
exit('Forbidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
+3
-1
@@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Domain;
|
namespace App\Domain;
|
||||||
@@ -10,5 +11,6 @@ final class User
|
|||||||
public string $email,
|
public string $email,
|
||||||
public string $passwordHash,
|
public string $passwordHash,
|
||||||
public bool $isActive = true,
|
public bool $isActive = true,
|
||||||
) {}
|
) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-8
@@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
class FileManager
|
class FileManager
|
||||||
{
|
{
|
||||||
private PDO $db;
|
private PDO $db;
|
||||||
@@ -27,10 +29,10 @@ class FileManager
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$stmt = $this->db->prepare("
|
$stmt = $this->db->prepare('
|
||||||
INSERT INTO post_files (post_id, file_type, file_path, original_name)
|
INSERT INTO post_files (post_id, file_type, file_path, original_name)
|
||||||
VALUES (:post_id, :file_type, :file_path, :original_name)
|
VALUES (:post_id, :file_type, :file_path, :original_name)
|
||||||
");
|
');
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
'post_id' => $postId,
|
'post_id' => $postId,
|
||||||
'file_type' => $type,
|
'file_type' => $type,
|
||||||
@@ -43,14 +45,14 @@ class FileManager
|
|||||||
|
|
||||||
public function getFilesForPost(int $postId): array
|
public function getFilesForPost(int $postId): array
|
||||||
{
|
{
|
||||||
$stmt = $this->db->prepare("SELECT * FROM post_files WHERE post_id = :post_id ORDER BY uploaded_at");
|
$stmt = $this->db->prepare('SELECT * FROM post_files WHERE post_id = :post_id ORDER BY uploaded_at');
|
||||||
$stmt->execute(['post_id' => $postId]);
|
$stmt->execute(['post_id' => $postId]);
|
||||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(int $fileId): bool
|
public function delete(int $fileId): bool
|
||||||
{
|
{
|
||||||
$stmt = $this->db->prepare("SELECT file_path FROM post_files WHERE id = :id");
|
$stmt = $this->db->prepare('SELECT file_path FROM post_files WHERE id = :id');
|
||||||
$stmt->execute(['id' => $fileId]);
|
$stmt->execute(['id' => $fileId]);
|
||||||
$file = $stmt->fetch(PDO::FETCH_ASSOC);
|
$file = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
@@ -63,15 +65,21 @@ class FileManager
|
|||||||
unlink($fullPath);
|
unlink($fullPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
$stmt = $this->db->prepare("DELETE FROM post_files WHERE id = :id");
|
$stmt = $this->db->prepare('DELETE FROM post_files WHERE id = :id');
|
||||||
return $stmt->execute(['id' => $fileId]);
|
return $stmt->execute(['id' => $fileId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function guessType(string $mime): string
|
private function guessType(string $mime): string
|
||||||
{
|
{
|
||||||
if (str_starts_with($mime, 'image/')) return 'image';
|
if (str_starts_with($mime, 'image/')) {
|
||||||
if (str_starts_with($mime, 'video/')) return 'video';
|
return 'image';
|
||||||
if (str_starts_with($mime, 'audio/')) return 'audio';
|
}
|
||||||
|
if (str_starts_with($mime, 'video/')) {
|
||||||
|
return 'video';
|
||||||
|
}
|
||||||
|
if (str_starts_with($mime, 'audio/')) {
|
||||||
|
return 'audio';
|
||||||
|
}
|
||||||
return 'file';
|
return 'file';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Http;
|
namespace App\Http;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Infrastructure;
|
namespace App\Infrastructure;
|
||||||
@@ -17,9 +18,11 @@ final class Database
|
|||||||
return self::$pdo;
|
return self::$pdo;
|
||||||
}
|
}
|
||||||
|
|
||||||
$get = static function (string $k, ?string $default=null): ?string {
|
$get = static function (string $k, ?string $default = null): ?string {
|
||||||
$v = getenv($k);
|
$v = getenv($k);
|
||||||
if ($v !== false && $v !== '') return (string)$v;
|
if ($v !== false && $v !== '') {
|
||||||
|
return (string)$v;
|
||||||
|
}
|
||||||
return $_ENV[$k] ?? $default;
|
return $_ENV[$k] ?? $default;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -31,7 +34,9 @@ final class Database
|
|||||||
$host = $get('DB_HOST', 'localhost');
|
$host = $get('DB_HOST', 'localhost');
|
||||||
$port = $get('DB_PORT', '5432');
|
$port = $get('DB_PORT', '5432');
|
||||||
$name = $get('DB_NAME');
|
$name = $get('DB_NAME');
|
||||||
if ($name) $dsn = sprintf('pgsql:host=%s;port=%s;dbname=%s', $host, $port, $name);
|
if ($name) {
|
||||||
|
$dsn = sprintf('pgsql:host=%s;port=%s;dbname=%s', $host, $port, $name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$dsn) {
|
if (!$dsn) {
|
||||||
@@ -80,7 +85,9 @@ final class Database
|
|||||||
$pdo->commit();
|
$pdo->commit();
|
||||||
return $ret;
|
return $ret;
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
if ($pdo->inTransaction()) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
}
|
||||||
throw $e;
|
throw $e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Infrastructure;
|
namespace App\Infrastructure;
|
||||||
@@ -19,9 +20,13 @@ final class DbAdapter
|
|||||||
$host = getenv('DB_HOST') ?: ($_ENV['DB_HOST'] ?? 'localhost');
|
$host = getenv('DB_HOST') ?: ($_ENV['DB_HOST'] ?? 'localhost');
|
||||||
$port = getenv('DB_PORT') ?: ($_ENV['DB_PORT'] ?? '5432');
|
$port = getenv('DB_PORT') ?: ($_ENV['DB_PORT'] ?? '5432');
|
||||||
$name = getenv('DB_NAME') ?: ($_ENV['DB_NAME'] ?? null);
|
$name = getenv('DB_NAME') ?: ($_ENV['DB_NAME'] ?? null);
|
||||||
if ($name) $dsn = sprintf('pgsql:host=%s;port=%s;dbname=%s', $host, $port, $name);
|
if ($name) {
|
||||||
|
$dsn = sprintf('pgsql:host=%s;port=%s;dbname=%s', $host, $port, $name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$dsn) {
|
||||||
|
throw new \RuntimeException('Aucun DSN pour initialiser PDO');
|
||||||
}
|
}
|
||||||
if (!$dsn) throw new \RuntimeException('Aucun DSN pour initialiser PDO');
|
|
||||||
return new PDO($dsn, (string)$user, (string)$pass, [
|
return new PDO($dsn, (string)$user, (string)$pass, [
|
||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Infrastructure;
|
namespace App\Infrastructure;
|
||||||
@@ -7,7 +8,9 @@ final class Session
|
|||||||
{
|
{
|
||||||
public static function startSecure(string $name): void
|
public static function startSecure(string $name): void
|
||||||
{
|
{
|
||||||
if (session_status() === PHP_SESSION_ACTIVE) return;
|
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
session_name($name);
|
session_name($name);
|
||||||
session_set_cookie_params([
|
session_set_cookie_params([
|
||||||
|
|||||||
+190
-356
File diff suppressed because it is too large
Load Diff
+9
-7
@@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
class PostManager
|
class PostManager
|
||||||
{
|
{
|
||||||
private PDO $db;
|
private PDO $db;
|
||||||
@@ -11,13 +13,13 @@ class PostManager
|
|||||||
|
|
||||||
public function getAll(): array
|
public function getAll(): array
|
||||||
{
|
{
|
||||||
$stmt = $this->db->query("SELECT * FROM posts ORDER BY created_at DESC");
|
$stmt = $this->db->query('SELECT * FROM posts ORDER BY created_at DESC');
|
||||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get(int $id): ?array
|
public function get(int $id): ?array
|
||||||
{
|
{
|
||||||
$stmt = $this->db->prepare("SELECT * FROM posts WHERE id = :id");
|
$stmt = $this->db->prepare('SELECT * FROM posts WHERE id = :id');
|
||||||
$stmt->execute(['id' => $id]);
|
$stmt->execute(['id' => $id]);
|
||||||
$post = $stmt->fetch(PDO::FETCH_ASSOC);
|
$post = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
return $post ?: null;
|
return $post ?: null;
|
||||||
@@ -25,10 +27,10 @@ class PostManager
|
|||||||
|
|
||||||
public function create(string $title, string $content, string $published_at): int
|
public function create(string $title, string $content, string $published_at): int
|
||||||
{
|
{
|
||||||
$stmt = $this->db->prepare("
|
$stmt = $this->db->prepare('
|
||||||
INSERT INTO posts (title, content, created_at, is_published)
|
INSERT INTO posts (title, content, created_at, is_published)
|
||||||
VALUES (:title, :content, :published_at, true)
|
VALUES (:title, :content, :published_at, true)
|
||||||
");
|
');
|
||||||
$stmt->execute([
|
$stmt->execute([
|
||||||
'title' => $title,
|
'title' => $title,
|
||||||
'content' => $content,
|
'content' => $content,
|
||||||
@@ -40,7 +42,7 @@ class PostManager
|
|||||||
|
|
||||||
public function update(int $id, string $title, string $content, string $published_at, bool $published): bool
|
public function update(int $id, string $title, string $content, string $published_at, bool $published): bool
|
||||||
{
|
{
|
||||||
$stmt = $this->db->prepare("
|
$stmt = $this->db->prepare('
|
||||||
UPDATE posts
|
UPDATE posts
|
||||||
SET title = :title,
|
SET title = :title,
|
||||||
content = :content,
|
content = :content,
|
||||||
@@ -48,7 +50,7 @@ class PostManager
|
|||||||
is_published = :published,
|
is_published = :published,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = :id
|
WHERE id = :id
|
||||||
");
|
');
|
||||||
return $stmt->execute([
|
return $stmt->execute([
|
||||||
'id' => $id,
|
'id' => $id,
|
||||||
'title' => $title,
|
'title' => $title,
|
||||||
@@ -61,7 +63,7 @@ class PostManager
|
|||||||
|
|
||||||
public function delete(int $id): bool
|
public function delete(int $id): bool
|
||||||
{
|
{
|
||||||
$stmt = $this->db->prepare("DELETE FROM posts WHERE id = :id");
|
$stmt = $this->db->prepare('DELETE FROM posts WHERE id = :id');
|
||||||
return $stmt->execute(['id' => $id]);
|
return $stmt->execute(['id' => $id]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Repository;
|
namespace App\Repository;
|
||||||
@@ -7,39 +8,47 @@ use PDO;
|
|||||||
|
|
||||||
final class DictionaryRepository
|
final class DictionaryRepository
|
||||||
{
|
{
|
||||||
public function __construct(private PDO $pdo) {}
|
public function __construct(private PDO $pdo)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public function getEntityByCode(string $code): ?array {
|
public function getEntityByCode(string $code): ?array
|
||||||
|
{
|
||||||
$st = $this->pdo->prepare('SELECT * FROM dd_entities WHERE code = :c AND is_active IS TRUE');
|
$st = $this->pdo->prepare('SELECT * FROM dd_entities WHERE code = :c AND is_active IS TRUE');
|
||||||
$st->execute([':c'=>$code]);
|
$st->execute([':c' => $code]);
|
||||||
$e = $st->fetch(PDO::FETCH_ASSOC);
|
$e = $st->fetch(PDO::FETCH_ASSOC);
|
||||||
if (!$e) return null;
|
if (!$e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
$e['fields'] = $this->getFields((int)$e['id']);
|
$e['fields'] = $this->getFields((int)$e['id']);
|
||||||
$e['rules'] = $this->getRules((int)$e['id']);
|
$e['rules'] = $this->getRules((int)$e['id']);
|
||||||
return $e;
|
return $e;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getFields(int $entityId): array {
|
public function getFields(int $entityId): array
|
||||||
|
{
|
||||||
$st = $this->pdo->prepare('SELECT * FROM dd_fields WHERE entity_id = :id ORDER BY ui_order NULLS LAST, id');
|
$st = $this->pdo->prepare('SELECT * FROM dd_fields WHERE entity_id = :id ORDER BY ui_order NULLS LAST, id');
|
||||||
$st->execute([':id'=>$entityId]);
|
$st->execute([':id' => $entityId]);
|
||||||
return $st->fetchAll(PDO::FETCH_ASSOC);
|
return $st->fetchAll(PDO::FETCH_ASSOC);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getRules(int $entityId): array {
|
public function getRules(int $entityId): array
|
||||||
|
{
|
||||||
$st = $this->pdo->prepare('SELECT * FROM dd_rules WHERE entity_id = :id AND active IS TRUE');
|
$st = $this->pdo->prepare('SELECT * FROM dd_rules WHERE entity_id = :id AND active IS TRUE');
|
||||||
$st->execute([':id'=>$entityId]);
|
$st->execute([':id' => $entityId]);
|
||||||
return $st->fetchAll(PDO::FETCH_ASSOC);
|
return $st->fetchAll(PDO::FETCH_ASSOC);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getEnum(string $name): array {
|
public function getEnum(string $name): array
|
||||||
|
{
|
||||||
$st = $this->pdo->prepare('
|
$st = $this->pdo->prepare('
|
||||||
SELECT ev.code, ev.label
|
SELECT ev.code, ev.label
|
||||||
FROM dd_enums e JOIN dd_enum_values ev ON ev.enum_id = e.id
|
FROM dd_enums e JOIN dd_enum_values ev ON ev.enum_id = e.id
|
||||||
WHERE e.name = :n AND ev.active IS TRUE
|
WHERE e.name = :n AND ev.active IS TRUE
|
||||||
ORDER BY ev.sort_order, ev.id
|
ORDER BY ev.sort_order, ev.id
|
||||||
');
|
');
|
||||||
$st->execute([':n'=>$name]);
|
$st->execute([':n' => $name]);
|
||||||
return $st->fetchAll(PDO::FETCH_ASSOC);
|
return $st->fetchAll(PDO::FETCH_ASSOC);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Repository;
|
namespace App\Repository;
|
||||||
@@ -13,24 +14,39 @@ final class ProfileRepository
|
|||||||
public function __construct(?PDO $pdo = null)
|
public function __construct(?PDO $pdo = null)
|
||||||
{
|
{
|
||||||
// 0) DI directe
|
// 0) DI directe
|
||||||
if ($pdo instanceof PDO) { $this->pdo = $pdo; return; }
|
if ($pdo instanceof PDO) {
|
||||||
|
$this->pdo = $pdo;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 1) App\Infrastructure\Database (si elle expose quelque chose)
|
// 1) App\Infrastructure\Database (si elle expose quelque chose)
|
||||||
if (class_exists(Database::class)) {
|
if (class_exists(Database::class)) {
|
||||||
if (method_exists(Database::class, 'pdo')) {
|
if (method_exists(Database::class, 'pdo')) {
|
||||||
$try = Database::pdo();
|
$try = Database::pdo();
|
||||||
if ($try instanceof PDO) { $this->pdo = $try; return; }
|
if ($try instanceof PDO) {
|
||||||
|
$this->pdo = $try;
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (method_exists(Database::class, 'getPdo')) {
|
if (method_exists(Database::class, 'getPdo')) {
|
||||||
$try = Database::getPdo();
|
$try = Database::getPdo();
|
||||||
if ($try instanceof PDO) { $this->pdo = $try; return; }
|
if ($try instanceof PDO) {
|
||||||
|
$this->pdo = $try;
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (method_exists(Database::class, 'getInstance')) {
|
if (method_exists(Database::class, 'getInstance')) {
|
||||||
$db = Database::getInstance();
|
$db = Database::getInstance();
|
||||||
if ($db instanceof PDO) { $this->pdo = $db; return; }
|
if ($db instanceof PDO) {
|
||||||
|
$this->pdo = $db;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (is_object($db) && method_exists($db, 'pdo')) {
|
if (is_object($db) && method_exists($db, 'pdo')) {
|
||||||
$try = $db->pdo();
|
$try = $db->pdo();
|
||||||
if ($try instanceof PDO) { $this->pdo = $try; return; }
|
if ($try instanceof PDO) {
|
||||||
|
$this->pdo = $try;
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,7 +54,10 @@ final class ProfileRepository
|
|||||||
// 2) Fonction globale éventuelle
|
// 2) Fonction globale éventuelle
|
||||||
if (function_exists('db')) {
|
if (function_exists('db')) {
|
||||||
$try = db();
|
$try = db();
|
||||||
if ($try instanceof PDO) { $this->pdo = $try; return; }
|
if ($try instanceof PDO) {
|
||||||
|
$this->pdo = $try;
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Variable globale éventuelle
|
// 3) Variable globale éventuelle
|
||||||
@@ -110,7 +129,7 @@ final class ProfileRepository
|
|||||||
':slug' => $slug,
|
':slug' => $slug,
|
||||||
':label' => $label,
|
':label' => $label,
|
||||||
':desc' => $description,
|
':desc' => $description,
|
||||||
':perms' => json_encode($permissions, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),
|
':perms' => json_encode($permissions, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||||
':sys' => $isSystem,
|
':sys' => $isSystem,
|
||||||
':act' => $isActive,
|
':act' => $isActive,
|
||||||
]);
|
]);
|
||||||
@@ -125,7 +144,7 @@ final class ProfileRepository
|
|||||||
':slug' => $slug,
|
':slug' => $slug,
|
||||||
':label' => $label,
|
':label' => $label,
|
||||||
':desc' => $description,
|
':desc' => $description,
|
||||||
':perms' => json_encode($permissions, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),
|
':perms' => json_encode($permissions, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||||
':sys' => $isSystem,
|
':sys' => $isSystem,
|
||||||
':act' => $isActive,
|
':act' => $isActive,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Repository;
|
namespace App\Repository;
|
||||||
@@ -8,7 +9,9 @@ use PDO;
|
|||||||
|
|
||||||
final class UserRepository
|
final class UserRepository
|
||||||
{
|
{
|
||||||
public function __construct(private PDO $pdo) {}
|
public function __construct(private PDO $pdo)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Crée (si besoin) un utilisateur OIDC.
|
* Crée (si besoin) un utilisateur OIDC.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Service;
|
namespace App\Service;
|
||||||
@@ -7,7 +8,9 @@ use App\Repository\UserRepository;
|
|||||||
|
|
||||||
final class AuthService
|
final class AuthService
|
||||||
{
|
{
|
||||||
public function __construct(private UserRepository $users) {}
|
public function __construct(private UserRepository $users)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public function canAttempt(string $email, string $ip): bool
|
public function canAttempt(string $email, string $ip): bool
|
||||||
{
|
{
|
||||||
@@ -30,12 +33,12 @@ final class AuthService
|
|||||||
$user = $this->users->findByEmail($email);
|
$user = $this->users->findByEmail($email);
|
||||||
$ok = $user && $user->isActive && password_verify($password, $user->passwordHash);
|
$ok = $user && $user->isActive && password_verify($password, $user->passwordHash);
|
||||||
|
|
||||||
$pdo = \App\Infrastructure\Database::pdo();
|
$pdo = \App\Infrastructure\Database::pdo();
|
||||||
$st = $pdo->prepare('insert into login_attempts(email, ip, success) values(:e, :ip, :s)');
|
$st = $pdo->prepare('insert into login_attempts(email, ip, success) values(:e, :ip, :s)');
|
||||||
$st->bindValue(':e', $email, \PDO::PARAM_STR);
|
$st->bindValue(':e', $email, \PDO::PARAM_STR);
|
||||||
$st->bindValue(':ip', $ip, \PDO::PARAM_STR);
|
$st->bindValue(':ip', $ip, \PDO::PARAM_STR);
|
||||||
$st->bindValue(':s', $ok, \PDO::PARAM_BOOL);
|
$st->bindValue(':s', $ok, \PDO::PARAM_BOOL);
|
||||||
$st->execute();
|
$st->execute();
|
||||||
|
|
||||||
if ($ok) {
|
if ($ok) {
|
||||||
\App\Infrastructure\Session::regenerate();
|
\App\Infrastructure\Session::regenerate();
|
||||||
@@ -53,7 +56,9 @@ final class AuthService
|
|||||||
$st = $pdo->prepare('select id, email, password_hash, is_active from users where id = :id');
|
$st = $pdo->prepare('select id, email, password_hash, is_active from users where id = :id');
|
||||||
$st->execute([':id' => $userId]);
|
$st->execute([':id' => $userId]);
|
||||||
$row = $st->fetch(\PDO::FETCH_ASSOC);
|
$row = $st->fetch(\PDO::FETCH_ASSOC);
|
||||||
if (!$row || !(bool)$row['is_active']) return false;
|
if (!$row || !(bool)$row['is_active']) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Vérifier l’ancien mot de passe
|
// Vérifier l’ancien mot de passe
|
||||||
if (!password_verify($currentPassword, (string)$row['password_hash'])) {
|
if (!password_verify($currentPassword, (string)$row['password_hash'])) {
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ final class MailQueue
|
|||||||
|
|
||||||
if ($rows) {
|
if ($rows) {
|
||||||
// 2) Marquer locked_at + sending
|
// 2) Marquer locked_at + sending
|
||||||
$ids = array_map(static fn($r) => (int)$r['id'], $rows);
|
$ids = array_map(static fn ($r) => (int)$r['id'], $rows);
|
||||||
$in = implode(',', array_fill(0, count($ids), '?'));
|
$in = implode(',', array_fill(0, count($ids), '?'));
|
||||||
|
|
||||||
$up = $this->pdo->prepare(
|
$up = $this->pdo->prepare(
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ use App\Infrastructure\Database;
|
|||||||
use PHPMailer\PHPMailer\PHPMailer;
|
use PHPMailer\PHPMailer\PHPMailer;
|
||||||
use PHPMailer\PHPMailer\Exception as MailException;
|
use PHPMailer\PHPMailer\Exception as MailException;
|
||||||
use PDO;
|
use PDO;
|
||||||
use DateInterval;
|
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Service;
|
namespace App\Service;
|
||||||
@@ -7,21 +8,30 @@ use App\Repository\DictionaryRepository;
|
|||||||
|
|
||||||
final class UiFormRenderer
|
final class UiFormRenderer
|
||||||
{
|
{
|
||||||
public function __construct(private DictionaryRepository $dict) {}
|
public function __construct(private DictionaryRepository $dict)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public function renderControls(string $entityCode, array $values = []): string {
|
public function renderControls(string $entityCode, array $values = []): string
|
||||||
|
{
|
||||||
$e = $this->dict->getEntityByCode($entityCode);
|
$e = $this->dict->getEntityByCode($entityCode);
|
||||||
if (!$e) return '<div class="alert alert-danger">Entité inconnue</div>';
|
if (!$e) {
|
||||||
|
return '<div class="alert alert-danger">Entité inconnue</div>';
|
||||||
|
}
|
||||||
|
|
||||||
$html = '';
|
$html = '';
|
||||||
foreach ($e['fields'] as $f) {
|
foreach ($e['fields'] as $f) {
|
||||||
if (!$f['form_visible']) continue;
|
if (!$f['form_visible']) {
|
||||||
if ($f['read_only']) continue;
|
continue;
|
||||||
|
}
|
||||||
|
if ($f['read_only']) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$name = $f['code'];
|
$name = $f['code'];
|
||||||
$label = $f['label'];
|
$label = $f['label'];
|
||||||
$help = $f['help_text'] ?? '';
|
$help = $f['help_text'] ?? '';
|
||||||
$widget= $f['ui_widget'] ?? 'text';
|
$widget = $f['ui_widget'] ?? 'text';
|
||||||
$val = $values[$name] ?? '';
|
$val = $values[$name] ?? '';
|
||||||
|
|
||||||
$html .= '<div class="mb-3">';
|
$html .= '<div class="mb-3">';
|
||||||
@@ -41,7 +51,7 @@ final class UiFormRenderer
|
|||||||
'email' => 'email',
|
'email' => 'email',
|
||||||
'number' => 'number',
|
'number' => 'number',
|
||||||
'date' => 'date',
|
'date' => 'date',
|
||||||
'checkbox'=> 'checkbox',
|
'checkbox' => 'checkbox',
|
||||||
default => 'text',
|
default => 'text',
|
||||||
};
|
};
|
||||||
if ($type === 'checkbox') {
|
if ($type === 'checkbox') {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Service;
|
namespace App\Service;
|
||||||
@@ -7,12 +8,17 @@ use App\Repository\DictionaryRepository;
|
|||||||
|
|
||||||
final class Validator
|
final class Validator
|
||||||
{
|
{
|
||||||
public function __construct(private DictionaryRepository $dict) {}
|
public function __construct(private DictionaryRepository $dict)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public function validate(string $entityCode, array $payload): array {
|
public function validate(string $entityCode, array $payload): array
|
||||||
|
{
|
||||||
$errors = [];
|
$errors = [];
|
||||||
$e = $this->dict->getEntityByCode($entityCode);
|
$e = $this->dict->getEntityByCode($entityCode);
|
||||||
if (!$e) return ['_global' => ['Entité inconnue']];
|
if (!$e) {
|
||||||
|
return ['_global' => ['Entité inconnue']];
|
||||||
|
}
|
||||||
|
|
||||||
// Index les champs
|
// Index les champs
|
||||||
$fields = [];
|
$fields = [];
|
||||||
@@ -30,7 +36,9 @@ final class Validator
|
|||||||
|
|
||||||
switch ($type) {
|
switch ($type) {
|
||||||
case 'required':
|
case 'required':
|
||||||
if ($code && ($v === null || $v === '')) $errors[$code][] = $msg;
|
if ($code && ($v === null || $v === '')) {
|
||||||
|
$errors[$code][] = $msg;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'regex':
|
case 'regex':
|
||||||
if ($code && $v !== null && $v !== '' && !preg_match('#'.$val.'#u', (string)$v)) {
|
if ($code && $v !== null && $v !== '' && !preg_match('#'.$val.'#u', (string)$v)) {
|
||||||
@@ -38,16 +46,22 @@ final class Validator
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'min':
|
case 'min':
|
||||||
if ($code && is_numeric($v) && (float)$v < (float)$val) $errors[$code][] = $msg;
|
if ($code && is_numeric($v) && (float)$v < (float)$val) {
|
||||||
|
$errors[$code][] = $msg;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'max':
|
case 'max':
|
||||||
if ($code && is_numeric($v) && (float)$v > (float)$val) $errors[$code][] = $msg;
|
if ($code && is_numeric($v) && (float)$v > (float)$val) {
|
||||||
|
$errors[$code][] = $msg;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'between':
|
case 'between':
|
||||||
if ($code && is_numeric($v)) {
|
if ($code && is_numeric($v)) {
|
||||||
[$a,$b] = array_map('floatval', explode(',', $val));
|
[$a,$b] = array_map('floatval', explode(',', $val));
|
||||||
$fv = (float)$v;
|
$fv = (float)$v;
|
||||||
if ($fv < $a || $fv > $b) $errors[$code][] = $msg;
|
if ($fv < $a || $fv > $b) {
|
||||||
|
$errors[$code][] = $msg;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'unique':
|
case 'unique':
|
||||||
|
|||||||
+6
-2
@@ -1,10 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
use Jumbojett\OpenIDConnectClient;
|
use Jumbojett\OpenIDConnectClient;
|
||||||
|
|
||||||
require_once BASE_PATH . '/vendor/autoload.php';
|
require_once BASE_PATH . '/vendor/autoload.php';
|
||||||
session_start();
|
session_start();
|
||||||
|
|
||||||
function require_auth() {
|
function require_auth()
|
||||||
|
{
|
||||||
if (!isset($_SESSION['user'])) {
|
if (!isset($_SESSION['user'])) {
|
||||||
// Redirige vers la page de login
|
// Redirige vers la page de login
|
||||||
header('Location: /auth/login.php');
|
header('Location: /auth/login.php');
|
||||||
@@ -12,7 +15,8 @@ function require_auth() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_oidc_client(): OpenIDConnectClient {
|
function get_oidc_client(): OpenIDConnectClient
|
||||||
|
{
|
||||||
$oidc = new OpenIDConnectClient(
|
$oidc = new OpenIDConnectClient(
|
||||||
'https://idp.a5l.fr/realms/master',
|
'https://idp.a5l.fr/realms/master',
|
||||||
'varlog-client-id',
|
'varlog-client-id',
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
require_once BASE_PATH . '/config/config.php';
|
require_once BASE_PATH . '/config/config.php';
|
||||||
|
|
||||||
// Comment récupérer les valeurs de .env
|
// Comment récupérer les valeurs de .env
|
||||||
|
|||||||
+4
-1
@@ -1,6 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
function vd($var, ...$moreVars) {
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
function vd($var, ...$moreVars)
|
||||||
|
{
|
||||||
ob_start();
|
ob_start();
|
||||||
var_dump($var, ...$moreVars);
|
var_dump($var, ...$moreVars);
|
||||||
$output = ob_get_clean();
|
$output = ob_get_clean();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
// projet : mug.a5l.fr
|
// projet : mug.a5l.fr
|
||||||
// fichier : includes/mailer.php
|
// fichier : includes/mailer.php
|
||||||
// version : 20251011
|
// version : 20251011
|
||||||
@@ -10,24 +11,35 @@ use PHPMailer\PHPMailer\Exception;
|
|||||||
require_once dirname(__DIR__) . '/vendor/autoload.php';
|
require_once dirname(__DIR__) . '/vendor/autoload.php';
|
||||||
|
|
||||||
if (!function_exists('env')) {
|
if (!function_exists('env')) {
|
||||||
function env(string $key, ?string $default = null): ?string {
|
function env(string $key, ?string $default = null): ?string
|
||||||
if (array_key_exists($key, $_ENV) && $_ENV[$key] !== '') return (string)$_ENV[$key];
|
{
|
||||||
|
if (array_key_exists($key, $_ENV) && $_ENV[$key] !== '') {
|
||||||
|
return (string)$_ENV[$key];
|
||||||
|
}
|
||||||
$v = getenv($key);
|
$v = getenv($key);
|
||||||
if ($v !== false && $v !== '') return (string)$v;
|
if ($v !== false && $v !== '') {
|
||||||
|
return (string)$v;
|
||||||
|
}
|
||||||
return $default;
|
return $default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!function_exists('db')) {
|
if (!function_exists('db')) {
|
||||||
function db(): \PDO { return \App\Infrastructure\Database::get(); }
|
function db(): \PDO
|
||||||
|
{
|
||||||
|
return \App\Infrastructure\Database::get();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Anti-abus simple : 1 envoi / 5 min et 5 en 12 h par destinataire (status in ('sent','queued')).
|
* Anti-abus simple : 1 envoi / 5 min et 5 en 12 h par destinataire (status in ('sent','queued')).
|
||||||
*/
|
*/
|
||||||
function mailer_can_send(string $email, int $coolMin = 5, int $maxPer12h = 5): array {
|
function mailer_can_send(string $email, int $coolMin = 5, int $maxPer12h = 5): array
|
||||||
|
{
|
||||||
// bypass complet si désactivé
|
// bypass complet si désactivé
|
||||||
$enabled = (int) (env('SMTP_RATE_LIMIT_ENABLE', '1'));
|
$enabled = (int) (env('SMTP_RATE_LIMIT_ENABLE', '1'));
|
||||||
if ($enabled === 0) return [true, ''];
|
if ($enabled === 0) {
|
||||||
|
return [true, ''];
|
||||||
|
}
|
||||||
|
|
||||||
$pdo = db();
|
$pdo = db();
|
||||||
|
|
||||||
@@ -38,8 +50,10 @@ function mailer_can_send(string $email, int $coolMin = 5, int $maxPer12h = 5): a
|
|||||||
AND status IN ('sent','queued')
|
AND status IN ('sent','queued')
|
||||||
LIMIT 1";
|
LIMIT 1";
|
||||||
$stmt = $pdo->prepare($q1);
|
$stmt = $pdo->prepare($q1);
|
||||||
$stmt->execute([':e'=>$email, ':cool'=>sprintf('%d minutes', $coolMin)]);
|
$stmt->execute([':e' => $email, ':cool' => sprintf('%d minutes', $coolMin)]);
|
||||||
if ($stmt->fetchColumn()) return [false, "Un email vient d’être envoyé. Réessayez dans {$coolMin} min."];
|
if ($stmt->fetchColumn()) {
|
||||||
|
return [false, "Un email vient d’être envoyé. Réessayez dans {$coolMin} min."];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plafond 12h (actif seulement si >0)
|
// Plafond 12h (actif seulement si >0)
|
||||||
@@ -48,8 +62,10 @@ function mailer_can_send(string $email, int $coolMin = 5, int $maxPer12h = 5): a
|
|||||||
WHERE to_email = :e AND created_at >= NOW() - INTERVAL '12 hours'
|
WHERE to_email = :e AND created_at >= NOW() - INTERVAL '12 hours'
|
||||||
AND status IN ('sent','queued')";
|
AND status IN ('sent','queued')";
|
||||||
$stmt = $pdo->prepare($q2);
|
$stmt = $pdo->prepare($q2);
|
||||||
$stmt->execute([':e'=>$email]);
|
$stmt->execute([':e' => $email]);
|
||||||
if ((int)$stmt->fetchColumn() >= $maxPer12h) return [false, 'Quota atteint. Réessayez plus tard.'];
|
if ((int)$stmt->fetchColumn() >= $maxPer12h) {
|
||||||
|
return [false, 'Quota atteint. Réessayez plus tard.'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [true, ''];
|
return [true, ''];
|
||||||
@@ -64,9 +80,12 @@ function mailer_can_send(string $email, int $coolMin = 5, int $maxPer12h = 5): a
|
|||||||
* @param string|null $text corps texte brut (optionnel, auto-généré si null)
|
* @param string|null $text corps texte brut (optionnel, auto-généré si null)
|
||||||
* @param array $opts ['reply_to'=>['email','name']]
|
* @param array $opts ['reply_to'=>['email','name']]
|
||||||
*/
|
*/
|
||||||
function envoyer_mail_smtp(string $to, string $subject, string $html, ?string $text = null, array $opts = []): bool {
|
function envoyer_mail_smtp(string $to, string $subject, string $html, ?string $text = null, array $opts = []): bool
|
||||||
|
{
|
||||||
[$ok, $msg] = mailer_can_send($to, (int)env('SMTP_COOLDOWN_MINUTES', '5'), (int)env('SMTP_MAX_PER_12H', '5'));
|
[$ok, $msg] = mailer_can_send($to, (int)env('SMTP_COOLDOWN_MINUTES', '5'), (int)env('SMTP_MAX_PER_12H', '5'));
|
||||||
if (!$ok) throw new RuntimeException($msg);
|
if (!$ok) {
|
||||||
|
throw new RuntimeException($msg);
|
||||||
|
}
|
||||||
|
|
||||||
$pdo = db();
|
$pdo = db();
|
||||||
$pdo->beginTransaction();
|
$pdo->beginTransaction();
|
||||||
@@ -87,7 +106,9 @@ function envoyer_mail_smtp(string $to, string $subject, string $html, ?string $t
|
|||||||
$rowId = (int)$stmt->fetchColumn();
|
$rowId = (int)$stmt->fetchColumn();
|
||||||
$pdo->commit();
|
$pdo->commit();
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
if ($pdo->inTransaction()) $pdo->rollBack();
|
if ($pdo->inTransaction()) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
}
|
||||||
throw $e;
|
throw $e;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,12 +121,15 @@ function envoyer_mail_smtp(string $to, string $subject, string $html, ?string $t
|
|||||||
$mail->Username = (string)env('SMTP_USER', '');
|
$mail->Username = (string)env('SMTP_USER', '');
|
||||||
$mail->Password = (string)env('SMTP_PASS', '');
|
$mail->Password = (string)env('SMTP_PASS', '');
|
||||||
$secure = strtolower((string)env('SMTP_SECURE', 'tls'));
|
$secure = strtolower((string)env('SMTP_SECURE', 'tls'));
|
||||||
if ($secure === 'ssl') $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
|
if ($secure === 'ssl') {
|
||||||
elseif ($secure === 'tls') $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
|
$mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
|
||||||
|
} elseif ($secure === 'tls') {
|
||||||
|
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
|
||||||
|
}
|
||||||
|
|
||||||
$mail->SMTPKeepAlive = true; // réutilise la connexion
|
$mail->SMTPKeepAlive = true; // réutilise la connexion
|
||||||
$mail->Timeout = 30; // évite les blocages longs
|
$mail->Timeout = 30; // évite les blocages longs
|
||||||
$mail->SMTPOptions = ['ssl'=>['verify_peer'=>true,'verify_peer_name'=>true,'allow_self_signed'=>false]];
|
$mail->SMTPOptions = ['ssl' => ['verify_peer' => true,'verify_peer_name' => true,'allow_self_signed' => false]];
|
||||||
|
|
||||||
$mail->CharSet = 'UTF-8';
|
$mail->CharSet = 'UTF-8';
|
||||||
$mail->isHTML(true);
|
$mail->isHTML(true);
|
||||||
@@ -138,12 +162,11 @@ function envoyer_mail_smtp(string $to, string $subject, string $html, ?string $t
|
|||||||
|
|
||||||
$mail->send();
|
$mail->send();
|
||||||
|
|
||||||
$pdo->prepare("UPDATE journal_smtp SET status='sent', sent_at=NOW() WHERE id=:id")->execute([':id'=>$rowId]);
|
$pdo->prepare("UPDATE journal_smtp SET status='sent', sent_at=NOW() WHERE id=:id")->execute([':id' => $rowId]);
|
||||||
return true;
|
return true;
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$pdo->prepare("UPDATE journal_smtp SET status='error', error_message=:err, sent_at=NOW() WHERE id=:id")
|
$pdo->prepare("UPDATE journal_smtp SET status='error', error_message=:err, sent_at=NOW() WHERE id=:id")
|
||||||
->execute([':id'=>$rowId, ':err'=>substr($e->getMessage(),0,1000)]);
|
->execute([':id' => $rowId, ':err' => substr($e->getMessage(), 0, 1000)]);
|
||||||
throw new RuntimeException('Envoi email impossible: '.$e->getMessage());
|
throw new RuntimeException('Envoi email impossible: '.$e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,8 +16,8 @@
|
|||||||
// Créer une instance de MessageManager avec le fichier de base de données SQLite
|
// Créer une instance de MessageManager avec le fichier de base de données SQLite
|
||||||
$messageManager = new ace\MessageManager('database.db');
|
$messageManager = new ace\MessageManager('database.db');
|
||||||
|
|
||||||
if ($messageManager->sessionAlready()) {
|
if ($messageManager->sessionAlready()) {
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="dropdown text-end">
|
<div class="dropdown text-end">
|
||||||
<a href="#" class="d-block link-dark text-decoration-none dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
<a href="#" class="d-block link-dark text-decoration-none dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
@@ -22,9 +22,9 @@ ob_start();
|
|||||||
<div class="card-text text-body">
|
<div class="card-text text-body">
|
||||||
<?php
|
<?php
|
||||||
$html = $Parsedown->text($post['content']);
|
$html = $Parsedown->text($post['content']);
|
||||||
$preview = mb_strimwidth(strip_tags($html), 0, 300, '…');
|
$preview = mb_strimwidth(strip_tags($html), 0, 300, '…');
|
||||||
echo '<p>' . $preview . '</p>';
|
echo '<p>' . $preview . '</p>';
|
||||||
?>
|
?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-muted small mt-auto mb-2">📅 Publié le <?= date('d/m/Y', strtotime($post['created_at'])) ?></p>
|
<p class="text-muted small mt-auto mb-2">📅 Publié le <?= date('d/m/Y', strtotime($post['created_at'])) ?></p>
|
||||||
@@ -42,5 +42,5 @@ ob_start();
|
|||||||
|
|
||||||
<?php
|
<?php
|
||||||
$content = ob_get_clean();
|
$content = ob_get_clean();
|
||||||
$title = "Liste des posts";
|
$title = 'Liste des posts';
|
||||||
include __DIR__ . '/layout.php';
|
include __DIR__ . '/layout.php';
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ $files = $fileManager->getFilesForPost($post['id']);
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<?php
|
<?php
|
||||||
$fileUrl = $publicDir . '/' . $file['file_path'];
|
$fileUrl = $publicDir . '/' . $file['file_path'];
|
||||||
$type = $file['file_type'];
|
$type = $file['file_type'];
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<?php if ($type === 'image'): ?>
|
<?php if ($type === 'image'): ?>
|
||||||
<img src="<?= $fileUrl ?>" class="img-fluid" alt="<?= htmlspecialchars($file['original_name']) ?>">
|
<img src="<?= $fileUrl ?>" class="img-fluid" alt="<?= htmlspecialchars($file['original_name']) ?>">
|
||||||
|
|||||||
Reference in New Issue
Block a user